14 Commits

Author SHA1 Message Date
Timo Brennförder
a38366600c fixed a bug where the SHIPDESTROYED Sound was played everytime 2024-10-14 11:12:07 +02:00
Timo Brennförder
8c45784246 Code cleanup
added JavaDocs
removed unnecessary lines
2024-10-13 01:59:46 +02:00
Timo Brennförder
febdd63422 solution for number 13
added an AnimationState in Client and Server
added 2D and 3D representation of a missile
added missile launch sound
updated Singlemode to continue functionality
2024-10-12 18:24:33 +02:00
Timo Brennförder
264b854cbe bug fix
fixed first few fire frames being displayed at the edge of the map
added README.txt files for used images
2024-10-10 23:26:15 +02:00
Timo Brennförder
24f20f855f solution for number 12
added water splash for misfires
added explosion, debris and fire for hit
2024-10-10 21:51:11 +02:00
Timo Brennförder
dac84388fc added different music for different game states 2024-10-05 20:56:26 +02:00
Timo Brennförder
329d3d7372 reworked ex 8
return to EditorState if map is invalid
2024-10-05 12:59:38 +02:00
Timo Brennförder
22bcd55024 added JavaDocs
corrected typos
2024-10-04 18:05:26 +02:00
Timo Brennförder
161fa5cb22 Solution for exercise 11
added possibility to start own Server from client
added a Checkbox to the connection menu to manage starting own server
2024-10-04 17:57:45 +02:00
Timo Brennförder
9cfabdb15d added background music and improved model code
reduced switch case for creating different ship models
created a new class for background music
added a button to toggle background music
added a slider so adjust volume
added new style for labeling the slider
2024-10-03 22:08:26 +02:00
Timo Brennförder
3bcaca8810 activated Singlemode and added different ship models depending on size
1 -> Patrol boat
2 -> BattleshipV2
3 -> Uboat
4 -> King George V
default -> boxes of given length
2024-10-03 16:49:44 +02:00
Timo Brennförder
0f70f4691d added logic for server- and clientside verification of JSON-files 2024-10-02 19:38:42 +02:00
Timo Brennförder
b56f4d35a3 corrected an error in the BattleState class
receivedEffect-function added remaining Opponent Ships to ownMap instead of opponentMap
2024-10-02 12:12:51 +02:00
Timo Brennförder
09f3e9e403 corrected an error in ShipMap.java.
remove-function called a new ItemAddedEvent instead of a ItemRemovedEvent
2024-10-02 11:48:58 +02:00
204 changed files with 734956 additions and 151601 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@ implementation project(":jme-common")
implementation project(":battleship:model") implementation project(":battleship:model")
implementation libs.jme3.desktop implementation libs.jme3.desktop
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

@@ -9,7 +9,7 @@
# #
# Specifies the map used by the opponent in single mode. # Specifies the map used by the opponent in single mode.
# Single mode is activated if this property is set. # Single mode is activated if this property is set.
#map.opponent=maps/map2.json map.opponent=maps/map2.json
# #
# Specifies the map used by the player in single mode. # Specifies the map used by the player in single mode.
# The player must define their own map if this property is not set. # The player must define their own map if this property is not set.
@@ -26,13 +26,10 @@ map.own=maps/map1.json
robot.targets=2, 0,\ robot.targets=2, 0,\
2, 1,\ 2, 1,\
2, 2,\ 2, 2,\
2, 3,\ 2, 3
2, 4,\
2, 5,\
2, 6
# #
# Delay in milliseconds between each shot fired by the RobotClient. # Delay in milliseconds between each shot fired by the RobotClient.
robot.delay=4000 robot.delay=500
# #
# The dimensions of the game map used in single mode. # The dimensions of the game map used in single mode.
# 'map.width' defines the number of columns, and 'map.height' defines the number of rows. # 'map.width' defines the number of columns, and 'map.height' defines the number of rows.

View File

@@ -0,0 +1,238 @@
package pp.battleship.client;
import com.jme3.app.Application;
import com.jme3.audio.AudioData.DataType;
import com.jme3.audio.AudioNode;
import com.jme3.audio.AudioSource.Status;
import pp.battleship.notification.Music;
import pp.battleship.notification.MusicEvent;
import pp.battleship.notification.GameEventListener;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.prefs.Preferences;
/**
* Class to play Background music in game
*/
public class BackgroundMusic implements GameEventListener {
private static final String VOLUME_PREF = "volume";
private static final String MUSIC_ENABLED_PREF = "musicEnabled";
private final Preferences pref = Preferences.userNodeForPackage(BackgroundMusic.class);
static final Logger LOGGER = System.getLogger(BackgroundMusic.class.getName());
private static final String MENU_MUSIC = "Sound/Music/menu/cinematictrailerelite.ogg";
private static final String GAME_MUSIC = "Sound/Music/game/Aluminum.ogg";
private static final String VICTORY_MUSIC = "Sound/Music/victory/victorymarchofvalor.ogg";
private static final String LOSE_MUSIC = "Sound/Music/lose/TouchofDream.ogg";
private final AudioNode menuMusic;
private final AudioNode gameMusic;
private final AudioNode victoryMusic;
private final AudioNode loseMusic;
private String lastPlayedMusic;
private boolean musicEnabled;
private float volume;
private Application app;
/**
* Constructor for BackgroundMusic class
*
* @param app The main Application
*/
public BackgroundMusic(Application app) {
this.volume = pref.getFloat(VOLUME_PREF, 1.0f);
this.musicEnabled = pref.getBoolean(MUSIC_ENABLED_PREF, true);
this.app = app;
menuMusic = createMusicNode(MENU_MUSIC);
gameMusic = createMusicNode(GAME_MUSIC);
victoryMusic = createMusicNode(VICTORY_MUSIC);
loseMusic = createMusicNode(LOSE_MUSIC);
stop(gameMusic);
stop(victoryMusic);
stop(loseMusic);
lastPlayedMusic = menuMusic.getName();
if (musicEnabled) {
play(menuMusic);
}
}
/**
* Creates an audio node for the music
*
* @param musicFilePath the file path to the music
* @return the created audio node
*/
private AudioNode createMusicNode(String musicFilePath) {
AudioNode audioNode = new AudioNode(app.getAssetManager(), musicFilePath, DataType.Stream);
audioNode.setVolume(volume);
audioNode.setPositional(false);
audioNode.setLooping(true);
audioNode.setName(musicFilePath);
return audioNode;
}
/**
* Starts the music
*
* @param audioNode the audio node to be played
*/
public void play(AudioNode audioNode) {
if (musicEnabled && (audioNode.getStatus() == Status.Stopped || audioNode.getStatus() == Status.Paused)) {
audioNode.play();
lastPlayedMusic = audioNode.getName();
}
}
/**
* Pauses the music
*
* @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
*/
public void stop(AudioNode audioNode) {
if (audioNode.getStatus() == Status.Playing) {
audioNode.stop();
}
}
/**
* Controls which music should be played
*/
public void toggleMusic() {
this.musicEnabled = !this.musicEnabled;
if (musicEnabled) {
switch (lastPlayedMusic) {
case MENU_MUSIC:
play(menuMusic);
break;
case GAME_MUSIC:
play(gameMusic);
break;
case VICTORY_MUSIC:
play(victoryMusic);
break;
case LOSE_MUSIC:
play(loseMusic);
break;
}
}
else {
pause(menuMusic);
pause(gameMusic);
pause(victoryMusic);
pause(loseMusic);
}
pref.putBoolean(MUSIC_ENABLED_PREF, 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 && !lastPlayedMusic.equals(MENU_MUSIC)) {
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
*
* @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
*/
public void setVolume(float volume) {
this.volume = volume;
menuMusic.setVolume(volume);
gameMusic.setVolume(volume);
victoryMusic.setVolume(volume);
loseMusic.setVolume(volume);
pref.putFloat(VOLUME_PREF, volume);
}
/**
* Returns the volume
*
* @return the current volume as a float
*/
public float getVolume() {
return volume;
}
/**
* Returns if music should be played or not
*
* @return boolean value in music should be played
*/
public boolean isMusicEnabled() {
return musicEnabled;
}
}

View File

@@ -1,10 +1,3 @@
////////////////////////////////////////
// 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;
@@ -122,7 +115,10 @@ 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);
private EffectHandler effectHandler; /**
* Object handling the background music
*/
private BackgroundMusic backgroundMusic;
static { static {
// Configure logging // Configure logging
@@ -157,7 +153,6 @@ private BattleshipApp() {
logic.addListener(this); logic.addListener(this);
setShowSettings(config.getShowSettings()); setShowSettings(config.getShowSettings());
setSettings(makeSettings()); setSettings(makeSettings());
effectHandler = null;
} }
/** /**
@@ -227,8 +222,9 @@ public void simpleInitApp() {
setupInput(); setupInput();
setupStates(); setupStates();
setupGui(); setupGui();
effectHandler = new EffectHandler(this);
serverConnection.connect(); serverConnection.connect();
backgroundMusic = new BackgroundMusic(this);
logic.addListener(backgroundMusic);
} }
/** /**
@@ -269,20 +265,11 @@ private void setupStates() {
flyCam.setEnabled(false); flyCam.setEnabled(false);
stateManager.detach(stateManager.getState(StatsAppState.class)); stateManager.detach(stateManager.getState(StatsAppState.class));
stateManager.detach(stateManager.getState(DebugKeysAppState.class)); stateManager.detach(stateManager.getState(DebugKeysAppState.class));
attachGameMusic();
attachGameSound(); attachGameSound();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState()); stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
} }
/**
* Attaches the game sound state and sets its initial enabled state.
*/
private void attachGameMusic() {
final GameMusic gameMusic = new GameMusic();
gameMusic.setEnabled(GameMusic.enabledInPreferences());
stateManager.attach(gameMusic);
}
/** /**
* Attaches the game sound state and sets its initial enabled state. * Attaches the game sound state and sets its initial enabled state.
*/ */
@@ -328,6 +315,10 @@ 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.
@@ -439,8 +430,4 @@ void errorDialog(String errorMessage) {
.build() .build()
.open(); .open();
} }
public EffectHandler getEffectHandler() {
return effectHandler;
}
} }

