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
186 changed files with 25641 additions and 5274 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,8 @@ implementation project(":jme-common")
implementation project(":battleship:model") implementation project(":battleship:model")
implementation libs.jme3.desktop implementation libs.jme3.desktop
implementation libs.jme3.effects
runtimeOnly libs.jme3.awt.dialogs runtimeOnly libs.jme3.awt.dialogs
runtimeOnly libs.jme3.plugins runtimeOnly libs.jme3.plugins
runtimeOnly libs.jme3.jogg runtimeOnly libs.jme3.jogg

View File

@@ -23,10 +23,10 @@ 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=500 robot.delay=500

View File

@@ -1,6 +1,5 @@
package pp.battleship.client; package pp.battleship.client;
import com.jme3.app.Application;
import com.jme3.audio.AudioData.DataType; import com.jme3.audio.AudioData.DataType;
import com.jme3.audio.AudioNode; import com.jme3.audio.AudioNode;
import com.jme3.audio.AudioSource.Status; import com.jme3.audio.AudioSource.Status;
@@ -12,62 +11,63 @@
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
/**
* Class to play Background music in game
*/
public class BackgroundMusic implements GameEventListener { public class BackgroundMusic implements GameEventListener {
private static final String VOLUME_PREF = "volume"; private static final String VOLUME_PREF = "volume";
private static final String MUSIC_ENABLED_PREF = "musicEnabled"; private static final String MUSIC_ENABLED_PREF = "musicEnabled";
private final Preferences pref = Preferences.userNodeForPackage(BackgroundMusic.class); private static final Preferences PREFS = Preferences.userNodeForPackage(BackgroundMusic.class);
static final Logger LOGGER = System.getLogger(BackgroundMusic.class.getName()); static final Logger LOGGER = System.getLogger(BackgroundMusic.class.getName());
private static final String MENU_MUSIC = "Sound/Music/menu/cinematictrailerelite.ogg"; private static final String MENU_MUSIC = "Music/MainMenu/Dark_Intro.ogg";
private static final String GAME_MUSIC = "Sound/Music/game/Aluminum.ogg"; private static final String BATTLE_MUSIC = "Music/BattleTheme/boss_battle_#2_metal_loop.wav";
private static final String VICTORY_MUSIC = "Sound/Music/victory/victorymarchofvalor.ogg"; private static final String GAME_OVER_MUSIC_L = "Music/GameOver/Lose/Lose.ogg";
private static final String LOSE_MUSIC = "Sound/Music/lose/TouchofDream.ogg"; private static final String GAME_OVER_MUSIC_V = "Music/GameOver/Victory/Victory.wav";
private final AudioNode menuMusic; private final AudioNode menuMusic;
private final AudioNode gameMusic; private final AudioNode battleMusic;
private final AudioNode victoryMusic; private final AudioNode gameOverMusicL;
private final AudioNode loseMusic; private final AudioNode gameOverMusicV;
private String lastPlayedMusic; private String lastNodePlayed;
private boolean musicEnabled; private boolean musicEnabled;
private float volume; private float volume;
private Application app;
private final BattleshipApp app;
/** /**
* Constructor for BackgroundMusic class * Initializes and controls the BackgroundMusic
* *
* @param app The main Application * @param app The main Application
*/ */
public BackgroundMusic(Application app) { public BackgroundMusic(BattleshipApp app) {
this.volume = pref.getFloat(VOLUME_PREF, 1.0f); this.volume = PREFS.getFloat(VOLUME_PREF, 1.0f);
this.musicEnabled = pref.getBoolean(MUSIC_ENABLED_PREF, true); this.musicEnabled = PREFS.getBoolean(MUSIC_ENABLED_PREF, true);
this.app = app; this.app = app;
menuMusic = createMusicNode(MENU_MUSIC); menuMusic = createAudioNode(MENU_MUSIC);
gameMusic = createMusicNode(GAME_MUSIC); battleMusic = createAudioNode(BATTLE_MUSIC);
victoryMusic = createMusicNode(VICTORY_MUSIC); gameOverMusicL = createAudioNode(GAME_OVER_MUSIC_L);
loseMusic = createMusicNode(LOSE_MUSIC); gameOverMusicV = createAudioNode(GAME_OVER_MUSIC_V);
stop(gameMusic); stop(battleMusic);
stop(victoryMusic); stop(gameOverMusicL);
stop(loseMusic); stop(gameOverMusicV);
lastPlayedMusic = menuMusic.getName();
if (musicEnabled) { lastNodePlayed = menuMusic.getName();
if(musicEnabled) {
play(menuMusic); play(menuMusic);
} }
} }
/** /**
* Creates an audio node for the music * This method will be used to create the audio node containing the music
* *
* @param musicFilePath the file path to the music * @param musicFilePath the file path to the music
* @return the created audio node * @return the created audio node
*/ */
private AudioNode createAudioNode(String musicFilePath) {
private AudioNode createMusicNode(String musicFilePath) {
AudioNode audioNode = new AudioNode(app.getAssetManager(), musicFilePath, DataType.Stream); AudioNode audioNode = new AudioNode(app.getAssetManager(), musicFilePath, DataType.Stream);
audioNode.setVolume(volume); audioNode.setVolume(volume * app.getMainVolumeControl().getMainVolume());
audioNode.setPositional(false); audioNode.setPositional(false);
audioNode.setLooping(true); audioNode.setLooping(true);
audioNode.setName(musicFilePath); audioNode.setName(musicFilePath);
@@ -75,30 +75,19 @@ private AudioNode createMusicNode(String musicFilePath) {
} }
/** /**
* Starts the music * sets the give audio node to play
* *
* @param audioNode the audio node to be played * @param audioNode the audio node which should start to play
*/ */
public void play(AudioNode audioNode) { public void play(AudioNode audioNode) {
if (musicEnabled && (audioNode.getStatus() == Status.Stopped || audioNode.getStatus() == Status.Paused)) { if (musicEnabled && (audioNode.getStatus() == Status.Stopped || audioNode.getStatus() == Status.Paused)) {
audioNode.play(); audioNode.play();
lastPlayedMusic = audioNode.getName(); lastNodePlayed = audioNode.getName();
} }
} }
/** /**
* Pauses the music * stops the given audio node from playing
*
* @param audioNode the audio node to be paused
*/
public void pause(AudioNode audioNode) {
if (audioNode.getStatus() == Status.Playing) {
audioNode.pause();
}
}
/**
* Stops the music
* *
* @param audioNode the audio node to be stopped * @param audioNode the audio node to be stopped
*/ */
@@ -109,117 +98,71 @@ public void stop(AudioNode audioNode) {
} }
/** /**
* Controls which music should be played * 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() { public void toggleMusic() {
this.musicEnabled = !this.musicEnabled; this.musicEnabled = !this.musicEnabled;
if (musicEnabled) { if (musicEnabled) {
switch (lastPlayedMusic) { switch (lastNodePlayed){
case MENU_MUSIC: case MENU_MUSIC:
play(menuMusic); play(menuMusic);
break; break;
case GAME_MUSIC: case BATTLE_MUSIC:
play(gameMusic); play(battleMusic);
break; break;
case VICTORY_MUSIC: case GAME_OVER_MUSIC_L:
play(victoryMusic); play(gameOverMusicL);
break; break;
case LOSE_MUSIC: case GAME_OVER_MUSIC_V:
play(loseMusic); play(gameOverMusicV);
break; break;
} }
} } else {
else {
pause(menuMusic); pause(menuMusic);
pause(gameMusic); pause(battleMusic);
pause(victoryMusic); pause(gameOverMusicL);
pause(loseMusic); pause(gameOverMusicV);
} }
pref.putBoolean(MUSIC_ENABLED_PREF, musicEnabled); PREFS.putBoolean(MUSIC_ENABLED_PREF, musicEnabled);
} }
/** /**
* Changes the music to the specified music if it isn't already playing * this method is used when the main volume changes
*
* @param music the music to play
*/ */
public void changeMusic(Music music) { public void setVolume(){
if (music == Music.MENU_THEME && !lastPlayedMusic.equals(MENU_MUSIC)) { setVolume(PREFS.getFloat(VOLUME_PREF, 1.0f));
LOGGER.log(Level.DEBUG, "Received Music change Event {0}", music.toString());
stop(gameMusic);
stop(victoryMusic);
stop(loseMusic);
play(menuMusic);
lastPlayedMusic = menuMusic.getName();
}
else if (music == Music.GAME_THEME && !lastPlayedMusic.equals(GAME_MUSIC)) {
LOGGER.log(Level.DEBUG, "Received Music change Event {0}", music.toString());
stop(menuMusic);
stop(loseMusic);
stop(victoryMusic);
play(gameMusic);
lastPlayedMusic = gameMusic.getName();
}
else if (music == Music.VICTORY_THEME && !lastPlayedMusic.equals(VICTORY_MUSIC)) {
LOGGER.log(Level.DEBUG, "Received Music change Event {0}", music.toString());
stop(menuMusic);
stop(gameMusic);
stop(loseMusic);
play(victoryMusic);
lastPlayedMusic = victoryMusic.getName();
}
else if (music == Music.LOSE_THEME && !lastPlayedMusic.equals(LOSE_MUSIC)) {
LOGGER.log(Level.DEBUG, "Received Music change Event {0}", music.toString());
stop(menuMusic);
stop(gameMusic);
stop(victoryMusic);
play(loseMusic);
lastPlayedMusic = loseMusic.getName();
}
} }
/** /**
* Receives the different MusicEvents * Method to set the volume for the music
*
* @param music the received Event
*/
@Override
public void receivedEvent(MusicEvent music) {
LOGGER.log(Level.DEBUG, "Received Music change Event {0}", music.toString());
switch (music.music()) {
case MENU_THEME:
changeMusic(Music.MENU_THEME);
break;
case GAME_THEME:
changeMusic(Music.GAME_THEME);
break;
case VICTORY_THEME:
changeMusic(Music.VICTORY_THEME);
break;
case LOSE_THEME:
changeMusic(Music.LOSE_THEME);
break;
}
}
/**
* Set the volume for the music
* *
* @param volume float to transfer the new volume * @param volume float to transfer the new volume
*/ */
public void setVolume(float volume) { public void setVolume(float volume) {
this.volume = volume; this.volume = volume;
menuMusic.setVolume(volume); float mainVolume = app.getMainVolumeControl().getMainVolume();
gameMusic.setVolume(volume); menuMusic.setVolume(volume * mainVolume);
victoryMusic.setVolume(volume); battleMusic.setVolume(volume * mainVolume);
loseMusic.setVolume(volume); gameOverMusicL.setVolume(volume * mainVolume);
gameOverMusicV.setVolume(volume * mainVolume);
pref.putFloat(VOLUME_PREF, volume); PREFS.putFloat(VOLUME_PREF, volume);
} }
/** /**
* Returns the volume * This method retuns the volume
* *
* @return the current volume as a float * @return the current volume as a float
*/ */
@@ -235,4 +178,65 @@ public float getVolume() {
public boolean isMusicEnabled() { public boolean isMusicEnabled() {
return musicEnabled; 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

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client;
import com.jme3.app.DebugKeysAppState; import com.jme3.app.DebugKeysAppState;
@@ -116,10 +123,15 @@ 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);
/** /**
* Object handling the background music * The Object which handles the background music
*/ */
private BackgroundMusic backgroundMusic; 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();
@@ -223,6 +235,8 @@ public void simpleInitApp() {
setupStates(); setupStates();
setupGui(); setupGui();
serverConnection.connect(); serverConnection.connect();
mainVolume = new MainVolume(this);
backgroundMusic = new BackgroundMusic(this); backgroundMusic = new BackgroundMusic(this);
logic.addListener(backgroundMusic); logic.addListener(backgroundMusic);
} }
@@ -315,10 +329,6 @@ public Draw getDraw() {
return draw; return draw;
} }
public BackgroundMusic getBackgroundMusic() {
return backgroundMusic;
}
/** /**
* Handles a request to close the application. * Handles a request to close the application.
* If the request is initiated by pressing ESC, this parameter is true. * If the request is initiated by pressing ESC, this parameter is true.
@@ -430,4 +440,22 @@ void errorDialog(String errorMessage) {
.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

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;

View File

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client;
import com.jme3.app.Application; import com.jme3.app.Application;

View File

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client;
import com.jme3.app.Application; import com.jme3.app.Application;
@@ -7,8 +14,8 @@
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.Sound;
import pp.battleship.notification.SoundEvent; import pp.battleship.notification.SoundEvent;
import java.lang.System.Logger; import java.lang.System.Logger;
@@ -24,11 +31,16 @@ public class GameSound extends AbstractAppState implements GameEventListener {
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 missileLaunch; private AudioNode rocketSound;
private BattleshipApp app;
/** /**
* Checks if sound is enabled in the preferences. * Checks if sound is enabled in the preferences.
@@ -70,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
missileLaunch = loadSound(app, "Sound/Effects/missilefiring.wav"); //NON-NLS rocketSound = loadSound(app, "Sound/Effects/rocket.wav");
volume = PREFERENCES.getFloat(SOUND_VOLUME_PREF, 1.0f);
} }
/** /**
@@ -96,14 +111,6 @@ private AudioNode loadSound(Application app, String name) {
return null; return null;
} }
/**
* Plays the splash sound effect.
*/
public void missileLaunch() {
if (isEnabled() && missileLaunch != null)
missileLaunch.playInstance();
}
/** /**
* Plays the splash sound effect. * Plays the splash sound effect.
*/ */
@@ -129,17 +136,64 @@ public void shipDestroyed() {
} }
/** /**
* Plays sound according to the received SoundEvent * Plays sound effect when a rocket starts
*
* @param event the received SoundEvent
*/ */
public void rocket() {
if (isEnabled() && rocketSound != null)
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 MISSILE_LAUNCH -> missileLaunch(); 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

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client;
import com.simsilica.lemur.Button; import com.simsilica.lemur.Button;
@@ -29,7 +36,14 @@ class Menu extends Dialog {
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.
@@ -40,19 +54,26 @@ public Menu(BattleshipApp app) {
super(app.getDialogManager()); super(app.getDialogManager());
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 Label(lookup("menu.volume"), new ElementId("label")));
Checkbox musicToggle = new Checkbox(lookup("menu.music.toggle")); Checkbox musicToggle = new Checkbox(lookup("menu.music.toggle"));
musicToggle.setChecked(app.getBackgroundMusic().isMusicEnabled()); musicToggle.setChecked(app.getBackgroundMusic().isMusicEnabled());
musicToggle.addClickCommands(s -> toggleMusic()); musicToggle.addClickCommands(s -> toggleMusic());
addChild(musicToggle); addChild(musicToggle);
Slider volumeSlider = createSlider(app.getBackgroundMusic().getVolume());
addChild(new Label(lookup("menu.music.volume"), new ElementId("slider_label")));
Slider volumeSlider = new Slider();
volumeSlider.setModel(new DefaultRangedValueModel(0.00, 1.00, app.getBackgroundMusic().getVolume()));
volumeSlider.setDelta(0.05);
addChild(volumeSlider); addChild(volumeSlider);
volumeRef = volumeSlider.getModel().createReference(); volumeRef = volumeSlider.getModel().createReference();
addChild(loadButton) addChild(loadButton)
@@ -63,34 +84,70 @@ public Menu(BattleshipApp app) {
.addClickCommands(s -> ifTopDialog(this::close)); .addClickCommands(s -> ifTopDialog(this::close));
addChild(new Button(lookup("menu.quit"))) addChild(new Button(lookup("menu.quit")))
.addClickCommands(s -> ifTopDialog(app::closeApp)); .addClickCommands(s -> ifTopDialog(app::closeApp));
update(); update();
} }
/** /**
* Updates the volume when the slider is moved * 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 * @param tpf time per frame
*/ */
@Override @Override
public void update(float tpf) { public void update(float tpf){
if (volumeRef.update()) { if(volumeRef.update()){
double newVolume = volumeRef.get(); double newVolume = volumeRef.get();
adjustVolume(newVolume); adjustMusicVolume(newVolume);
}
else if (soundVolumeRef.update()) {
double newSoundVolume = soundVolumeRef.get();
adjustSoundVolume(newSoundVolume);
} else if (mainVolumeRef.update()) {
double newMainVolume = mainVolumeRef.get();
adjustMainVolume(newMainVolume);
} }
} }
/** /**
* Adjusts the volume for the background music * 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 * @param volume is the double value of the volume
*/ */
private void adjustVolume(double volume) { private void adjustMusicVolume(double volume) {
app.getBackgroundMusic().setVolume((float) volume); app.getBackgroundMusic().setVolume((float) volume);
} }
/** /**
* Toggles the background music on and off * 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() { private void toggleMusic() {
app.getBackgroundMusic().toggleMusic(); app.getBackgroundMusic().toggleMusic();

View File

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client;
import com.simsilica.lemur.Checkbox; import com.simsilica.lemur.Checkbox;
@@ -5,10 +12,10 @@
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;
import server.BattleshipServer;
import java.lang.System.Logger; import java.lang.System.Logger;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
@@ -46,9 +53,9 @@ class NetworkDialog extends SimpleDialog {
host.setPreferredWidth(400f); host.setPreferredWidth(400f);
port.setSingleLine(true); port.setSingleLine(true);
Checkbox hostServer = new Checkbox(lookup("start.own.server")); Checkbox serverHost = new Checkbox(lookup("host.own.server"));
hostServer.setChecked(false); serverHost.setChecked(false);
hostServer.addClickCommands(s -> toggleOwnServer()); 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());
@@ -56,7 +63,7 @@ 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(hostServer); input.addChild(serverHost);
DialogBuilder.simple(app.getDialogManager()) DialogBuilder.simple(app.getDialogManager())
.setTitle(lookup("server.dialog")) .setTitle(lookup("server.dialog"))
@@ -69,10 +76,10 @@ class NetworkDialog extends SimpleDialog {
} }
/** /**
* Handles the action for establishing the connection to a server. * 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 connectToServer() { 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();
@@ -85,6 +92,41 @@ private void connectToServer() {
} }
} }
/**
* This method will start a server or just connect to one based on the boolean hostServer
*/
private void connect() {
if(hostServer){
startServer();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
connectServer();
} else {
connectServer();
}
}
/**
* This method starts a server in a new thread
*/
private void startServer() {
new Thread(() -> {
try{
BattleshipServer battleshipServer = new BattleshipServer(Integer.parseInt(port.getText()));
battleshipServer.run();
} catch (Exception e) {
LOGGER.log(Level.ERROR, e.getMessage(), e);
}
}).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.
*/ */
@@ -151,46 +193,4 @@ private void failure(Throwable e) {
network.getApp().errorDialog(lookup("server.connection.failed")); network.getApp().errorDialog(lookup("server.connection.failed"));
network.getApp().setInfoText(e.getLocalizedMessage()); network.getApp().setInfoText(e.getLocalizedMessage());
} }
/**
* Handles the action for the connect-button.
* If hostServer-Checkbox is active, starts a new Server on the clients machine, else tries to connect to existing server
*/
private void connect() {
if (hostServer) {
startServer();
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
connectToServer();
}
else {
connectToServer();
}
}
/**
* Starts a server on the clients machine
*/
private void startServer() {
new Thread(() -> {
try {
BattleshipServer battleshipServer = new BattleshipServer(Integer.parseInt(port.getText()));
battleshipServer.run();
}
catch (Exception e) {
LOGGER.log(Level.ERROR, e);
}
}).start();
}
/**
* Handles the action for the hostServer-Checkbox
*/
private void toggleOwnServer() {
hostServer = !hostServer;
}
} }