View File

@@ -1,10 +1,3 @@
////////////////////////////////////////
// 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,10 +1,3 @@
////////////////////////////////////////
// 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,197 +0,0 @@
package pp.battleship.client;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import pp.battleship.model.Battleship;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
/**
* EffectHandler manages the creation and manipulation of particle effects in the Battleship application.
*/
public class EffectHandler {
private final BattleshipApp app;
private final Map<Battleship, List<ParticleEmitter>> effects;
/**
* Constructs an EffectHandler with the specified BattleshipApp instance.
*
* @param app the BattleshipApp instance
*/
public EffectHandler(BattleshipApp app) {
this.app = app;
effects = new HashMap<>();
}
/**
* Creates a fire effect at the specified position for the given Battleship.
*
* @param point the position where the fire effect will be created
* @param ship the Battleship associated with the fire effect
* @return a Node containing the fire effect
*/
public Node createFire(Vector3f point, Battleship ship) {
Node parent = new Node();
parent.setLocalTranslation(point);
ParticleEmitter fire = initializeParticleEmitter(
"Effects/Explosion/flame.png",
2,2,
new ColorRGBA(1f, 0f, 0f, 1f),
new ColorRGBA(1f, 1f, 0f, 0.5f),
new Vector3f(0, 1.5f, 0),
50,
.4f,
0.05f,
1f,
2f,
0.2f,
new Vector3f(0, 0, 0)
);
ParticleEmitter smoke = initializeParticleEmitter(
"Effects/Smoke/Smoke.png",
15,
1,
new ColorRGBA(1f, 1f, 1f, 0f),
new ColorRGBA(0.5f, 0.5f, 0.5f, 0.5f),
new Vector3f(0, 1f, 0),
600,
.2f,
0.1f,
1f,
5f,
0.25f,
new Vector3f(0, 0, 0)
);
parent.attachChild(fire);
parent.attachChild(smoke);
List<ParticleEmitter> oldEffects = new ArrayList<>(effects.getOrDefault(ship, new ArrayList<>()));
oldEffects.add(fire);
oldEffects.add(smoke);
effects.put(ship, oldEffects);
return parent;
}
/**
* Creates a water splash effect at the specified position.
*
* @param pos the position where the water splash effect will be created
* @return a Geometry representing the water splash effect
*/
public Geometry waterSplash(Vector3f pos) {
ParticleEmitter water = initializeParticleEmitter(
"Effects/Explosion/flash.png",
2,2,
new ColorRGBA(0.3f, 0.8f, 1f, 0f),
new ColorRGBA(0f, 0f, 1f, 1f),
new Vector3f(0, 3, 0),
100,
.6f,
0.05f,
1f,
1.5f,
0.3f,
new Vector3f(0, 4f, 0)
);
water.setLocalTranslation(pos);
water.emitAllParticles();
water.setParticlesPerSec(0);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
deleteSplash(water);
}
}, 2000);
return water;
}
/**
* Creates a debris splash effect at the specified position.
*
* @param pos the position where the debris splash effect will be created
* @return a Geometry representing the debris splash effect
*/
public Geometry debrisSplash(Vector3f pos) {
ParticleEmitter debris = initializeParticleEmitter(
"Effects/Explosion/Debris.png",
3,3,
new ColorRGBA(0.1f, 0.1f, 0.1f, 0f),
new ColorRGBA(0.5f, 0.5f, 0.5f, .8f),
new Vector3f(0, 2f, 0),
50,
0.1f,
0.5f,
1f,
1.5f,
0.5f,
new Vector3f(0, 0, 0)
);
debris.setLocalTranslation(pos);
debris.emitAllParticles();
debris.setParticlesPerSec(0);
return debris;
}
/**
* Deletes the specified splash effect from the scene.
*
* @param splash the Geometry representing the splash effect to be deleted
*/
private void deleteSplash(Geometry splash) {
splash.getParent().detachChild(splash);
}
/**
* Stops all particle effects associated with the specified Battleship.
*
* @param ship the Battleship whose effects are to be destroyed
*/
public void destroyShip(Battleship ship) {
for (ParticleEmitter emitter : effects.get(ship)) {
emitter.setParticlesPerSec(0);
}
}
private ParticleEmitter initializeParticleEmitter(
String texturePath, int imagesX, int imagesY, ColorRGBA endColor, ColorRGBA startColor, Vector3f initialVelocity,
int particleCount, float startSize, float endSize, float lowLife, float highLife,
float velocityVariation, Vector3f gravity
) {
ParticleEmitter emitter = new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, particleCount);
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
mat.setTexture("Texture", app.getAssetManager().loadTexture(texturePath));
emitter.setMaterial(mat);
emitter.setImagesX(imagesX);
emitter.setImagesY(imagesY);
emitter.setEndColor(endColor);
emitter.setStartColor(startColor);
emitter.getParticleInfluencer().setInitialVelocity(initialVelocity);
emitter.setStartSize(startSize);
emitter.setEndSize(endSize);
emitter.setLowLife(lowLife);
emitter.setHighLife(highLife);
emitter.getParticleInfluencer().setVelocityVariation(velocityVariation);
emitter.setGravity(gravity);
return emitter;
}
}

View File

@@ -1,114 +0,0 @@
package pp.battleship.client;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.audio.AudioNode;
import java.util.prefs.Preferences;
import static pp.JmeUtil.loadSound;
import static pp.util.PreferencesUtils.getPreferences;
/**
* An application state that plays sounds.
*/
public class GameMusic extends AbstractAppState {
private static final Preferences PREFERENCES = getPreferences(GameMusic.class);
private static final String ENABLED_PREF = "enabled";
private static final String VOLUME_PREF = "volume";
private static final String MUSIC_PATH = "Sound/background.wav";
private AudioNode backgroundMusic;
private float volume;
/**
* Returns whether the music is enabled in the user preferences.
*
* @return true if music is enabled, false otherwise
*/
public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true);
}
/**
* Returns the music volume level stored in the user preferences.
*
* @return the volume level as a float (default is 0.5f)
*/
public static float volumeInPreferences() {
return PREFERENCES.getFloat(VOLUME_PREF, 0.5f);
}
/**
* Initializes the game music system
*
* @param stateManager the state manager of the game
* @param app the main application
*/
@Override
public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app);
backgroundMusic = loadSound(app, MUSIC_PATH);
setMusicVolume(volumeInPreferences());
if (isEnabled()) playMusic();
}
/**
* Sets the enabled state of this AppState.
* Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
*
* @param enabled {@code true} to enable the AppState, {@code false} to disable it.
*/
@Override
public void setEnabled(boolean enabled) {
if (enabled && !isEnabled()) {
playMusic();
}
else if (!enabled && isEnabled()) {
stopMusic();
}
super.setEnabled(enabled);
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
}
/**
* Plays the background music.
*/
public void playMusic() {
if (backgroundMusic != null) {
backgroundMusic.play();
}
}
/**
* Stops background music.
*/
public void stopMusic() {
if (backgroundMusic != null) {
backgroundMusic.stop();
}
}
/**
* Sets the volume of the background music and saves the volume setting in user preferences.
*
* @param volume the volume level to set (0.0f to 1.0f)
*/
public void setMusicVolume(float volume) {
if (backgroundMusic != null) {
backgroundMusic.setVolume(volume);
this.volume = volume;
PREFERENCES.putFloat(VOLUME_PREF, volume);
}
}
/**
* Returns volume stored in class
* @return volume
*/
public float getVolume() {
return this.volume;
}
}

View File

@@ -1,38 +1,34 @@
////////////////////////////////////////
// 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;
import com.jme3.app.state.AbstractAppState; import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager; import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode; import com.jme3.audio.AudioNode;
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;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import static pp.JmeUtil.loadSound;
import static pp.util.PreferencesUtils.getPreferences; import static pp.util.PreferencesUtils.getPreferences;
/** /**
* An application state that plays sounds. * An application state that plays sounds.
*/ */
public class GameSound extends AbstractAppState implements GameEventListener { public class GameSound extends AbstractAppState implements GameEventListener {
private static final Logger LOGGER = System.getLogger(GameSound.class.getName()); static final Logger LOGGER = System.getLogger(GameSound.class.getName());
private static final Preferences PREFERENCES = getPreferences(GameSound.class); private static final Preferences PREFERENCES = getPreferences(GameSound.class);
private static final String ENABLED_PREF = "enabled"; //NON-NLS private static final String ENABLED_PREF = "enabled"; //NON-NLS
private AudioNode splashSound; private AudioNode splashSound;
private AudioNode shipDestroyedSound; private AudioNode shipDestroyedSound;
private AudioNode explosionSound; private AudioNode explosionSound;
private AudioNode shellFlyingSound; private AudioNode missileLaunch;
/** /**
* Checks if sound is enabled in the preferences. * Checks if sound is enabled in the preferences.
@@ -77,16 +73,35 @@ public void initialize(AppStateManager stateManager, Application app) {
shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS
splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS
explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS
shellFlyingSound = loadSound(app, "Sound/Effects/shell_flying.wav"); missileLaunch = loadSound(app, "Sound/Effects/missilefiring.wav"); //NON-NLS
} }
/** /**
* Plays the shell flying sound effect. * Loads a sound from the specified file.
*
* @param app The application
* @param name The name of the sound file.
* @return The loaded AudioNode.
*/ */
public void shellFly() { private AudioNode loadSound(Application app, String name) {
if (isEnabled() && shellFlyingSound != null) { try {
shellFlyingSound.playInstance(); final AudioNode sound = new AudioNode(app.getAssetManager(), name, AudioData.DataType.Buffer);
sound.setLooping(false);
sound.setPositional(false);
return sound;
} }
catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
}
return null;
}
/**
* Plays the splash sound effect.
*/
public void missileLaunch() {
if (isEnabled() && missileLaunch != null)
missileLaunch.playInstance();
} }
/** /**
@@ -113,13 +128,18 @@ public void shipDestroyed() {
shipDestroyedSound.playInstance(); shipDestroyedSound.playInstance();
} }
/**
* Plays sound according to the received SoundEvent
*
* @param event the received SoundEvent
*/
@Override @Override
public void receivedEvent(SoundEvent event) { public void receivedEvent(SoundEvent event) {
switch (event.sound()) { switch (event.sound()) {
case EXPLOSION -> explosion(); case EXPLOSION -> explosion();
case SPLASH -> splash(); case SPLASH -> splash();
case DESTROYED_SHIP -> shipDestroyed(); case DESTROYED_SHIP -> shipDestroyed();
case SHELL_FLYING -> shellFly(); case MISSILE_LAUNCH -> missileLaunch();
} }
} }
} }