View File

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client; package pp.battleship.client;
import com.jme3.network.Client; import com.jme3.network.Client;

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui; package pp.battleship.client.gui;

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui; package pp.battleship.client.gui;

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

@@ -1,181 +0,0 @@
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.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;
public class HitEffectHandler {
private final AssetManager assetManager;
private static final Logger LOGGER = System.getLogger(HitEffectHandler.class.getName());
/**
* Constructor for the HitEffectHandler class
*
* @param app the main application
*/
public HitEffectHandler(Application app) {
assetManager = app.getAssetManager();
}
/**
* Creates explosion, debris and fire effects when ship gets hit
*
* @param battleshipNode the node of the ship that gets hit and the effect should be attached to
* @param shot The shot taken on a field
*/
public void hitEffect(Node battleshipNode, Shot shot) {
//Explosion
ParticleEmitter explosion = new ParticleEmitter("Explosion", Type.Triangle, 30);
explosion.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md"));
explosion.setImagesX(2);
explosion.setImagesY(2);
explosion.setStartColor(new ColorRGBA(0.96f, 0.82f, 0.6f, 1f));
explosion.setEndColor(new ColorRGBA(0.88f, 0.32f, 0.025f, 1f));
explosion.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 1, 0));
explosion.setStartSize(0.45f);
explosion.setEndSize(0.1f);
explosion.setGravity(0, -0.5f, 0);
explosion.setLowLife(1f);
explosion.setHighLife(3.5f);
explosion.setParticlesPerSec(0);
explosion.setLocalTranslation(shot.getY() + 0.5f, 0, shot.getX() + 0.5f);
explosion.emitAllParticles();
//Debris
ParticleEmitter debris = new ParticleEmitter("Debris", Type.Triangle, 6);
Material debrisMaterial = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
debrisMaterial.setTexture("Texture", assetManager.loadTexture("Textures/Debris/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.25f, 2f, 0.25f));
debris.setStartSize(0.25f);
debris.setEndSize(0.1f);
debris.setGravity(0, 1.5f, 0);
debris.getParticleInfluencer().setVelocityVariation(0.3f);
debris.setLowLife(1f);
debris.setHighLife(3.5f);
debris.setParticlesPerSec(0);
debris.setLocalTranslation(shot.getY() + 0.5f, 0, shot.getX() + 0.5f);
debris.emitAllParticles();
//Fire
ParticleEmitter fire = new ParticleEmitter("Fire", Type.Triangle, 30);
Material fireMaterial = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
fireMaterial.setTexture("Texture", assetManager.loadTexture("Textures/Fire/fire.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(battleshipNode.getLocalTranslation());
battleshipNode.attachChild(fire);
fire.emitAllParticles();
// LOGGER.log(Level.DEBUG, "Created HitEffect at {0}", explosion.getLocalTranslation().toString());
// LOGGER.log(Level.DEBUG, "Created HitEffect at {0}", debris.getLocalTranslation().toString());
// LOGGER.log(Level.INFO, "Created HitEffect at {0}", fire.getLocalTranslation().toString());
battleshipNode.attachChild(explosion);
explosion.addControl(new EffectControl(explosion, battleshipNode));
fire.addControl(new EffectControl(fire, battleshipNode));
battleshipNode.attachChild(debris);
debris.addControl(new EffectControl(debris, battleshipNode));
}
/**
* Creates a splash effect if the shot hits the water
*
* @param shot The shot taken on a field
*/
public ParticleEmitter missEffect(Shot shot) {
ParticleEmitter missEffect = new ParticleEmitter("HitEffect", Type.Triangle, 45);
missEffect.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md"));
missEffect.setImagesX(2);
missEffect.setImagesY(2);
missEffect.setStartColor(new ColorRGBA(0.067f, 0.06f, 0.37f, 0.87f));
missEffect.setEndColor(new ColorRGBA(0.32f, 0.55f, 0.87f, 0.79f));
missEffect.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 1, 0));
missEffect.setStartSize(0.1f);
missEffect.setEndSize(0.08f);
missEffect.setGravity(0, 0.36f, 0);
missEffect.setLowLife(0.7f);
missEffect.setHighLife(1.8f);
missEffect.setParticlesPerSec(0);
missEffect.setLocalTranslation(shot.getY() + 0.5f, 0, shot.getX() + 0.5f);
LOGGER.log(Level.DEBUG, "Created MissEffect at {0}", missEffect.getLocalTranslation().toString());
missEffect.emitAllParticles();
missEffect.addControl(new EffectControl(missEffect));
return missEffect;
}
/**
* Inner class to control effects
*/
private static class EffectControl extends AbstractControl {
private final ParticleEmitter emitter;
private final Node parentNode;
/**
* Constructor used to attach effect to a 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;
}
/**
* Constructor used if the effect shouldn't be attached to a node
*
* @param emitter the particle emitter to be controlled
*/
public EffectControl(ParticleEmitter emitter) {
this.emitter = emitter;
this.parentNode = null;
}
/**
* 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

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui; package pp.battleship.client.gui;

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui; package pp.battleship.client.gui;
@@ -11,9 +16,7 @@
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 java.lang.System.Logger;
import java.lang.System.Logger.Level;
/** /**
* Synchronizes the visual representation of the ship map with the game model. * Synchronizes the visual representation of the ship map with the game model.
@@ -26,9 +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 MISSILE_DEPTH = 6f; private static final float SHELL_DEPTH = 8f;
private static final float MISSILE_SIZE = 0.8f;
private static final float MISSILE_CENTERED_IN_MAP_GRID = 0.0625f; 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;
@@ -63,14 +67,17 @@ public MapViewSynchronizer(MapView view) {
*/ */
@Override @Override
public Spatial visit(Shot shot) { public Spatial visit(Shot shot) {
LOGGER.log(Level.DEBUG, "visiting" + 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);
final ColorRGBA color = shot.isHit() ? HIT_COLOR : MISS_COLOR; final ColorRGBA color = shot.isHit() ? HIT_COLOR : MISS_COLOR;
// 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(), SHOT_DEPTH, p2.getX() - p1.getX(), p2.getY() - p1.getY(), color); return view.getApp().getDraw().makeRectangle(p1.getX(), p1.getY(),
SHOT_DEPTH,
p2.getX() - p1.getX(), p2.getY() - p1.getY(),
color);
} }
/** /**
@@ -113,29 +120,24 @@ public Spatial visit(Battleship ship) {
} }
/** /**
* Creates a visual representation on the map * this method will create a representation of a shell in the map
* *
* @param shell the Shell element to visit * @param shell the Shell element to visit
* @return the node the visual representation gets attached to. * @return the node the representation is attached to
*/ */
@Override @Override
public Spatial visit(Shell shell) { public Spatial visit(Shell shell) {
LOGGER.log(Logger.Level.DEBUG, "Visiting {0}", shell); LOGGER.log(Logger.Level.DEBUG, "Visiting {0}", shell);
final Node missileNode = new Node("missile"); final Node shellNode = new Node("shell");
final Position p1 = view.modelToView(shell.getX(), shell.getY()); final Position p1 = view.modelToView(shell.getX(), shell.getY());
final Position p2 = view.modelToView(shell.getX() + MISSILE_SIZE, shell.getY() + MISSILE_SIZE); final Position p2 = view.modelToView(shell.getX() + SHELL_SIZE, shell.getY() + SHELL_SIZE);
final float x1 = p1.getX() + INDENT; final Position startPosition = view.modelToView(SHELL_CENTERED_IN_MAP_GRID, SHELL_CENTERED_IN_MAP_GRID);
final float y1 = p1.getY() + INDENT;
final float x2 = p2.getX() - INDENT;
final float y2 = p2.getY() - INDENT;
final Position startPosition = view.modelToView(MISSILE_CENTERED_IN_MAP_GRID, MISSILE_CENTERED_IN_MAP_GRID); 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);
missileNode.attachChild(view.getApp().getDraw().makeRectangle(startPosition.getX(), startPosition.getY(), MISSILE_DEPTH, p2.getX() - p1.getX(), p2.getY() - p1.getY(), ColorRGBA.DarkGray)); shellNode.addControl(new ShellMapControl(p1, view.getApp(), new IntPoint(shell.getX(), shell.getY())));
missileNode.setLocalTranslation(startPosition.getX(), startPosition.getY(), MISSILE_DEPTH); return shellNode;
missileNode.addControl(new ShellMapControl(p1, view.getApp(), new IntPoint(shell.getX(), shell.getY())));
return missileNode;
} }
/** /**
@@ -149,7 +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"); 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

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui; package pp.battleship.client.gui;

View File

@@ -1,24 +1,20 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui; package pp.battleship.client.gui;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
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 com.jme3.scene.shape.Cylinder;
import pp.battleship.client.BattleshipApp; import pp.battleship.client.BattleshipApp;
import pp.battleship.model.Battleship; import pp.battleship.model.*;
import pp.battleship.model.Shell;
import pp.battleship.model.Rotation;
import pp.battleship.model.ShipMap;
import pp.battleship.model.Shot;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static pp.util.FloatMath.HALF_PI; import static pp.util.FloatMath.HALF_PI;
@@ -33,20 +29,14 @@
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"; private static final String KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o";
private static final String UBOAT_MODEL = "Models/UBOAT/14084_WWII_Ship_German_Type_II_U-boat_v2_L1.obj"; private static final String UBOAT = "Models/UBoat/14084_WWII_Ship_German_Type_II_U-boat_v2_L1.obj"; //NON-NLS
private static final String PATROL_BOAT_MODEL = "Models/PATROL_BOAT/12219_boat_v2_L2.obj"; private static final String BATTLE_SHIP_MODERN = "Models/BattleShipModern/Destroyer.j3o";
private static final String MODERN_BATTLESHIP_MODEL = "Models/BATTLESHIP/10619_Battleship.obj"; private static final String BATTLE_SHIP_MODERN_TEXTURE = "Models/BattleShipModern/BattleshipC.jpg";
private static final String MODERN_BATTLESHIP_TEXTURES = "Models/BATTLESHIP/BattleshipC.jpg"; private static final String PATROL_BOAT = "Models/PatrolBoat/12219_boat_v2_L2.obj";
private static final String MISSILE_MODEL = "Models/Missile/AIM120D.obj"; private static final String SHELL_ROCKET = "Models/Rocket/Rocket.obj";
private static final String MISSILE_TEXTURE = "Models/Missile/texture.png";
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 SHOT = "shot"; //NON-NLS private static final String SHELL = "shell";
private static final String MISSILE = "missile"; //NON-NLS private final EffectHandler effectHandler;
private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray;
private static final ColorRGBA SPLASH_COLOR = new ColorRGBA(0f, 0f, 1f, 0.4f);
private static final ColorRGBA HIT_COLOR = new ColorRGBA(1f, 0f, 0f, 0.4f);
private final HitEffectHandler hitEffectHandler;
private final ShipMap map; private final ShipMap map;
private final BattleshipApp app; private final BattleshipApp app;
@@ -62,7 +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;
hitEffectHandler = new HitEffectHandler(app); effectHandler = new EffectHandler(app);
addExisting(); addExisting();
} }
@@ -76,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) : hitEffectHandler.missEffect(shot); return shot.isHit() ? handleHit(shot) : effectHandler.createMissEffect(shot);
} }
/** /**
@@ -91,33 +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");
hitEffectHandler.hitEffect(shipNode, shot); effectHandler.createHitEffect(shipNode, shot);
effectHandler.createFireEffect(shipNode, shot);
return null; return null;
} }
/**
* Creates a cylinder geometry representing the specified shot.
* The appearance of the cylinder depends on whether the shot is a hit or a miss.
*
* @param shot the shot to be represented
* @return the geometry representing the shot
*/
private Geometry createCylinder(Shot shot) {
final ColorRGBA color = shot.isHit() ? HIT_COLOR : SPLASH_COLOR;
final float height = shot.isHit() ? 1.2f : 0.1f;
final Cylinder cylinder = new Cylinder(2, 20, 0.45f, height, true);
final Geometry geometry = new Geometry(SHOT, cylinder);
geometry.setMaterial(createColoredMaterial(color));
geometry.rotate(HALF_PI, 0f, 0f);
// compute the center of the shot in world coordinates
geometry.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
return geometry;
}
/** /**
* 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
@@ -139,23 +108,41 @@ public Spatial visit(Battleship ship) {
} }
/** /**
* Visits a Shell and creates a graphical representation of it. * Visits a shell and creates a graphical representation
* *
* @param shell the Shell to be represented * @param shell the Shell element to visit
* @return the node containing the graphical representation of the Shell * @return the node containing the graphical representation
*/ */
@Override @Override
public Spatial visit(Shell shell) { public Spatial visit(Shell shell){
final Node node = new Node(MISSILE); final Node node = new Node(SHELL);
node.attachChild(createMissile()); node.attachChild(createRocket());
final float x = shell.getY(); final float x = shell.getY();
final float z = shell.getX(); final float z = shell.getX();
node.setLocalTranslation(x + 0.5f, 10f, z + 0.5f); node.setLocalTranslation(x + 0.5f, 10f, z + 0.5f);
node.addControl(new ShellControl(shell, app)); ShellControl shellControl = new ShellControl(shell, app);
node.addControl(shellControl);
return node; 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.
@@ -167,48 +154,12 @@ private Spatial createShip(Battleship ship) {
return switch (ship.getLength()) { return switch (ship.getLength()) {
case 1 -> createPatrolBoat(ship); case 1 -> createPatrolBoat(ship);
case 2 -> createModernBattleship(ship); case 2 -> createModernBattleship(ship);
case 3 -> createUboat(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(BOX_COLOR));
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.
*
* @param color the {@link ColorRGBA} to be applied to the material. If the alpha value
* of the color is less than 1, the material will support transparency.
* @return a {@link Material} instance configured with the specified color and,
* if necessary, alpha blending enabled.
*/
private Material createColoredMaterial(ColorRGBA color) {
final Material material = new Material(app.getAssetManager(), UNSHADED);
if (color.getAlpha() < 1f)
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
material.setColor(COLOR, 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.
* *
@@ -226,80 +177,58 @@ private Spatial createBattleship(Battleship ship) {
} }
/** /**
* Creates a detailed 3D model to represent a modern 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 battleship
*/
private Spatial createModernBattleship(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(MODERN_BATTLESHIP_MODEL);
Material mat = new Material(app.getAssetManager(), UNSHADED);
mat.setTexture("ColorMap", app.getAssetManager().loadTexture(MODERN_BATTLESHIP_TEXTURES));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
model.setMaterial(mat);
model.setQueueBucket(RenderQueue.Bucket.Opaque);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()) + HALF_PI, 0f);
model.scale(0.000075f);
model.setShadowMode(ShadowMode.CastAndReceive);
model.move(0, 0.2f, 0);
return model;
}
/**
* Creates a detailed 3D model to represent a missile.
*
* @return the spatial representing the missile
*/
private Spatial createMissile() {
final Spatial model = app.getAssetManager().loadModel(MISSILE_MODEL);
Material mat = new Material(app.getAssetManager(), UNSHADED);
mat.setTexture("ColorMap", app.getAssetManager().loadTexture(MISSILE_TEXTURE));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
model.setMaterial(mat);
model.setQueueBucket(RenderQueue.Bucket.Opaque);
model.rotate(-HALF_PI, 0, 0);
model.scale(0.009f);
model.setShadowMode(ShadowMode.CastAndReceive);
model.move(0, 0f, 0);
return model;
}
/**
* Creates a detailed 3D model to represent a Uboat.
*
* @param ship the battleship to be represented
* @return the spatial representing the Uboat * @return the spatial representing the Uboat
*/ */
private Spatial createUBoat(Battleship ship) {
private Spatial createUboat(Battleship ship) { final Spatial model = app.getAssetManager().loadModel(UBOAT);
final Spatial model = app.getAssetManager().loadModel(UBOAT_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f); model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.45f); model.scale(0.5f);
model.setShadowMode(ShadowMode.CastAndReceive); model.setShadowMode(ShadowMode.CastAndReceive);
model.move(0, -0.25f, 0); model.move(0, -0.3f, 0);
return model; return model;
} }
/** /**
* Creates a detailed 3D model to represent a Patrol boat. * creates a detailed 3D model to represent the modern battleship
* *
* @param ship the battleship to be represented * @param ship the ship to be represented
* @return the spatial representing the Patrol boat * @return the spatial representing the Modern Battleship
*/ */
private Spatial createModernBattleship(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(BATTLE_SHIP_MODERN);
private Spatial createPatrolBoat(Battleship ship) { Material mat = new Material(app.getAssetManager(), UNSHADED);
final Spatial model = app.getAssetManager().loadModel(PATROL_BOAT_MODEL); mat.setTexture("ColorMap", app.getAssetManager().loadTexture(BATTLE_SHIP_MODERN_TEXTURE));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
model.setMaterial(mat);
model.setQueueBucket(RenderQueue.Bucket.Opaque);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f); model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.00045f); model.scale(0.08f);
model.setLocalTranslation(0f, 0.2f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/**
* creates a detailed 3D model to represent the patrol boat
*
* @param ship the ship to be represented
* @return the spatial representing the patrol boat
*/
private Spatial createPatrolBoat(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(PATROL_BOAT);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.0005f);
model.setShadowMode(ShadowMode.CastAndReceive); model.setShadowMode(ShadowMode.CastAndReceive);
return model; return model;

View File

@@ -1,31 +1,32 @@
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.client.BattleshipApp;
import pp.battleship.message.client.EndAnimationMessage; import pp.battleship.message.client.AnimationEndMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import java.lang.System.Logger; import java.lang.System.Logger;
import java.lang.System.Logger.Level;
/** /**
* Class to control the 3D representation of a shell * This class controls a 3D representation of a shell
*/ */
public class ShellControl extends AbstractControl { public class ShellControl extends AbstractControl {
private final Shell shell; private final Shell shell;
private final BattleshipApp app; private final BattleshipApp app;
private static final float TRAVEL_SPEED = 8.5f;
static final Logger LOGGER = System.getLogger(BattleshipApp.class.getName()); private static final float MOVE_SPEED = 8.0f;
static final Logger LOGGER = System.getLogger(ShellControl.class.getName());
/** /**
* Constructor for ShellControl class * Constructor to create a new ShellControl object
* *
* @param shell The Shell to be displayed * @param shell the shell to be displayed
* @param app the main application * @param app the main application
*/ */
public ShellControl(Shell shell, BattleshipApp app) { public ShellControl(Shell shell, BattleshipApp app) {
this.shell = shell; this.shell = shell;
@@ -33,17 +34,19 @@ public ShellControl(Shell shell, BattleshipApp app) {
} }
/** /**
* Method to control movement of the Shell and remove it when target is reached * this method moves the representation towards it destination
* and deletes it if it reaches its target
* *
* @param tpf time per frame (in seconds) * @param tpf time per frame (in seconds)
*/ */
@Override @Override
public void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
//LOGGER.log(Level.DEBUG, "missile at x=" + shell.getX() + ", y=" + shell.getY()); spatial.move(0, -MOVE_SPEED * tpf, 0);
spatial.move(0, -TRAVEL_SPEED * tpf, 0); spatial.rotate(0f, 0.05f, 0f);
if (spatial.getLocalTranslation().getY() <= 0.2) { //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 EndAnimationMessage(new IntPoint(shell.getX(), shell.getY()))); app.getGameLogic().send(new AnimationEndMessage(new IntPoint(shell.getX(), shell.getY())));
} }
} }

View File

@@ -5,45 +5,46 @@
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.client.BattleshipApp;
import pp.battleship.message.client.EndAnimationMessage; import pp.battleship.message.client.AnimationEndMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.util.Position; import pp.util.Position;
/** /**
* Class to control the 2D representation of a shell * This class controls a ShellMap element
*/ */
public class ShellMapControl extends AbstractControl { public class ShellMapControl extends AbstractControl {
private final Position position; private final Position position;
private final IntPoint pos; private final IntPoint pos;
private static final Vector3f VECTOR_3_F = new Vector3f(); private static final Vector3f VECTOR = new Vector3f();
private final BattleshipApp app; private final BattleshipApp app;
/** /**
* Constructor for ShellMapControl * constructs a new ShellMapControl object
* *
* @param position the target position of the shell * @param position the position where the shell should move to on the map
* @param app the main application * @param app the main application
* @param pos the position the then to render shot goes to * @param pos the position the then to render shot goes to
*/ */
public ShellMapControl(Position position, BattleshipApp app, IntPoint pos) { public ShellMapControl(Position position, BattleshipApp app, IntPoint pos) {
super(); super();
this.position = position; this.position = position;
this.pos = pos; this.pos = pos;
this.app = app; this.app = app;
VECTOR_3_F.set(new Vector3f(position.getX(), position.getY(), 0)); VECTOR.set(new Vector3f(position.getX(), position.getY(), 0));
} }
/** /**
* Method to control movement of the Shell on the map and remove it when target is reached * this method moves the shell representation to its correct spot and removes it after
* it arrived at its destination
* *
* @param tpf time per frame (in seconds) * @param tpf time per frame (in seconds)
*/ */
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
spatial.move(VECTOR_3_F.mult(tpf)); spatial.move(VECTOR.mult(tpf));
if (spatial.getLocalTranslation().getX() >= position.getX() && spatial.getLocalTranslation().getY() >= position.getY()) { if (spatial.getLocalTranslation().getX() >= position.getX() && spatial.getLocalTranslation().getY() >= position.getY()) {
spatial.getParent().detachChild(spatial); spatial.getParent().detachChild(spatial);
app.getGameLogic().send(new EndAnimationMessage(pos)); app.getGameLogic().send(new AnimationEndMessage(pos));
} }
} }

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui; package pp.battleship.client.gui;
@@ -41,16 +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;
/**
* Ship to be controlled
*/
private final Battleship battleship;
private static final Logger LOGGER = System.getLogger(ShipControl.class.getName()); /**
* The ship to be controlled
*/
private final Battleship battleship;
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,7 +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) {
this.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;
@@ -75,6 +92,7 @@ public ShipControl(Battleship 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
*/ */
@@ -83,14 +101,12 @@ 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;
if (battleship.isDestroyed() && spatial.getLocalTranslation().getY() < -0.6f) { if (battleship.isDestroyed() && spatial.getLocalTranslation().getY() <= SHIP_SINKING_REMOVE_THRESHOLD) {
LOGGER.log(Level.INFO, "Ship removed {0}", spatial.getName()); LOGGER.log(Level.INFO, "Ship removed {0}", spatial.getName());
spatial.getParent().detachChild(spatial); spatial.getParent().detachChild(spatial);
} } else if (battleship.isDestroyed()) {
else if (battleship.isDestroyed()) { spatial.move(0, SINKING_SPEED * tpf, 0);
spatial.move(0, -0.2f * tpf, 0); } else {
}
else {
// Update the time within the oscillation cycle // Update the time within the oscillation cycle
time = (time + tpf) % cycle; time = (time + tpf) % cycle;
@@ -99,10 +115,11 @@ else if (battleship.isDestroyed()) {
// Update the pitch Quaternion with the new angle // Update the pitch Quaternion with the new angle
pitch.fromAngleAxis(angle, axis); pitch.fromAngleAxis(angle, axis);
// Apply the pitch rotation to the spatial
spatial.setLocalRotation(pitch);
} }
// Apply the pitch rotation to the spatial
spatial.setLocalRotation(pitch);
} }
/** /**

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui; package pp.battleship.client.gui;

View File

@@ -1,6 +1,11 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.server;
package server;
import com.jme3.network.ConnectionListener; import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection; import com.jme3.network.HostedConnection;
@@ -13,16 +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.AnimationEndMessage;
import pp.battleship.message.client.ClientMessage; import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.EndAnimationMessage;
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.StartAnimationMessage;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.message.server.SwitchToBattleState;
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;
@@ -72,18 +72,12 @@ public BattleshipServer(int port) {
logic = new ServerGameLogic(this, config); logic = new ServerGameLogic(this, config);
} }
/**
* Runs a server
*/
public void run() { public void run() {
startServer(); startServer();
while (true) while (true)
processNextMessage(); processNextMessage();
} }
/**
* Starts a server
*/
private void startServer() { private void startServer() {
try { try {
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
@@ -99,9 +93,6 @@ private void startServer() {
} }
} }
/**
* Processes next received message
*/
private void processNextMessage() { private void processNextMessage() {
try { try {
pendingMessages.take().process(logic); pendingMessages.take().process(logic);
@@ -112,9 +103,6 @@ private void processNextMessage() {
} }
} }
/**
* Registers all serializable classes
*/
private void initializeSerializables() { private void initializeSerializables() {
Serializer.registerClass(GameDetails.class); Serializer.registerClass(GameDetails.class);
Serializer.registerClass(StartBattleMessage.class); Serializer.registerClass(StartBattleMessage.class);
@@ -124,26 +112,18 @@ private void initializeSerializables() {
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(StartAnimationMessage.class); Serializer.registerClass(AnimationEndMessage.class);
Serializer.registerClass(EndAnimationMessage.class); Serializer.registerClass(AnimationStartMessage.class);
Serializer.registerClass(SwitchToBattleState.class); Serializer.registerClass(SwitchBattleState.class);
} }
/**
* Registers all listeners
*/
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, EndAnimationMessage.class); myServer.addMessageListener(this, AnimationEndMessage.class);
myServer.addConnectionListener(this); myServer.addConnectionListener(this);
} }
/**
* Handles a received message
* @param source the connection the message comes from
* @param message the received message
*/
@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
@@ -151,22 +131,12 @@ public void messageReceived(HostedConnection source, Message message) {
pendingMessages.add(new ReceivedMessage(clientMessage, source.getId())); pendingMessages.add(new ReceivedMessage(clientMessage, source.getId()));
} }
/**
* Adds a new connection to a server
* @param server the server to add the connection to
* @param hostedConnection the connection to be 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());
} }
/**
* Removes a standing connection from a server
* @param server the server to add the connection to
* @param hostedConnection the connection to be added
*/
@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
@@ -179,10 +149,6 @@ public void connectionRemoved(Server server, HostedConnection hostedConnection)
} }
} }
/**
* Shuts down the server and terminates the application with the given exit code
* @param exitValue the exit status code
*/
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