View File

@@ -1,15 +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; package pp.battleship.client;
import com.simsilica.lemur.Button; import com.simsilica.lemur.Button;
import com.simsilica.lemur.Checkbox; import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.DefaultRangedValueModel;
import com.simsilica.lemur.Label; import com.simsilica.lemur.Label;
import com.simsilica.lemur.Slider;
import com.simsilica.lemur.core.VersionedReference;
import com.simsilica.lemur.style.ElementId; import com.simsilica.lemur.style.ElementId;
import pp.dialog.Dialog; import pp.dialog.Dialog;
import pp.dialog.StateCheckboxModel; import pp.dialog.StateCheckboxModel;
@@ -33,7 +29,7 @@ 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 final VolumeSlider volumeSlider; private final VersionedReference<Double> volumeRef;
/** /**
* Constructs the Menu dialog for the Battleship application. * Constructs the Menu dialog for the Battleship application.
@@ -43,14 +39,22 @@ class Menu extends Dialog {
public Menu(BattleshipApp app) { public Menu(BattleshipApp app) {
super(app.getDialogManager()); super(app.getDialogManager());
this.app = app; this.app = app;
volumeSlider = new VolumeSlider(app.getStateManager().getState(GameMusic.class));
addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS
addChild(new Checkbox(lookup("menu.sound-enabled"), addChild(new Checkbox(lookup("menu.sound-enabled"),
new StateCheckboxModel(app, GameSound.class))); new StateCheckboxModel(app, GameSound.class)));
addChild(new Checkbox(lookup("menu.music-enabled"), new StateCheckboxModel(app, GameMusic.class))); Checkbox musicToggle = new Checkbox(lookup("menu.music.toggle"));
musicToggle.setChecked(app.getBackgroundMusic().isMusicEnabled());
musicToggle.addClickCommands(s -> toggleMusic());
addChild(musicToggle);
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();
addChild(loadButton) addChild(loadButton)
.addClickCommands(s -> ifTopDialog(this::loadDialog)); .addClickCommands(s -> ifTopDialog(this::loadDialog));
addChild(saveButton) addChild(saveButton)
@@ -59,9 +63,39 @@ 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
*
* @param tpf time per frame
*/
@Override
public void update(float tpf) {
if (volumeRef.update()) {
double newVolume = volumeRef.get();
adjustVolume(newVolume);
}
}
/**
* Adjusts the volume for the background music
*
* @param volume is the double value of the volume
*/
private void adjustVolume(double volume) {
app.getBackgroundMusic().setVolume((float) volume);
}
/**
* Toggles the background music on and off
*/
private void toggleMusic() {
app.getBackgroundMusic().toggleMusic();
}
/** /**
* Updates the state of the load and save buttons based on the game logic. * Updates the state of the load and save buttons based on the game logic.
*/ */
@@ -71,11 +105,6 @@ public void update() {
saveButton.setEnabled(app.getGameLogic().maySaveMap()); saveButton.setEnabled(app.getGameLogic().maySaveMap());
} }
@Override
public void update(float delta) {
volumeSlider.update();
}
/** /**
* As an escape action, this method closes the menu if it is the top dialog. * As an escape action, this method closes the menu if it is the top dialog.
*/ */

View File

@@ -1,10 +1,3 @@
////////////////////////////////////////
// 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;
@@ -12,10 +5,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.clienthost.BattleshipServerClient;
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;
@@ -38,10 +31,8 @@ class NetworkDialog extends SimpleDialog {
private String hostname; private String hostname;
private int portNumber; private int portNumber;
private Future<Object> connectionFuture; private Future<Object> connectionFuture;
private Future<Object> serverFuture;
private Dialog progressDialog; private Dialog progressDialog;
private BattleshipServerClient server; private boolean hostServer = false;
private final Checkbox clientHostCheckbox;
/** /**
* Constructs a new NetworkDialog. * Constructs a new NetworkDialog.
@@ -55,14 +46,18 @@ class NetworkDialog extends SimpleDialog {
host.setPreferredWidth(400f); host.setPreferredWidth(400f);
port.setSingleLine(true); port.setSingleLine(true);
Checkbox hostServer = new Checkbox(lookup("start.own.server"));
hostServer.setChecked(false);
hostServer.addClickCommands(s -> toggleOwnServer());
final BattleshipApp app = network.getApp(); final BattleshipApp app = network.getApp();
final Container input = new Container(new SpringGridLayout()); final Container input = new Container(new SpringGridLayout());
input.addChild(new Label(lookup("host.name") + ": ")); input.addChild(new Label(lookup("host.name") + ": "));
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);
clientHostCheckbox = new Checkbox("Host Server"); input.addChild(hostServer);
input.addChild(clientHostCheckbox);
DialogBuilder.simple(app.getDialogManager()) DialogBuilder.simple(app.getDialogManager())
.setTitle(lookup("server.dialog")) .setTitle(lookup("server.dialog"))
.setExtension(d -> d.addChild(input)) .setExtension(d -> d.addChild(input))
@@ -74,31 +69,20 @@ class NetworkDialog extends SimpleDialog {
} }
/** /**
* Handles the action for the connect button in the connection dialog. * Handles the action for establishing the connection to a server.
* Tries to parse the port number and initiate connection to the server. * Tries to parse the port number and initiate connection to the server.
*/ */
private void connect() { private void connectToServer() {
LOGGER.log(Level.INFO, "connect to host={0}, port={1}", host, port); //NON-NLS LOGGER.log(Level.INFO, "connect to host={0}, port={1}", host, port); //NON-NLS
try { try {
hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText(); hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText();
portNumber = Integer.parseInt(port.getText()); portNumber = Integer.parseInt(port.getText());
openProgressDialog(); openProgressDialog();
if (clientHostCheckbox.isChecked()) {
serverFuture = network.getApp().getExecutor().submit(this::initServer);
while (server == null || !server.isReady()) {
Thread.sleep(100);
}
}
connectionFuture = network.getApp().getExecutor().submit(this::initNetwork); connectionFuture = network.getApp().getExecutor().submit(this::initNetwork);
} }
catch (NumberFormatException e) { catch (NumberFormatException e) {
network.getApp().errorDialog(lookup("port.must.be.integer")); network.getApp().errorDialog(lookup("port.must.be.integer"));
} }
catch (InterruptedException e) {
throw new RuntimeException(e);
}
} }
/** /**
@@ -126,23 +110,6 @@ private Object initNetwork() {
} }
} }
/**
* Tries to initialize the server hosted by the client.
*
* @throws RuntimeException If an error occurs when starting the server.
*/
private Object initServer() {
try {
server = new BattleshipServerClient();
server.run(Integer.parseInt(port.getText()));
return null;
}
catch (Exception e) {
LOGGER.log(Level.ERROR, "Error while starting server", e);
throw new RuntimeException(e);
}
}
/** /**
* This method is called by {@linkplain pp.dialog.DialogManager#update(float)} for periodically * This method is called by {@linkplain pp.dialog.DialogManager#update(float)} for periodically
* updating this dialog. T * updating this dialog. T
@@ -161,19 +128,6 @@ public void update(float delta) {
LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
if (serverFuture != null && serverFuture.isDone()) {
try {
serverFuture.get();
}
catch (ExecutionException e) {
LOGGER.log(Level.ERROR, "Failed to start server", e.getCause());
}
catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Server thread was interrupted", e);
Thread.currentThread().interrupt();
}
}
} }
/** /**
@@ -197,4 +151,46 @@ 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,10 +1,3 @@
////////////////////////////////////////
// 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,38 +0,0 @@
package pp.battleship.client;
import com.simsilica.lemur.Slider;
/**
* Represents a volume slider for controlling the background music volume in the Battleship game.
* This class extends the {@link Slider} class and interfaces with the {@link GameMusic} instance
* to adjust the volume settings based on user input.
*/
public class VolumeSlider extends Slider {
private final GameMusic gameMusic;
private float volume;
/**
* Constructs a new VolumeSlider instance and initializes it with the current volume level
* from the game music preferences.
*
* @param gameMusic the instance of {@link GameMusic} to control music volume
*/
public VolumeSlider(GameMusic gameMusic) {
super();
this.gameMusic = gameMusic;
volume = gameMusic.getVolume();
getModel().setPercent(volume);
}
/**
* Updates the volume setting based on the current slider position.
* If the slider's percent value has changed, it updates the music volume
* in the associated {@link GameMusic} instance.
*/
public void update() {
if (getModel().getPercent() != volume) {
this.volume = (float) getModel().getPercent();
gameMusic.setMusicVolume(volume);
}
}
}

View File

@@ -1,17 +0,0 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.clienthost;
import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.ClientMessage;
record ReceivedMessageClient(ClientMessage message, int from) {
void process(ClientInterpreter interpreter) {
message.accept(interpreter, from);
}
}

View File

@@ -1,9 +1,4 @@
////////////////////////////////////////
// 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,9 +1,4 @@
////////////////////////////////////////
// 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,181 @@
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,9 +1,4 @@
////////////////////////////////////////
// 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;
@@ -28,7 +23,7 @@
* and interaction between the model and the view. * and interaction between the model and the view.
*/ */
class MapView { class MapView {
public static final float FIELD_SIZE = 40f; private static final float FIELD_SIZE = 40f;
private static final float GRID_LINE_WIDTH = 2f; private static final float GRID_LINE_WIDTH = 2f;
private static final float BACKGROUND_DEPTH = -4f; private static final float BACKGROUND_DEPTH = -4f;
private static final float GRID_DEPTH = -1f; private static final float GRID_DEPTH = -1f;

View File

@@ -1,26 +1,19 @@
////////////////////////////////////////
// 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.RenderState;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Sphere;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import pp.battleship.model.Shot; import pp.battleship.model.Shot;
import pp.util.Position; import pp.util.Position;
import static com.jme3.material.Materials.UNSHADED; 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.
@@ -32,8 +25,10 @@ class MapViewSynchronizer extends ShipMapSynchronizer {
private static final float SHIP_LINE_WIDTH = 6f; private static final float SHIP_LINE_WIDTH = 6f;
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 SHELL_DEPTH = 1f;
private static final float INDENT = 4f; private static final float INDENT = 4f;
private static final float MISSILE_DEPTH = 6f;
private static final float MISSILE_SIZE = 0.8f;
private static final float MISSILE_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;
@@ -45,6 +40,8 @@ class MapViewSynchronizer extends ShipMapSynchronizer {
// The MapView associated with this synchronizer // The MapView associated with this synchronizer
private final MapView view; private final MapView view;
static final Logger LOGGER = System.getLogger(MapViewSynchronizer.class.getName());
/** /**
* Constructs a new MapViewSynchronizer for the given MapView. * Constructs a new MapViewSynchronizer for the given MapView.
* Initializes the synchronizer and adds existing elements from the model to the view. * Initializes the synchronizer and adds existing elements from the model to the view.
@@ -66,16 +63,14 @@ public MapViewSynchronizer(MapView view) {
*/ */
@Override @Override
public Spatial visit(Shot shot) { public Spatial visit(Shot shot) {
LOGGER.log(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(), return view.getApp().getDraw().makeRectangle(p1.getX(), p1.getY(), SHOT_DEPTH, p2.getX() - p1.getX(), p2.getY() - p1.getY(), color);
SHOT_DEPTH,
p2.getX() - p1.getX(), p2.getY() - p1.getY(),
color);
} }
/** /**
@@ -118,23 +113,29 @@ public Spatial visit(Battleship ship) {
} }
/** /**
* Creates and returns a Spatial representation of the given {@code Shell} object * Creates a visual representation on the map
* for 2D visualization in the game. The shell is represented as a circle.
* *
* @param shell The {@code Shell} object to be visualized. * @param shell the Shell element to visit
* @return A {@code Spatial} object representing the shell on the map. * @return the node the visual representation gets attached to.
*/ */
@Override @Override
public Spatial visit(Shell shell) { public Spatial visit(Shell shell) {
final ColorRGBA color = ColorRGBA.Black; LOGGER.log(Logger.Level.DEBUG, "Visiting {0}", shell);
Geometry ellipse = new Geometry("ellipse", new Sphere(50, 50, MapView.FIELD_SIZE / 2 * 0.8f)); final Node missileNode = new Node("missile");
Material mat = new Material(view.getApp().getAssetManager(), UNSHADED); //NON-NLS final Position p1 = view.modelToView(shell.getX(), shell.getY());
mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); final Position p2 = view.modelToView(shell.getX() + MISSILE_SIZE, shell.getY() + MISSILE_SIZE);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mat.setColor("Color", color); final float x1 = p1.getX() + INDENT;
ellipse.setMaterial(mat); final float y1 = p1.getY() + INDENT;
ellipse.addControl(new Shell2DControl(view, shell)); final float x2 = p2.getX() - INDENT;
return ellipse; final float y2 = p2.getY() - INDENT;
final Position startPosition = view.modelToView(MISSILE_CENTERED_IN_MAP_GRID, MISSILE_CENTERED_IN_MAP_GRID);
missileNode.attachChild(view.getApp().getDraw().makeRectangle(startPosition.getX(), startPosition.getY(), MISSILE_DEPTH, p2.getX() - p1.getX(), p2.getY() - p1.getY(), ColorRGBA.DarkGray));
missileNode.setLocalTranslation(startPosition.getX(), startPosition.getY(), MISSILE_DEPTH);
missileNode.addControl(new ShellMapControl(p1, view.getApp(), new IntPoint(shell.getX(), shell.getY())));
return missileNode;
} }
/** /**
@@ -148,6 +149,7 @@ public Spatial visit(Shell shell) {
* @return a Geometry representing the line * @return a Geometry representing the line
*/ */
private Geometry shipLine(float x1, float y1, float x2, float y2, ColorRGBA color) { private Geometry shipLine(float x1, float y1, float x2, float y2, ColorRGBA color) {
LOGGER.log(Logger.Level.DEBUG, "created Ship line");
return view.getApp().getDraw().makeFatLine(x1, y1, x2, y2, SHIP_DEPTH, color, SHIP_LINE_WIDTH); return view.getApp().getDraw().makeFatLine(x1, y1, x2, y2, SHIP_DEPTH, color, SHIP_LINE_WIDTH);
} }
} }

View File

@@ -1,9 +1,4 @@
////////////////////////////////////////
// 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,15 +1,12 @@
////////////////////////////////////////
// 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.BlendMode; import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
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.Geometry;
import com.jme3.scene.Node; import com.jme3.scene.Node;
@@ -18,16 +15,12 @@
import com.jme3.scene.shape.Cylinder; 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.Battleship;
import pp.battleship.model.Rotation;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import pp.battleship.model.Rotation;
import pp.battleship.model.ShipMap; import pp.battleship.model.ShipMap;
import pp.battleship.model.Shot; import pp.battleship.model.Shot;
import java.util.Timer;
import java.util.TimerTask;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static pp.JmeUtil.mapToWorldCord;
import static pp.util.FloatMath.HALF_PI; import static pp.util.FloatMath.HALF_PI;
import static pp.util.FloatMath.PI; import static pp.util.FloatMath.PI;
@@ -39,13 +32,21 @@
*/ */
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 LIGHTING = "Common/MatDefs/Light/Lighting.j3md"; 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 PATROL_BOAT_MODEL = "Models/PATROL_BOAT/12219_boat_v2_L2.obj";
private static final String MODERN_BATTLESHIP_MODEL = "Models/BATTLESHIP/10619_Battleship.obj";
private static final String MODERN_BATTLESHIP_TEXTURES = "Models/BATTLESHIP/BattleshipC.jpg";
private static final String MISSILE_MODEL = "Models/Missile/AIM120D.obj";
private static final String MISSILE_TEXTURE = "Models/Missile/texture.png";
private static final String COLOR = "Color"; //NON-NLS private static final String COLOR = "Color"; //NON-NLS
private static final String SHIP = "ship"; //NON-NLS private static final String SHIP = "ship"; //NON-NLS
private static final String SHOT = "shot"; //NON-NLS private static final String SHOT = "shot"; //NON-NLS
private static final String MISSILE = "missile"; //NON-NLS
private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray; 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 SPLASH_COLOR = new ColorRGBA(0f, 0f, 1f, 0.4f);
private static final ColorRGBA HIT_COLOR = new ColorRGBA(1f, 0f, 0f, 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;
@@ -61,6 +62,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);
addExisting(); addExisting();
} }
@@ -74,11 +76,7 @@ public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) {
*/ */
@Override @Override
public Spatial visit(Shot shot) { public Spatial visit(Shot shot) {
return shot.isHit() ? handleHit(shot) : handleMiss(shot); return shot.isHit() ? handleHit(shot) : hitEffectHandler.missEffect(shot);
}
private Spatial handleMiss(Shot shot) {
return app.getEffectHandler().waterSplash(mapToWorldCord(shot.getX(), shot.getY()));
} }
/** /**
@@ -92,24 +90,12 @@ private Spatial handleMiss(Shot shot) {
private Spatial handleHit(Shot shot) { 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");
shipNode.getControl(ShipControl.class).hit(shot);
if (ship.isDestroyed()) {
shipNode.getControl(ShipControl.class).destroyed();
new Timer().schedule(new TimerTask() { hitEffectHandler.hitEffect(shipNode, shot);
@Override
public void run() {
handleShipDestroy(shipNode);
}
}, 10000);
}
return null; return null;
} }
private void handleShipDestroy(Node shipNode) {
shipNode.getParent().detachChild(shipNode);
}
/** /**
* Creates a cylinder geometry representing the specified shot. * Creates a cylinder geometry representing the specified shot.
* The appearance of the cylinder depends on whether the shot is a hit or a miss. * The appearance of the cylinder depends on whether the shot is a hit or a miss.
@@ -148,29 +134,26 @@ public Spatial visit(Battleship ship) {
final float x = 0.5f * (ship.getMinY() + ship.getMaxY() + 1f); final float x = 0.5f * (ship.getMinY() + ship.getMaxY() + 1f);
final float z = 0.5f * (ship.getMinX() + ship.getMaxX() + 1f); final float z = 0.5f * (ship.getMinX() + ship.getMaxX() + 1f);
node.setLocalTranslation(x, 0f, z); node.setLocalTranslation(x, 0f, z);
node.addControl(new ShipControl(ship, node, app.getEffectHandler())); node.addControl(new ShipControl(ship));
return node; return node;
} }
/** /**
* Creates and returns a 3D model representation of the given {@code Shell} object * Visits a Shell and creates a graphical representation of it.
* for visualization in the game.
* *
* @param shell The {@code Shell} object to be visualized. * @param shell the Shell to be represented
* @return A {@code Spatial} object representing the 3D model of the shell. * @return the node containing the graphical representation of the Shell
*/ */
@Override @Override
public Spatial visit(Shell shell) { public Spatial visit(Shell shell) {
final Spatial model = app.getAssetManager().loadModel("Models/Shell/shell.j3o"); final Node node = new Node(MISSILE);
model.setLocalScale(.05f); node.attachChild(createMissile());
model.setShadowMode(ShadowMode.CastAndReceive);
Material mat = new Material(app.getAssetManager(), LIGHTING);
mat.setTexture("DiffuseMap", app.getAssetManager().loadTexture("Models/Shell/shell_color.png"));
mat.setReceivesShadows(true);
model.setMaterial(mat);
model.addControl(new ShellControl(shell)); final float x = shell.getY();
return model; final float z = shell.getX();
node.setLocalTranslation(x + 0.5f, 10f, z + 0.5f);
node.addControl(new ShellControl(shell, app));
return node;
} }
/** /**
@@ -182,10 +165,10 @@ public Spatial visit(Shell shell) {
*/ */
private Spatial createShip(Battleship ship) { private Spatial createShip(Battleship ship) {
return switch (ship.getLength()) { return switch (ship.getLength()) {
case 1 -> createBattleship(ship, ShipModel.SHIP1); case 1 -> createPatrolBoat(ship);
case 2 -> createBattleship(ship, ShipModel.SHIP2); case 2 -> createModernBattleship(ship);
case 3 -> createBattleship(ship, ShipModel.SHIP3); case 3 -> createUboat(ship);
case 4 -> createBattleship(ship, ShipModel.SHIP4); case 4 -> createBattleship(ship);
default -> createBox(ship); default -> createBox(ship);
}; };
} }
@@ -232,23 +215,93 @@ private Material createColoredMaterial(ColorRGBA color) {
* @param ship the battleship to be represented * @param ship the battleship to be represented
* @return the spatial representing the "King George V" battleship * @return the spatial representing the "King George V" battleship
*/ */
private Spatial createBattleship(Battleship ship, ShipModel shipModel) { private Spatial createBattleship(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(shipModel.getPath()); final Spatial model = app.getAssetManager().loadModel(KING_GEORGE_V_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f); model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(shipModel.getScale()); model.scale(1.48f);
model.setLocalTranslation(shipModel.getTranslation());
model.setShadowMode(ShadowMode.CastAndReceive); model.setShadowMode(ShadowMode.CastAndReceive);
Material mat = new Material(app.getAssetManager(), LIGHTING); return model;
}
String colorPath = shipModel.getColorPath(); /**
String bumpPath = shipModel.getBumpPath(); * Creates a detailed 3D model to represent a modern battleship.
if (colorPath != null) mat.setTexture("DiffuseMap", app.getAssetManager().loadTexture(colorPath)); *
if (bumpPath != null) mat.setTexture("NormalMap", app.getAssetManager().loadTexture(bumpPath)); * @param ship the battleship to be represented
* @return the spatial representing the battleship
*/
mat.setReceivesShadows(true); 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.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
*/
private Spatial createUboat(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(UBOAT_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.45f);
model.setShadowMode(ShadowMode.CastAndReceive);
model.move(0, -0.25f, 0);
return model;
}
/**
* Creates a detailed 3D model to represent a Patrol boat.
*
* @param ship the battleship to be represented
* @return the spatial representing the Patrol boat
*/
private Spatial createPatrolBoat(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(PATROL_BOAT_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.00045f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model; return model;
} }

View File

@@ -1,49 +0,0 @@
package pp.battleship.client.gui;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl;
import pp.battleship.model.Shell;
import pp.util.Position;
/**
* Controls the 2D representation of a {@code Shell} in the game, updating its position
* based on the shell's current state in the game model. The {@code Shell2DControl} class
* is responsible for translating the shell's 3D position to a 2D view position within
* the game's map view.
*/
public class Shell2DControl extends AbstractControl {
private final Shell shell;
private final MapView view;
/**
* Constructs a new {@code Shell2DControl} to manage the 2D visualization of the given {@code Shell}.
*
* @param view The {@code MapView} used to get information about the map to display.
* @param shell The {@code Shell} being visualized.
*/
public Shell2DControl(MapView view, Shell shell){
this.shell = shell;
this.view = view;
}
/**
* Updates the position of the shell's 2D representation based on the shell's current
* 3D position in the game model. The position is mapped from model space to view space
* coordinates and translated to the appropriate location within the {@code MapView}.
*
* @param tpf Time per frame, representing the time elapsed since the last frame.
*/
@Override
protected void controlUpdate(float tpf) {
Vector3f shellPos = shell.getPosition();
Position viewPos = view.modelToView(shellPos.x, shellPos.z);
spatial.setLocalTranslation(viewPos.getX() + MapView.FIELD_SIZE / 2, viewPos.getY() + MapView.FIELD_SIZE / 2, 0);
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering-specific behavior required for this control
}
}

View File

@@ -1,51 +1,62 @@
package pp.battleship.client.gui; package pp.battleship.client.gui;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager; import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort; import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import pp.battleship.client.BattleshipApp;
import pp.battleship.message.client.EndAnimationMessage;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import static pp.JmeUtil.mapToWorldCord; import java.lang.System.Logger;
import static pp.util.FloatMath.PI; import java.lang.System.Logger.Level;
/** /**
* Controls the 3D representation of a {@code Shell} in the game, updating its position * Class to control the 3D representation of a shell
* and rotation based on the shell's current state in the game model. The {@code ShellControl}
* class ensures that the spatial associated with the shell is positioned and oriented correctly
* within the world.
*/ */
public class ShellControl extends AbstractControl { public class ShellControl extends AbstractControl {
private final Shell shell; private final Shell shell;
private final BattleshipApp app;
private static final float TRAVEL_SPEED = 8.5f;
static final Logger LOGGER = System.getLogger(BattleshipApp.class.getName());
/** /**
* Constructs a new {@code ShellControl} to manage the 3D visualization of the given {@code Shell}. * Constructor for ShellControl class
* *
* @param shell The {@code Shell} being visualized and controlled. * @param shell The Shell to be displayed
* @param app the main application
*/ */
public ShellControl(Shell shell){ public ShellControl(Shell shell, BattleshipApp app) {
super();
this.shell = shell; this.shell = shell;
this.app = app;
} }
/** /**
* Updates the 3D position and rotation of the shell based on its current state. * Method to control movement of the Shell and remove it when target is reached
* Converts map coordinates to world coordinates and applies the shell's orientation.
* *
* @param tpf Time per frame, representing the elapsed time since the last update. * @param tpf time per frame (in seconds)
*/ */
@Override @Override
protected void controlUpdate(float tpf) { public void controlUpdate(float tpf) {
Vector3f pos = shell.getPosition(); //LOGGER.log(Level.DEBUG, "missile at x=" + shell.getX() + ", y=" + shell.getY());
Vector3f fixed = mapToWorldCord(pos.x, pos.z); spatial.move(0, -TRAVEL_SPEED * tpf, 0);
fixed.setY(pos.y); if (spatial.getLocalTranslation().getY() <= 0.2) {
spatial.setLocalTranslation(fixed); spatial.getParent().detachChild(spatial);
spatial.setLocalRotation(shell.getRotation()); app.getGameLogic().send(new EndAnimationMessage(new IntPoint(shell.getX(), shell.getY())));
spatial.rotate(PI/2,0,0); }
} }
/**
* This method is called during the rendering phase, but it does not perform any
* operations in this implementation as the control only influences the spatial's
* transformation, not its rendering process.
*
* @param rm the RenderManager rendering the controlled Spatial (not null)
* @param vp the ViewPort being rendered (not null)
*/
@Override @Override
protected void controlRender(RenderManager rm, ViewPort vp) { protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering-specific behavior required for this control
} }
} }

View File

@@ -0,0 +1,62 @@
package pp.battleship.client.gui;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl;
import pp.battleship.client.BattleshipApp;
import pp.battleship.message.client.EndAnimationMessage;
import pp.battleship.model.IntPoint;
import pp.util.Position;
/**
* Class to control the 2D representation of a shell
*/
public class ShellMapControl extends AbstractControl {
private final Position position;
private final IntPoint pos;
private static final Vector3f VECTOR_3_F = new Vector3f();
private final BattleshipApp app;
/**
* Constructor for ShellMapControl
*
* @param position the target position of the shell
* @param app the main application
* @param pos the position the then to render shot goes to
*/
public ShellMapControl(Position position, BattleshipApp app, IntPoint pos) {
super();
this.position = position;
this.pos = pos;
this.app = app;
VECTOR_3_F.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
*
* @param tpf time per frame (in seconds)
*/
@Override
protected void controlUpdate(float tpf) {
spatial.move(VECTOR_3_F.mult(tpf));
if (spatial.getLocalTranslation().getX() >= position.getX() && spatial.getLocalTranslation().getY() >= position.getY()) {
spatial.getParent().detachChild(spatial);
app.getGameLogic().send(new EndAnimationMessage(pos));
}
}
/**
* This method is called during the rendering phase, but it does not perform any
* operations in this implementation as the control only influences the spatial's
* transformation, not its rendering process.
*
* @param rm the RenderManager rendering the controlled Spatial (not null)
* @param vp the ViewPort being rendered (not null)
*/
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
}
}

View File

@@ -1,9 +1,4 @@
////////////////////////////////////////
// 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,157 +6,115 @@
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager; import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort; import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import pp.battleship.client.EffectHandler;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
import pp.battleship.model.Rotation;
import pp.battleship.model.Shot;
import java.util.Timer; import java.lang.System.Logger;
import java.util.TimerTask; import java.lang.System.Logger.Level;
import static pp.JmeUtil.mapToWorldCord;
import static pp.util.FloatMath.DEG_TO_RAD; import static pp.util.FloatMath.DEG_TO_RAD;
import static pp.util.FloatMath.TWO_PI; import static pp.util.FloatMath.TWO_PI;
import static pp.util.FloatMath.sin; import static pp.util.FloatMath.sin;
/** /**
* Controls the pitch oscillation and sinking behavior of a {@code Battleship} model in the game. * Controls the oscillating pitch motion of a battleship model in the game.
* The ship tilts back and forth to simulate movement on water, and can also be animated to sink * The ship oscillates to simulate a realistic movement on water, based on its orientation and length.
* when destroyed.
*/ */
class ShipControl extends AbstractControl { class ShipControl extends AbstractControl {
private static final float SINK_SPEED = 0.04f; /**
private static final float SINK_ROT_SPEED = 0.1f; * The axis of rotation for the ship's pitch (tilting forward and backward).
*/
// The axis of rotation for the ship's pitch (tilting).
private final Vector3f axis; private final Vector3f axis;
// The duration of one oscillation cycle in seconds. /**
* The duration of one complete oscillation cycle in seconds.
*/
private final float cycle; private final float cycle;
// The amplitude of the pitch oscillation in radians. /**
* The amplitude of the pitch oscillation in radians, determining how much the ship tilts.
*/
private final float amplitude; private final float amplitude;
// Quaternion representing the ship's pitch rotation. /**
* A quaternion representing the ship's current pitch rotation.
*/
private final Quaternion pitch = new Quaternion(); private final Quaternion pitch = new Quaternion();
// The current time within the oscillation cycle. /**
* The current time within the oscillation cycle, used to calculate the ship's pitch angle.
*/
private float time; private float time;
/**
// Flag indicating if the ship is sinking. * Ship to be controlled
private boolean sinking; */
// The battleship being controlled.
private final Battleship battleship; private final Battleship battleship;
// Node representing the ship in the scene graph. private static final Logger LOGGER = System.getLogger(ShipControl.class.getName());
private final Node shipNode;
// Handles visual effects for the ship.
private final EffectHandler effectHandler;
/** /**
* Constructs a new {@code ShipControl} instance to manage the effects for a specified {@code Battleship}. * Constructs a new ShipControl instance for the specified Battleship.
* The ship's orientation determines the axis of rotation, and its length affects the oscillation cycle * The ship's orientation determines the axis of rotation, while its length influences
* and amplitude. * the cycle duration and amplitude of the oscillation.
* *
* @param battleship The {@code Battleship} being controlled. * @param ship the Battleship object to control
* @param shipNode The scene graph node representing the ship.
* @param effectHandler The {@code EffectHandler} for creating visual effects.
*/ */
public ShipControl(Battleship battleship, Node shipNode, EffectHandler effectHandler) { public ShipControl(Battleship ship) {
this.battleship = battleship; this.battleship = ship;
this.shipNode = shipNode; // Determine the axis of rotation based on the ship's orientation
this.effectHandler = effectHandler; axis = switch (ship.getRot()) {
sinking = false;
// Determine the axis of rotation based on the ship's orientation.
axis = switch (battleship.getRot()) {
case LEFT, RIGHT -> Vector3f.UNIT_X; case LEFT, RIGHT -> Vector3f.UNIT_X;
case UP, DOWN -> Vector3f.UNIT_Z; case UP, DOWN -> Vector3f.UNIT_Z;
}; };
// Set the cycle duration and amplitude based on the ship's length. // Set the cycle duration and amplitude based on the ship's length
cycle = battleship.getLength() * 2f; cycle = battleship.getLength() * 2f;
amplitude = 5f * DEG_TO_RAD / battleship.getLength(); amplitude = 5f * DEG_TO_RAD / ship.getLength();
} }
/** /**
* Updates the ship's motion. If the ship is sinking, it animates the sinking process. * Updates the ship's pitch oscillation each frame. The ship's pitch is adjusted
* Otherwise, it oscillates the ship to simulate wave motion. * to create a continuous tilting motion, simulating the effect of waves.
* *
* @param tpf Time per frame, used to update the ship's motion. * @param tpf time per frame (in seconds), used to calculate the new pitch angle
*/ */
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
if (sinking) { // If spatial is null, do nothing
handleSinking(tpf);
}
else {
handlePitch(tpf);
}
}
// Handles the sinking animation.
private void handleSinking(float tpf) {
if (spatial == null) return; if (spatial == null) return;
spatial.setLocalTranslation(spatial.getLocalTranslation().add(new Vector3f(0, -1, 0).mult(tpf * SINK_SPEED))); if (battleship.isDestroyed() && spatial.getLocalTranslation().getY() < -0.6f) {
if (battleship.getRot() == Rotation.UP || battleship.getRot() == Rotation.DOWN) { LOGGER.log(Level.INFO, "Ship removed {0}", spatial.getName());
spatial.rotate(tpf * SINK_ROT_SPEED, 0, 0); spatial.getParent().detachChild(spatial);
}
else if (battleship.isDestroyed()) {
spatial.move(0, -0.2f * tpf, 0);
} }
else { else {
spatial.rotate(0, 0, tpf * SINK_ROT_SPEED); // Update the time within the oscillation cycle
}
}
// Handles the pitch oscillation to simulate wave movement.
private void handlePitch(float tpf) {
if (spatial == null) return;
// Update time in the oscillation cycle.
time = (time + tpf) % cycle; time = (time + tpf) % cycle;
// Calculate the pitch angle. // Calculate the current angle of the oscillation
float angle = amplitude * sin(time * TWO_PI / cycle); final float angle = amplitude * sin(time * TWO_PI / cycle);
// Update pitch rotation. // Update the pitch Quaternion with the new angle
pitch.fromAngleAxis(angle, axis); pitch.fromAngleAxis(angle, axis);
}
// Apply rotation to the spatial. // Apply the pitch rotation to the spatial
spatial.setLocalRotation(pitch); spatial.setLocalRotation(pitch);
} }
/**
* This method is called during the rendering phase, but it does not perform any
* operations in this implementation as the control only influences the spatial's
* transformation, not its rendering process.
*
* @param rm the RenderManager rendering the controlled Spatial (not null)
* @param vp the ViewPort being rendered (not null)
*/
@Override @Override
protected void controlRender(RenderManager rm, ViewPort vp) { protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering-specific behavior required. // No rendering logic is needed for this control
}
/**
* Initiates the ship's sinking animation and schedules its destruction.
*/
public void destroyed() {
sinking = true;
shipNode.attachChild(effectHandler.debrisSplash(shipNode.getLocalTranslation()));
new Timer().schedule(new TimerTask() {
@Override
public void run() {
effectHandler.destroyShip(battleship);
}
}, 4000);
}
/**
* Triggers an effect when the ship is hit by a shot, creating a fire effect at the impact location.
*
* @param shot The shot that hit the ship.
*/
public void hit(Shot shot) {
Vector3f shipNodePos = shipNode.getLocalTranslation();
Vector3f shotWorld = mapToWorldCord(shot.getX(), shot.getY());
Vector3f firePos = shotWorld.subtract(shipNodePos);
shipNode.attachChild(effectHandler.createFire(firePos, battleship));
} }
} }