@@ -1,15 +1,15 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.server;
package 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 message received from a client
* @param message the received message
* @param from the ID of the client that sent the message
*/
record ReceivedMessage(ClientMessage message, int from) { record ReceivedMessage(ClientMessage message, int from) {
void process(ClientInterpreter interpreter) { void process(ClientInterpreter interpreter) {
message.accept(interpreter, from); message.accept(interpreter, from);

View File

@@ -0,0 +1,3 @@
based on:
https://free3d.com/3d-model/battleship-v1--611736.html
License: Free Personal Use Only

View File

@@ -1,12 +0,0 @@
# Blender MTL File: 'AIM120D.blend'
# Material Count: 1
newmtl Material.006
Ns 96.078431
Ka 0.000000 0.000000 0.000000
Kd 0.640000 0.640000 0.640000
Ks 0.500000 0.500000 0.500000
Ni 1.000000
d 1.000000
illum 2
map_Kd texture.png

View File

@@ -1,2 +0,0 @@
AIM-120D Missile (Air-to-Air) by https://free3d.com/3d-model/aim-120d-shell-air-to-air-20348.html
License: License for personal use

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 MiB

View File

@@ -0,0 +1,3 @@
based on:
https://free3d.com/3d-model/boat-v2--225787.html
License: Free Personal Use Only

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
based on:
https://free3d.com/3d-model/wwii-ship-german-type-ii-uboat-v2--700733.html
License: Free Personal Use Only

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,2 +0,0 @@
Missile firing fl by NHMWretched (https://pixabay.com/sound-effects/missile-firing-fl-106655/)
CCO License

View File

@@ -1 +0,0 @@
Aluminum | Roie Shpigler | https://artlist.io/royalty-free-music/song/aluminum/122360

View File

@@ -1 +0,0 @@
A Touch of Dream | Max H. | https://artlist.io/royalty-free-music/song/a-touch-of-dream/126111

View File

@@ -1,4 +0,0 @@
Epic Cinematic Trailer | ELITE by Alex-Productions | https://onsound.eu/
Music promoted by https://www.chosic.com/free-music/all/
Creative Commons CC BY 3.0
https://creativecommons.org/licenses/by/3.0/

View File

@@ -1 +0,0 @@
Victory march of Valor | Land_of_Books_YouTube | https://pixabay.com/users/land_of_books_youtube-7733644/s

View File

@@ -1,2 +0,0 @@
Created using Metal Plates 13 from ambientCG.com,
licensed under the Creative Commons CC0 1.0 Universal License.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 MiB

View File

@@ -1,2 +0,0 @@
https://www.rawpixel.com/image/13141087/png-fire-bonfire-illuminated-destruction-generated-image-rawpixel
Licence: Free for personal use

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.exporter; package pp.battleship.exporter;
@@ -36,7 +41,7 @@ public static void main(String[] args) {
*/ */
@Override @Override
public void simpleInitApp() { public void simpleInitApp() {
export("Models/KingGeorgeV/King_George_V.obj", "Models/KingGeorgeV/KingGeorgeV.j3o"); //NON-NLS export("Models/KingGeorgeV/King_George_V.obj", "KingGeorgeV.j3o"); //NON-NLS
stop(); stop();
} }

View File

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship; package pp.battleship;
import pp.util.config.Config; import pp.util.config.Config;

View File

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship; package pp.battleship;
import java.util.ResourceBundle; import java.util.ResourceBundle;

View File

@@ -1,44 +1,37 @@
package pp.battleship.game.client; package pp.battleship.game.client;
import pp.battleship.message.client.EndAnimationMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.SwitchToBattleState; 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;
import java.lang.System.Logger.Level;
/**
* Represents the state in which the animation is played
*/
public class AnimationState extends ClientState { public class AnimationState extends ClientState {
private boolean myTurn; private boolean myTurn;
/** /**
* Constructor for the AnimationState class * creates an object of AnimationState
* *
* @param logic the client logic * @param logic the client logic
* @param turn a boolean containing if it's the client's turn * @param myTurn a boolean containing if it is the clients turn
* @param position the position a Shell gets created * @param position the position a shell should be created
*/ */
public AnimationState(ClientGameLogic logic, boolean turn, IntPoint position) { public AnimationState(ClientGameLogic logic, boolean myTurn, IntPoint position) {
super(logic); super(logic);
logic.playMusic(Music.GAME_THEME); logic.playMusic(Music.BATTLE_THEME);
myTurn = turn; this.myTurn = myTurn;
if (myTurn) { if(myTurn) {
logic.getOpponentMap().add(new Shell(position)); logic.getOpponentMap().add(new Shell(position));
} }else {
else {
logic.getOwnMap().add(new Shell(position)); logic.getOwnMap().add(new Shell(position));
logic.playSound(Sound.ROCKET_FIRED);
} }
} }
/** /**
* Makes sure the client renders the correct view * This method makes sure the client renders the correct view
* *
* @return true * @return true
*/ */
@@ -54,13 +47,14 @@ boolean showBattle() {
*/ */
@Override @Override
public void receivedEffect(EffectMessage msg) { public void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS ClientGameLogic.LOGGER.log(System.Logger.Level.INFO, "report effect: {0}", msg); //NON-NLS
playSound(msg); playSound(msg);
myTurn = msg.isMyTurn(); myTurn = msg.isMyTurn();
logic.setInfoText(msg.getInfoTextKey()); logic.setInfoText(msg.getInfoTextKey());
affectedMap(msg).add(msg.getShot()); affectedMap(msg).add(msg.getShot());
if (destroyedOpponentShip(msg)) if (destroyedOpponentShip(msg)) {
logic.getOpponentMap().add(msg.getDestroyedShip()); logic.getOpponentMap().add(msg.getDestroyedShip());
}
if (msg.isGameOver()) { if (msg.isGameOver()) {
msg.getRemainingOpponentShips().forEach(logic.getOpponentMap()::add); msg.getRemainingOpponentShips().forEach(logic.getOpponentMap()::add);
logic.setState(new GameOverState(logic, msg.isGameLost())); logic.setState(new GameOverState(logic, msg.isGameLost()));
@@ -68,13 +62,13 @@ public void receivedEffect(EffectMessage msg) {
} }
/** /**
* Sets the client back to the battle state * this method is used to change the client to the battle state again
* *
* @param msg the received SwitchToBattleState message * @param msg the message to process
*/ */
@Override @Override
public void receivedSwitchToBattleState(SwitchToBattleState msg) { public void receivedSwitchBattleState(SwitchBattleState msg) {
logic.setState(new BattleState(logic, msg.getTurn())); logic.setState(new BattleState(logic, msg.isTurn()));
} }
/** /**

View File

@@ -1,16 +1,16 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
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.message.server.StartAnimationMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.notification.Music; import pp.battleship.notification.Music;
import pp.battleship.notification.Sound;
import java.lang.System.Logger.Level;
/** /**
* Represents the state of the client where players take turns to attack each other's ships. * Represents the state of the client where players take turns to attack each other's ships.
@@ -26,12 +26,12 @@ class BattleState extends ClientState {
*/ */
public BattleState(ClientGameLogic logic, boolean myTurn) { public BattleState(ClientGameLogic logic, boolean myTurn) {
super(logic); super(logic);
logic.playMusic(Music.GAME_THEME); logic.playMusic(Music.BATTLE_THEME);
this.myTurn = myTurn; this.myTurn = myTurn;
} }
/** /**
* Makes sure the client renders the correct view * This method makes sure the client renders the correct view
* *
* @return true * @return true
*/ */
@@ -40,11 +40,6 @@ public boolean showBattle() {
return true; return true;
} }
/**
* Triggers a shoot event if it's client's turn
*
* @param pos the position where the click occurred
*/
@Override @Override
public void clickOpponentMap(IntPoint pos) { public void clickOpponentMap(IntPoint pos) {
if (!myTurn) if (!myTurn)
@@ -53,14 +48,8 @@ else if (logic.getOpponentMap().isValid(pos))
logic.send(new ShootMessage(pos)); logic.send(new ShootMessage(pos));
} }
/**
* Triggers an animation if StartAnimationMessage is received
*
* @param msg the received Startanimation message
*/
@Override @Override
public void receivedStartAnimation(StartAnimationMessage msg) { public void receivedAnimationStart(AnimationStartMessage msg){
logic.setState(new AnimationState(logic, msg.isMyTurn(), msg.getPosition())); logic.setState(new AnimationState(logic, msg.isMyTurn(), msg.getPosition()));
logic.playSound(Sound.MISSILE_LAUNCH);
} }
} }

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client; package pp.battleship.game.client;

View File

@@ -1,14 +1,14 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
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.StartAnimationMessage;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.message.server.SwitchToBattleState;
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;
@@ -226,23 +226,23 @@ public void received(EffectMessage msg) {
} }
/** /**
* Reports that client should play an animation * Reports that the client should start an animation
* *
* @param msg * @param msg the AnimationStartMessage received
*/ */
@Override @Override
public void received(StartAnimationMessage msg) { public void received(AnimationStartMessage msg) {
state.receivedStartAnimation(msg); state.receivedAnimationStart(msg);
} }
/** /**
* Reports that client should switch to the battle state * Reports that the client should move to the battle state
* *
* @param msg * @param msg the SwitchBattleState received
*/ */
@Override @Override
public void received(SwitchToBattleState msg) { public void received(SwitchBattleState msg) {
state.receivedSwitchToBattleState(msg); state.receivedSwitchBattleState(msg);
} }
/** /**
@@ -277,6 +277,15 @@ public void playSound(Sound sound) {
notifyListeners(new SoundEvent(sound)); notifyListeners(new SoundEvent(sound));
} }
/**
* Emits an event to play the specified music
*
* @param music the music to be played
*/
public void playMusic(Music music) {
notifyListeners(new MusicEvent(music));
}
/** /**
* Loads a map from the specified file. * Loads a map from the specified file.
* *
@@ -323,7 +332,7 @@ public void saveMap(File file) throws IOException {
* *
* @param msg the message to be sent * @param msg the message to be sent
*/ */
public 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
@@ -371,13 +380,4 @@ public void notifyListeners(GameEvent event) {
public void update(float delta) { public void update(float delta) {
state.update(delta); state.update(delta);
} }
/**
* Triggers an event to play specified music
*
* @param music the music to be played
*/
public void playMusic(Music music) {
notifyListeners(new MusicEvent(music));
}
} }

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client; package pp.battleship.game.client;

View File

@@ -1,12 +1,13 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
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.StartAnimationMessage;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.message.server.SwitchToBattleState;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import java.io.File; import java.io.File;
@@ -163,21 +164,21 @@ void receivedEffect(EffectMessage msg) {
} }
/** /**
* Reports that client should switch to battle state * Reports that the client should start an animation
* *
* @param msg the received SwitchToBattleState message * @param msg the AnimationStartMessage received
*/ */
void receivedSwitchToBattleState(SwitchToBattleState msg) { void receivedAnimationStart(AnimationStartMessage msg){
ClientGameLogic.LOGGER.log(Level.ERROR, "receivedSwitchToBattleState not allowed in {0}", getName()); ClientGameLogic.LOGGER.log(Level.ERROR, "receivedEffect not allowed in {0}", getName());
} }
/** /**
* Reports that the client should start an animation * Reports that the client should move to the battle state
* *
* @param msg the received StartAnimation message * @param msg the SwitchBattleState received
*/ */
void receivedStartAnimation(StartAnimationMessage msg) { void receivedSwitchBattleState(SwitchBattleState msg){
ClientGameLogic.LOGGER.log(Level.ERROR, "receivedStartAnimation not allowed in {0}", getName()); ClientGameLogic.LOGGER.log(Level.ERROR, "receivedSwitchBattleState not allowed in {0}", getName());
} }
/** /**

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client; package pp.battleship.game.client;
@@ -247,10 +252,10 @@ else if (!checkMapToLoad(dto)) {
} }
/** /**
* Checks if the provided map meets the requirements * This method is used to check if the loaded map is correct
* *
* @param dto the data transfer object to check * @param dto the data transfer object to check
* @return boolean if the map meets the requirements * @return boolean if map is correct or not
*/ */
private boolean checkMapToLoad(ShipMapDTO dto) { private boolean checkMapToLoad(ShipMapDTO dto) {
int mapWidth = dto.getWidth(); int mapWidth = dto.getWidth();
@@ -267,15 +272,16 @@ private boolean checkMapToLoad(ShipMapDTO dto) {
// check if ships overlap // check if ships overlap
List<Battleship> ships = dto.getShips(); List<Battleship> ships = dto.getShips();
for (Battleship ship : ships) { for(Battleship ship:ships){
for (Battleship compareShip : ships) { for(Battleship compareShip:ships){
if (!(ship == compareShip)) { if(!(ship==compareShip)){
if (ship.collidesWith(compareShip)) { if(ship.collidesWith(compareShip)){
return false; return false;
} }
} }
} }
} }
return true; return true;
} }

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client; package pp.battleship.game.client;
@@ -13,13 +18,12 @@ class GameOverState extends ClientState {
* *
* @param logic the client game logic * @param logic the client game logic
*/ */
GameOverState(ClientGameLogic logic, boolean loser) { GameOverState(ClientGameLogic logic, boolean lost) {
super(logic); super(logic);
if (loser) { if (lost){
logic.playMusic(Music.LOSE_THEME); logic.playMusic(Music.GAME_OVER_THEME_L);
} } else {
else { logic.playMusic(Music.GAME_OVER_THEME_V);
logic.playMusic(Music.VICTORY_THEME);
} }
} }

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client; package pp.battleship.game.client;
@@ -53,11 +58,6 @@ private void fillHarbor(GameDetails details) {
} }
} }
/**
* Checks if map may be saved to file
*
* @return false
*/
@Override @Override
public boolean maySaveMap() { public boolean maySaveMap() {
return false; return false;

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client; package pp.battleship.game.client;

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.client; package pp.battleship.game.client;
@@ -36,14 +41,14 @@ public void receivedStartBattle(StartBattleMessage msg) {
} }
/** /**
* Reverts the client back to the editor state if an invalid map is provided * This method will revert the client from wait state to editor state
* in case a wrong map was submitted
* *
* @param details the game details including map size and ships * @param details the game details including map size and ships
*/ */
@Override @Override
public void receivedGameDetails(GameDetails details) { 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

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server; package pp.battleship.game.server;

View File

@@ -1,22 +1,28 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server; package pp.battleship.game.server;
import pp.battleship.BattleshipConfig; import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.AnimationEndMessage;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.EndAnimationMessage;
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.StartAnimationMessage;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.message.server.SwitchToBattleState;
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;
@@ -35,8 +41,8 @@ public class ServerGameLogic implements ClientInterpreter {
private Player activePlayer; private Player activePlayer;
private ServerState state = ServerState.WAIT; private ServerState state = ServerState.WAIT;
private boolean p1AnimationFinished = false; private boolean player1AnimationReady = false;
private boolean p2AnimationFinished = false; private boolean player2AnimationReady = false;
/** /**
* Constructs a ServerGameLogic with the specified sender and configuration. * Constructs a ServerGameLogic with the specified sender and configuration.
@@ -142,69 +148,38 @@ 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 (!checkMap(msg, from)) { else if (!checkMap(msg, from)) {
LOGGER.log(Level.ERROR, "The submitted map is not allowed"); LOGGER.log(Level.ERROR, "player submitted not allowed Map");
send(players.get(from), new GameDetails(config)); send(getPlayerById(from), new GameDetails(config));
} }
else else
playerReady(getPlayerById(from), msg.getShips()); playerReady(getPlayerById(from), msg.getShips());
} }
/**
* Handles the reception of an EndAnimation message
* @param msg received EndAnimation message
*/
@Override
public void received(EndAnimationMessage msg, int from) {
if (state != ServerState.WAIT_ANIMATION)
LOGGER.log(Level.ERROR, "animation not allowed in {0}", state);
else if (getPlayerById(from) == players.get(0)) {
LOGGER.log(Level.DEBUG, "{0} set to true", getPlayerById(from));
p1AnimationFinished = 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());
p2AnimationFinished = true;
shoot(getPlayerById(from), msg.getPosition());
}
if (p1AnimationFinished && p2AnimationFinished) {
setState(ServerState.BATTLE);
for (Player player : players)
send(player, new SwitchToBattleState(player == activePlayer));
p1AnimationFinished = false;
p2AnimationFinished = false;
}
}
/** /**
* Returns true if the map contains correct ship placement and is of the correct size * Returns true if the map contains correct ship placement and is of the correct size
* *
* @param msg the received MapMessage of the player * @param msg the received MapMessage of the player
* @param from the ID of the Player * @param from the ID of the Player
* @return a boolean based on if the transmitted map ist correct * @return a boolean based on if the transmitted map ist correct
*/ */
private boolean checkMap(MapMessage msg, int from) { private boolean checkMap(MapMessage msg, int from) {
int mapWidth = getPlayerById(from).getMap().getWidth(); int mapWidth = getPlayerById(from).getMap().getWidth();
int mapHeight = getPlayerById(from).getMap().getHeight(); int mapHeight = getPlayerById(from).getMap().getHeight();
if (mapHeight != 10 || mapWidth != 10)
return false;
// check if ship is out of bounds // check if ship is out of bounds
for (Battleship ship : msg.getShips()) { for (Battleship battleship : msg.getShips()){
if (ship.getMaxX() >= mapWidth || ship.getMinX() < 0 || ship.getMaxY() >= mapHeight || ship.getMinY() < 0) { if (battleship.getMaxX() >= mapWidth || battleship.getMinX() < 0 || battleship.getMaxY() >= mapHeight || battleship.getMinY() < 0) {
LOGGER.log(Level.ERROR, "Ship is out of bounds ({0})", ship.toString()); LOGGER.log(Level.ERROR, "Ship is out of bounds ({0})", battleship.toString());
return false; return false;
} }
} }
// check if ships overlap // check if ships overlap
List<Battleship> ships = msg.getShips(); List<Battleship> ships = msg.getShips();
for (Battleship ship : ships) { for(Battleship ship:ships){
for (Battleship compareShip : ships) { for(Battleship compareShip:ships){
if (!(ship == compareShip)) { if(!(ship==compareShip)){
if (ship.collidesWith(compareShip)) { if(ship.collidesWith(compareShip)){
return false; return false;
} }
} }
@@ -224,12 +199,42 @@ 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
for (Player player : players) { for (Player player : players){
send(player, new StartAnimationMessage(msg.getPosition(), player == activePlayer)); send(player, new AnimationStartMessage(msg.getPosition(), player == activePlayer));
setState(ServerState.WAIT_ANIMATION); setState(ServerState.ANIMATION_WAIT);
} }
} }
/**
* Handles a clients message that it is done with the animation
*
* @param msg the AnimationEndMessage to be processed
* @param from the connection ID from which the message was received
*/
@Override
public void received(AnimationEndMessage msg, int from){
if(state != ServerState.ANIMATION_WAIT) {
LOGGER.log(Level.ERROR, "animation not allowed in {0}", state);
return;
}
if(getPlayerById(from) == players.get(0)){
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);
for (Player player : players)
send(player, new SwitchBattleState(player == activePlayer));
player1AnimationReady = false;
player2AnimationReady = false;
}
}
/** /**
* Marks the player as ready and sets their ships. * Marks the player as ready and sets their ships.
* Transitions the state to PLAY if both players are ready. * Transitions the state to PLAY if both players are ready.
@@ -251,85 +256,56 @@ void playerReady(Player player, List<Battleship> ships) {
} }
/** /**
* Handles what Effect should be triggered based on the shot * This method decides what effectMessage the client should get based on the shot made
* @param player the player receiving the message * and switches the active player if a shot was missed
* @param position the position the shot hit *
* @param p the player to be sent the message
* @param position the position where the shot would hit in the 2d map model
*/ */
void shoot(Player player, IntPoint position) { void shoot(Player p, IntPoint position) {
final Battleship selectedShip; final Battleship selectedShip;
selectedShip = getSelectedShip(player, position); if(p != activePlayer){
selectedShip = p.getMap().findShipAt(position);
} else {
selectedShip = getOpponent(p).getMap().findShipAt(position);
}
if (selectedShip == null) { if (selectedShip == null) {
shotMissed(player, position); if (p != activePlayer) {
} send(p, EffectMessage.miss(false, position));
else { } else {
shotHit(player, position, selectedShip); send(activePlayer, EffectMessage.miss(true, position));
}
}
/**
* Returns the ship at a given position
* @param player the player whose map will be checked for a ship
* @param position the position to be checked for a ship
* @return if there is a ship at the given position, returns the ship, else null
*/
Battleship getSelectedShip(Player player, IntPoint position) {
if (player != activePlayer) {
return player.getMap().findShipAt(position);
}
else {
return getOpponent(player).getMap().findShipAt(position);
}
}
/**
* Sends a message to the client that the shot missed
* @param player the player receiving the message
* @param position the position at which the shot hit in the water
*/
void shotMissed(Player player, IntPoint position) {
if (player != activePlayer) {
send(player, EffectMessage.miss(false, position));
}
else
send(activePlayer, EffectMessage.miss(true, position));
if (p1AnimationFinished && p2AnimationFinished)
if (player == activePlayer)
activePlayer = getOpponent(player);
else
activePlayer = player;
}
/**
* Sends a message to the client that the shot missed
* @param player the player receiving the message
* @param position the position at which the shot hit in the ship
* @param ship the ship that has been hit
*/
void shotHit(Player player, IntPoint position, Battleship ship) {
ship.hit(position);
if (getOpponent(activePlayer).getMap().getRemainingShips().isEmpty()) {
if (player != activePlayer)
send(player, EffectMessage.lost(position, ship, activePlayer.getMap().getRemainingShips()));
else
send(activePlayer, EffectMessage.won(position, ship));
if (p1AnimationFinished && p2AnimationFinished) {
setState(ServerState.GAME_OVER);
} }
} if(player1AnimationReady && player2AnimationReady){
else if (ship.isDestroyed()) { LOGGER.log(Level.DEBUG, "switched active player");
if (player != activePlayer) if(p != activePlayer){
send(player, EffectMessage.shipDestroyed(false, position, ship)); activePlayer = p;
else } else {
send(activePlayer, EffectMessage.shipDestroyed(true, position, ship)); activePlayer = getOpponent(p);
} }
else {
if (player != activePlayer) {
send(player, EffectMessage.hit(false, position));
} }
else { } else {
send(activePlayer, EffectMessage.hit(true, position)); 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

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server; package pp.battleship.game.server;
import pp.battleship.message.server.ServerMessage; import pp.battleship.message.server.ServerMessage;

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.server; package pp.battleship.game.server;
@@ -21,13 +26,13 @@ enum ServerState {
*/ */
BATTLE, BATTLE,
/**
* Waits for the Animation to finish
*/
WAIT_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

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.BattleshipConfig; import pp.battleship.BattleshipConfig;

View File

@@ -1,12 +1,13 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.*;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.EndAnimationMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
/** /**
@@ -60,13 +61,15 @@ public void received(MapMessage msg, int from) {
} }
/** /**
* Creates a copy of the provided EndAnimation message * Handles the reception of a AnimationEndMessage
* @param msg thr received EndAnimation message * Creates a copy of the AnimationEndMessage
* @param from the identifier of the sender *
* @param msg the AnimationEndMessage to be processed
* @param from the connection ID from which the message was received
*/ */
@Override @Override
public void received(EndAnimationMessage msg, int from) { public void received(AnimationEndMessage msg, int from) {
copiedMessage = new EndAnimationMessage(msg.getPosition()); copiedMessage = new AnimationEndMessage(msg.getPosition());
} }
/** /**

View File

@@ -1,3 +1,10 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.game.client.BattleshipClient; import pp.battleship.game.client.BattleshipClient;
@@ -14,7 +21,6 @@ class InterpreterProxy implements ServerInterpreter {
private final BattleshipClient playerClient; private final BattleshipClient playerClient;
static final System.Logger LOGGER = System.getLogger(InterpreterProxy.class.getName()); static final System.Logger LOGGER = System.getLogger(InterpreterProxy.class.getName());
/** /**
* Constructs an InterpreterProxy with the specified BattleshipClient. * Constructs an InterpreterProxy with the specified BattleshipClient.
* *
@@ -80,7 +86,7 @@ public void received(EffectMessage msg) {
* @param msg the AnimationStartMessage received from the server * @param msg the AnimationStartMessage received from the server
*/ */
@Override @Override
public void received(StartAnimationMessage msg) { public void received(AnimationStartMessage msg) {
forward(msg); forward(msg);
} }
@@ -90,7 +96,7 @@ public void received(StartAnimationMessage msg) {
* @param msg the SwitchBattleState received from the server * @param msg the SwitchBattleState received from the server
*/ */
@Override @Override
public void received(SwitchToBattleState msg) { public void received(SwitchBattleState msg){
LOGGER.log(System.Logger.Level.INFO, "Received SwitchBattleState"); LOGGER.log(System.Logger.Level.INFO, "Received SwitchBattleState");
forward(msg); forward(msg);
} }

View File

@@ -1,7 +1,7 @@
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.game.client.BattleshipClient; import pp.battleship.game.client.BattleshipClient;
import pp.battleship.message.client.EndAnimationMessage; 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.*; import pp.battleship.message.server.*;
@@ -126,9 +126,9 @@ public void received(EffectMessage msg) {
* @param msg the AnimationStartMessage received * @param msg the AnimationStartMessage received
*/ */
@Override @Override
public void received(StartAnimationMessage msg) { public void received(AnimationStartMessage msg) {
LOGGER.log(Level.INFO, "Received AnimationStartMessage: {0}", msg); LOGGER.log(Level.INFO, "Received AnimationStartMessage: {0}", msg);
connection.sendRobotMessage(new EndAnimationMessage(msg.getPosition())); connection.sendRobotMessage(new AnimationEndMessage(msg.getPosition()));
} }
/** /**
@@ -137,9 +137,9 @@ public void received(StartAnimationMessage msg) {
* @param msg the SwitchBattleState received * @param msg the SwitchBattleState received
*/ */
@Override @Override
public void received(SwitchToBattleState msg) { public void received(SwitchBattleState msg){
LOGGER.log(Level.INFO, "Received SwitchBattleStateMessage: {0}", msg); LOGGER.log(Level.INFO, "Received SwitchBattleStateMessage: {0}", msg);
if (msg.getTurn()) if (msg.isTurn())
shoot(); shoot();
} }
} }

View File

@@ -1,4 +1,9 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;

View File

@@ -3,29 +3,36 @@
import com.jme3.network.serializing.Serializable; import com.jme3.network.serializing.Serializable;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
/**
* A message sent by the client telling the server the animation is finished
*/
@Serializable @Serializable
public class EndAnimationMessage extends ClientMessage { public class AnimationEndMessage extends ClientMessage {
private IntPoint position; private IntPoint position;
/** /**
* Default constructor for serialization purposes. * used for serialization
*/ */
private EndAnimationMessage() {/*do nothing */} private AnimationEndMessage(){ /* nothing */}
/** /**
* Constructs an EndAnimation message * constructs a new AnimationEndMessage
* @param position the position to be effected *
* @param position the position to be effected by the server
*/ */
public EndAnimationMessage(final IntPoint position) { public AnimationEndMessage(IntPoint position) {
this.position = position; this.position = position;
} }
/** /**
* Accepts a visitor for processing this message. * 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 interpreter the visitor to be used for processing
* @param from the connection ID of the sender * @param from the connection ID of the sender
@@ -34,12 +41,4 @@ public EndAnimationMessage(final IntPoint position) {
public void accept(ClientInterpreter interpreter, int from) { public void accept(ClientInterpreter interpreter, int from) {
interpreter.received(this, from); interpreter.received(this, from);
} }
/**
* Getter for the position
* @return IntPoint position
*/
public IntPoint getPosition() {
return position;
}
} }

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