View File

@@ -1,9 +1,4 @@
////////////////////////////////////////
// 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,82 +0,0 @@
package pp.battleship.client.gui;
import com.jme3.math.Vector3f;
/**
* Enum representing different ship models for the Battleship game.
* Each ship model has a corresponding 3D model path, scale, translation, color texture, and optional bump texture.
*/
public enum ShipModel {
SHIP1("Models/Ships/1/ship1.j3o", 0.15f, new Vector3f(0f, 0f, 0f), "Models/Ships/1/ship1_color.png", null),
SHIP2("Models/Ships/2/ship2.j3o", 0.03f, new Vector3f(0f, 0.2f, 0f), "Models/Ships/2/ship2.jpg", null),
SHIP3("Models/Ships/3/ship3.j3o", 0.47f, new Vector3f(0f, -0.2f, 0f), "Models/Ships/3/ship3_color.jpg", null),
SHIP4("Models/Ships/4/ship4.j3o", 1.48f, new Vector3f(0f, 0f, 0f), "Models/Ships/4/ship4_color.jpg", "Models/Ships/4/ship4_bump.jpg");
private final String modelPath;
private final float modelScale;
private final Vector3f translation;
private final String colorPath;
private final String bumpPath;
/**
* Constructs a new ShipModel with the specified parameters.
*
* @param modelPath the path to the 3D model of the ship
* @param modelScale the scale factor to be applied to the model
* @param translation the translation to be applied to the model
* @param colorPath the path to the color texture of the model
* @param bumpPath the optional path to the bump texture of the model (may be null)
*/
ShipModel(String modelPath, float modelScale, Vector3f translation, String colorPath, String bumpPath) {
this.modelPath = modelPath;
this.modelScale = modelScale;
this.translation = translation;
this.colorPath = colorPath;
this.bumpPath = bumpPath;
}
/**
* Returns the path to the bump texture of the ship model.
*
* @return the bump texture path, or null if no bump texture is defined
*/
public String getBumpPath() {
return bumpPath;
}
/**
* Returns the path to the color texture of the ship model.
*
* @return the color texture path
*/
public String getColorPath() {
return colorPath;
}
/**
* Returns the scale factor of the ship model.
*
* @return the scale factor
*/
public float getScale() {
return modelScale;
}
/**
* Returns the path to the 3D model of the ship.
*
* @return the model path
*/
public String getPath() {
return modelPath;
}
/**
* Returns the translation to be applied to the ship model.
*
* @return the translation vector
*/
public Vector3f getTranslation() {
return translation;
}
}

View File

@@ -1,11 +1,6 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.clienthost;
package server;
import com.jme3.network.ConnectionListener; import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection; import com.jme3.network.HostedConnection;
@@ -18,14 +13,16 @@
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.AnimationFinishedMessage;
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.EffectMessage;
import pp.battleship.message.server.GameDetails; import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage; import pp.battleship.message.server.ServerMessage;
import pp.battleship.message.server.StartAnimationMessage;
import pp.battleship.message.server.StartBattleMessage; 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;
@@ -42,14 +39,16 @@
/** /**
* Server implementing the visitor pattern as MessageReceiver for ClientMessages * Server implementing the visitor pattern as MessageReceiver for ClientMessages
*/ */
public class BattleshipServerClient implements MessageListener<HostedConnection>, ConnectionListener, ServerSender { public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
private static final Logger LOGGER = System.getLogger(BattleshipServerClient.class.getName()); private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName());
private static final File CONFIG_FILE = new File("server.properties"); private static final File CONFIG_FILE = new File("server.properties");
private static int port;
private final BattleshipConfig config = new BattleshipConfig(); private final BattleshipConfig config = new BattleshipConfig();
private Server myServer; private Server myServer;
private final ServerGameLogic logic; private final ServerGameLogic logic;
private final BlockingQueue<ReceivedMessageClient> pendingMessages = new LinkedBlockingQueue<>(); private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();
static { static {
// Configure logging // Configure logging
@@ -64,37 +63,28 @@ public class BattleshipServerClient implements MessageListener<HostedConnection>
} }
/** /**
* Creates the server and reads the configuration from the specified file. * Creates the server.
* Initializes the game logic and sets up logging.
*/ */
public BattleshipServerClient() { public BattleshipServer(int port) {
config.readFromIfExists(CONFIG_FILE); config.readFromIfExists(CONFIG_FILE);
BattleshipServer.port = port;
LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
logic = new ServerGameLogic(this, config); logic = new ServerGameLogic(this, config);
} }
/** /**
* Checks if the server is ready. * Runs a server
*
* @return true if the server is running, false otherwise
*/ */
public boolean isReady() { public void run() {
return myServer != null && myServer.isRunning(); startServer();
}
/**
* Starts the server and continuously processes incoming messages.
*/
public void run(int port) {
startServer(port);
while (true) while (true)
processNextMessage(); processNextMessage();
} }
/** /**
* Starts the server by creating a network server on the specified port. * Starts a server
*/ */
private void startServer(int port) { private void startServer() {
try { try {
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
myServer = Network.createServer(port); myServer = Network.createServer(port);
@@ -110,7 +100,7 @@ private void startServer(int port) {
} }
/** /**
* Processes the next message in the queue. * Processes next received message
*/ */
private void processNextMessage() { private void processNextMessage() {
try { try {
@@ -123,7 +113,7 @@ private void processNextMessage() {
} }
/** /**
* Registers all serializable message classes for network transmission. * Registers all serializable classes
*/ */
private void initializeSerializables() { private void initializeSerializables() {
Serializer.registerClass(GameDetails.class); Serializer.registerClass(GameDetails.class);
@@ -131,35 +121,52 @@ private void initializeSerializables() {
Serializer.registerClass(MapMessage.class); Serializer.registerClass(MapMessage.class);
Serializer.registerClass(ShootMessage.class); Serializer.registerClass(ShootMessage.class);
Serializer.registerClass(EffectMessage.class); Serializer.registerClass(EffectMessage.class);
Serializer.registerClass(AnimationFinishedMessage.class);
Serializer.registerClass(Battleship.class); Serializer.registerClass(Battleship.class);
Serializer.registerClass(IntPoint.class); Serializer.registerClass(IntPoint.class);
Serializer.registerClass(Shot.class); Serializer.registerClass(Shot.class);
Serializer.registerClass(StartAnimationMessage.class);
Serializer.registerClass(EndAnimationMessage.class);
Serializer.registerClass(SwitchToBattleState.class);
} }
/** /**
* Registers the message and connection listeners for the server. * 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, AnimationFinishedMessage.class); myServer.addMessageListener(this, EndAnimationMessage.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
if (message instanceof ClientMessage clientMessage) if (message instanceof ClientMessage clientMessage)
pendingMessages.add(new ReceivedMessageClient(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
@@ -173,9 +180,8 @@ public void connectionRemoved(Server server, HostedConnection hostedConnection)
} }
/** /**
* Shuts down the server and closes all active connections. * Shuts down the server and terminates the application with the given exit code
* * @param exitValue the exit status code
* @param exitValue the exit code to terminate the program with
*/ */
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

View File

@@ -0,0 +1,17 @@
package server;
import pp.battleship.message.client.ClientInterpreter;
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) {
void process(ClientInterpreter interpreter) {
message.accept(interpreter, from);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

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

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-uk-king-george-v-class-battleship-v1--185381.html
License: Free Personal Use Only

View File

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

File diff suppressed because it is too large Load Diff

View File

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

After

Width:  |  Height:  |  Size: 6.8 MiB

View File

@@ -0,0 +1,104 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 16.12.2011 14:18:52
newmtl white
Ns 53.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.6667 0.6667 0.6667
Kd 0.6667 0.6667 0.6667
Ks 0.1800 0.1800 0.1800
Ke 0.0000 0.0000 0.0000
newmtl boat_elements_black
Ns 55.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.0000 0.0000 0.0000
Kd 0.0000 0.0000 0.0000
Ks 0.3600 0.3600 0.3600
Ke 0.0000 0.0000 0.0000
newmtl boat_glass
Ns 60.0000
Ni 7.0000
d 0.4000
Tr 0.6000
Tf 0.4000 0.4000 0.4000
illum 2
Ka 0.1059 0.1569 0.1451
Kd 0.1059 0.1569 0.1451
Ks 0.6750 0.6750 0.6750
Ke 0.0000 0.0000 0.0000
newmtl boat_screw_hooks_bronze
Ns 80.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.2941 0.2157 0.0510
Kd 0.2941 0.2157 0.0510
Ks 0.7200 0.7200 0.7200
Ke 0.0000 0.0000 0.0000
newmtl boat_silver
Ns 80.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.3333 0.3333 0.3333
Kd 0.3333 0.3333 0.3333
Ks 0.7200 0.7200 0.7200
Ke 0.0000 0.0000 0.0000
newmtl boat_buffer
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.2700 0.2700 0.2700
Ke 0.0000 0.0000 0.0000
map_Ka boat_buffer_diffuse.jpg
map_Kd boat_buffer_diffuse.jpg
newmtl boat_roof_accessory
Ns 15.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.3600 0.3600 0.3600
Ke 0.0000 0.0000 0.0000
map_Ka boat_roof_accessory_diffuse.jpg
map_Kd boat_roof_accessory_diffuse.jpg
newmtl boat_body
Ns 55.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.3600 0.3600 0.3600
Ke 0.0000 0.0000 0.0000
map_Ka boat_body_diffuse.jpg
map_Kd boat_body_diffuse.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -1 +0,0 @@
"Fishing Boat" (https://skfb.ly/6UGtr) by JasperTobias is licensed under Creative Commons Attribution-NonCommercial (http://creativecommons.org/licenses/by-nc/4.0/).

View File

@@ -1,22 +0,0 @@
# Blender 3.6.5 MTL File: 'None'
# www.blender.org
newmtl Boat
Ns 0.000000
Ka 1.000000 1.000000 1.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
map_Kd ship1_color.png
newmtl Boat_2
Ns 0.000000
Ka 1.000000 1.000000 1.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
map_Kd ship1_color.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -1,82 +0,0 @@
# Blender 3.6.5 MTL File: 'None'
# www.blender.org
newmtl Battleship
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
map_Kd ship2.jpg
newmtl blinn2SG
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.356400 0.356400 0.366253
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl blinn3SG
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.346704 0.346704 0.356400
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl blinn4SG
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.351533 0.346704 0.361307
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl blinn5SG
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.346704 0.346704 0.356400
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl blinn6SG
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.349118 0.346704 0.358854
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl blinn7SG
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.346704 0.346704 0.356400
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl blinn8SG
Ns 250.000000
Ka 1.000000 1.000000 1.000000
Kd 0.351533 0.346704 0.361307
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2

View File

@@ -1,13 +0,0 @@
# Blender 3.6.5 MTL File: 'None'
# www.blender.org
newmtl _King_George_V
Ns 60.000008
Ka 1.000000 1.000000 1.000000
Ks 0.450000 0.450000 0.450000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
map_Kd ship4_color.jpg
map_Bump -bm 1.000000 ship4_bump.jpg

View File

@@ -6,10 +6,11 @@ newmtl default
Ni 1.5000 Ni 1.5000
d 1.0000 d 1.0000
Tr 0.0000 Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2 illum 2
Ka 1.0000 1.0000 1.0000 Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000 Kd 1.0000 1.0000 1.0000
Ks 0.5400 0.5400 0.5400 Ks 0.5400 0.5400 0.5400
Ke 0.0000 0.0000 0.0000 Ke 0.0000 0.0000 0.0000
map_Ka ship3_color.jpg map_Ka 14084_WWII_ship_German_Type_II_U-boat_diff.jpg
map_Kd ship3_color.jpg map_Kd 14084_WWII_ship_German_Type_II_U-boat_diff.jpg

View File

@@ -1,7 +1,7 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware # 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 29.03.2012 14:25:39 # File Created: 29.03.2012 14:25:39
mtllib ship3.mtl mtllib 14084_WWII_Ship_German_Type_II_U-boat_v2_L1.mtl
# #
# object 14084_WWII_ship_German_Type_II_U_boat # object 14084_WWII_ship_German_Type_II_U_boat
@@ -148958,6 +148958,7 @@ vt 0.0198 0.7313 0.0000
g 14084_WWII_ship_German_Type_II_U_boat g 14084_WWII_ship_German_Type_II_U_boat
usemtl default usemtl default
s 32
f 1/1/1 2/2/2 3/3/3 4/4/4 f 1/1/1 2/2/2 3/3/3 4/4/4
f 5/5/5 6/6/6 3/3/3 2/2/2 f 5/5/5 6/6/6 3/3/3 2/2/2
f 7/7/7 8/8/8 3/3/3 6/6/6 f 7/7/7 8/8/8 3/3/3 6/6/6

View File

@@ -0,0 +1,2 @@
Missile firing fl by NHMWretched (https://pixabay.com/sound-effects/missile-firing-fl-106655/)
CCO License

View File

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

View File

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

View File

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@@ -1,9 +1,4 @@
////////////////////////////////////////
// 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;
@@ -41,7 +36,7 @@ public static void main(String[] args) {
*/ */
@Override @Override
public void simpleInitApp() { public void simpleInitApp() {
export("shell.obj", "shell.j3o"); //NON-NLS export("Models/KingGeorgeV/King_George_V.obj", "Models/KingGeorgeV/KingGeorgeV.j3o"); //NON-NLS
stop(); stop();
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View File

@@ -0,0 +1,18 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 16.03.2012 14:15:53
newmtl _King_George_V
Ns 60.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.4500 0.4500 0.4500
Ke 0.0000 0.0000 0.0000
map_Ka King_George_V.jpg
map_Kd King_George_V.jpg
map_bump King_George_V_bump.jpg
bump King_George_V_bump.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,3 @@
based on:
https://free3d.com/3d-model/wwii-ship-uk-king-george-v-class-battleship-v1--185381.html
License: Free Personal Use Only

View File

@@ -1,42 +0,0 @@
# Blender 3.6.5 MTL File: 'untitled.blend'
# www.blender.org
newmtl base
Ns 467.358765
Ka 0.636364 0.636364 0.636364
Kd 0.000000 0.000000 0.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 3
newmtl ring
Ns 467.358765
Ka 0.636364 0.636364 0.636364
Kd 0.031430 0.012811 0.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 3
newmtl tip
Ns 467.358765
Ka 0.636364 0.636364 0.636364
Kd 0.032954 0.004269 0.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 3
newmtl top
Ns 467.358765
Ka 0.636364 0.636364 0.636364
Kd 0.000489 0.006614 0.000950
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 3

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,3 @@
////////////////////////////////////////
// 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,10 +1,3 @@
////////////////////////////////////////
// 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

@@ -0,0 +1,114 @@
package pp.battleship.game.client;
import pp.battleship.message.client.EndAnimationMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.SwitchToBattleState;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell;
import pp.battleship.model.ShipMap;
import pp.battleship.notification.Music;
import pp.battleship.notification.Sound;
import java.lang.System.Logger.Level;
/**
* Represents the state in which the animation is played
*/
public class AnimationState extends ClientState {
private boolean myTurn;
/**
* Constructor for the AnimationState class
*
* @param logic the client logic
* @param turn a boolean containing if it's the client's turn
* @param position the position a Shell gets created
*/
public AnimationState(ClientGameLogic logic, boolean turn, IntPoint position) {
super(logic);
logic.playMusic(Music.GAME_THEME);
myTurn = turn;
if (myTurn) {
logic.getOpponentMap().add(new Shell(position));
}
else {
logic.getOwnMap().add(new Shell(position));
}
}
/**
* Makes sure the client renders the correct view
*
* @return true
*/
@Override
boolean showBattle() {
return true;
}
/**
* Reports the effect of a shot based on the server message.
*
* @param msg the message containing the effect of the shot
*/
@Override
public void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS
playSound(msg);
myTurn = msg.isMyTurn();
logic.setInfoText(msg.getInfoTextKey());
affectedMap(msg).add(msg.getShot());
if (destroyedOpponentShip(msg))
logic.getOpponentMap().add(msg.getDestroyedShip());
if (msg.isGameOver()) {
msg.getRemainingOpponentShips().forEach(logic.getOpponentMap()::add);
logic.setState(new GameOverState(logic, msg.isGameLost()));
}
}
/**
* Sets the client back to the battle state
*
* @param msg the received SwitchToBattleState message
*/
@Override
public void receivedSwitchToBattleState(SwitchToBattleState msg) {
logic.setState(new BattleState(logic, msg.getTurn()));
}
/**
* Determines which map (own or opponent's) should be affected by the shot based on the message.
*
* @param msg the effect message received from the server
* @return the map (either the opponent's or player's own map) that is affected by the shot
*/
private ShipMap affectedMap(EffectMessage msg) {
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap();
}
/**
* Checks if the opponent's ship was destroyed by the player's shot.
*
* @param msg the effect message received from the server
* @return true if the shot destroyed an opponent's ship, false otherwise
*/
private boolean destroyedOpponentShip(EffectMessage msg) {
return msg.getDestroyedShip() != null && msg.isOwnShot();
}
/**
* Plays a sound based on the outcome of the shot. Different sounds are played for a miss, hit,
* or destruction of a ship.
*
* @param msg the effect message containing the result of the shot
*/
private void playSound(EffectMessage msg) {
if (!msg.getShot().isHit())
logic.playSound(Sound.SPLASH);
else if (msg.getDestroyedShip() == null)
logic.playSound(Sound.EXPLOSION);
else
logic.playSound(Sound.DESTROYED_SHIP);
}
}

View File

@@ -1,17 +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.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.StartAnimationMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell;
import pp.battleship.model.ShipMap; import pp.battleship.model.ShipMap;
import pp.battleship.notification.Music;
import pp.battleship.notification.Sound; import pp.battleship.notification.Sound;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
@@ -30,14 +26,25 @@ 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);
this.myTurn = myTurn; this.myTurn = myTurn;
} }
/**
* Makes sure the client renders the correct view
*
* @return true
*/
@Override @Override
public boolean showBattle() { 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)
@@ -47,29 +54,13 @@ else if (logic.getOpponentMap().isValid(pos))
} }
/** /**
* Reports the effect of a shot based on the server message. * Triggers an animation if StartAnimationMessage is received
* *
* @param msg the message containing the effect of the shot * @param msg the received Startanimation message
*/ */
@Override @Override
public void receivedEffect(EffectMessage msg) { public void receivedStartAnimation(StartAnimationMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS logic.setState(new AnimationState(logic, msg.isMyTurn(), msg.getPosition()));
logic.playSound(Sound.MISSILE_LAUNCH);
myTurn = msg.isMyTurn();
logic.setInfoText(msg.getInfoTextKey());
Shell shell = new Shell(msg.getShot());
affectedMap(msg).add(shell);
logic.playSound(Sound.SHELL_FLYING);
logic.setState(new ShootingState(logic, shell, myTurn, msg));
}
/**
* Determines which map (own or opponent's) should be affected by the shot based on the message.
*
* @param msg the effect message received from the server
* @return the map (either the opponent's or player's own map) that is affected by the shot
*/
private ShipMap affectedMap(EffectMessage msg) {
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap();
} }
} }

View File

@@ -1,9 +1,4 @@
////////////////////////////////////////
// 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,9 +1,4 @@
////////////////////////////////////////
// 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;
@@ -11,7 +6,9 @@
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails; import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter; import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.StartAnimationMessage;
import pp.battleship.message.server.StartBattleMessage; 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;
@@ -20,6 +17,8 @@
import pp.battleship.notification.GameEventBroker; import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.GameEventListener; import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent; import pp.battleship.notification.InfoTextEvent;
import pp.battleship.notification.Music;
import pp.battleship.notification.MusicEvent;
import pp.battleship.notification.Sound; import pp.battleship.notification.Sound;
import pp.battleship.notification.SoundEvent; import pp.battleship.notification.SoundEvent;
@@ -226,6 +225,26 @@ public void received(EffectMessage msg) {
state.receivedEffect(msg); state.receivedEffect(msg);
} }
/**
* Reports that client should play an animation
*
* @param msg
*/
@Override
public void received(StartAnimationMessage msg) {
state.receivedStartAnimation(msg);
}
/**
* Reports that client should switch to the battle state
*
* @param msg
*/
@Override
public void received(SwitchToBattleState msg) {
state.receivedSwitchToBattleState(msg);
}
/** /**
* Initializes the player's own map, opponent's map, and harbor based on the game details. * Initializes the player's own map, opponent's map, and harbor based on the game details.
* *
@@ -304,7 +323,7 @@ public void saveMap(File file) throws IOException {
* *
* @param msg the message to be sent * @param msg the message to be sent
*/ */
void send(ClientMessage msg) { public void send(ClientMessage msg) {
if (clientSender == null) if (clientSender == null)
LOGGER.log(Level.ERROR, "trying to send {0} with sender==null", msg); //NON-NLS LOGGER.log(Level.ERROR, "trying to send {0} with sender==null", msg); //NON-NLS
else else
@@ -352,4 +371,13 @@ 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));
}
} }

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