14 Commits

Author SHA1 Message Date
Daniel Grigencha
5174b84a1b fixed client-hosted server don't thorw an exception
- added serialization to the client-hosted server
2024-10-14 11:15:27 +02:00
Daniel Grigencha
42b995a4e7 added animation of a shell to the game
- in BattleShipServer class added serialization of the AnimationMessage classs
- added to VoidVisitor and Visitor the Shell class
- edited the ServerGameLogic class to implement a new Animation state (see new Server-State-Chart)
- added a new client state AnimationState (see new Client-State-Chart)
2024-10-13 03:19:44 +02:00
Daniel Grigencha
2c4e2fd92d improved code to pass the code analysis 2024-10-09 23:30:28 +02:00
Daniel Grigencha
08c5eeb63d added realistic visual effects to the game
- imported the jme3-effects library
- edited SeaSynchronizer to handle different effects
- added to the ShipControl class in the controlUpdate(float tpf) method the handler that moves a destroyed ship downward
2024-10-09 23:28:02 +02:00
Daniel Grigencha
9d5f3ac396 added the feature that a client can host a server
- added a class BattleshipServer (a client host a local server) and ReceivedMessage
- edited the NetworkDialog, that a client has a checkbox to select to host a server
-
2024-10-09 18:30:49 +02:00
Daniel Grigencha
28ba183b84 fixed BackgroundMusic
before play a new music, the volume prefences should be set
2024-10-09 17:04:08 +02:00
Daniel Grigencha
a44cbf2a72 added background music to the game
- added a class BackgroundMusic: is an AbstractAppState and GameEventListener that handles the backgroundmusic
- attached the BackgroundMusic to the stateManager  in the BattleshipApp
- added to the Menu a CheckBox and Slider to manipulate the volume of the backgroundmusic
- added four different music files (for different states of the game)
- edited the WaitState and BattleState to play different music files when chaing to that state
- added to ClientGameLogic a new method playMusic(Music) to play the right music (depends on the current state)
- added a new method receivedEvent(MusicEvent) to handle the music events
- added a new enum Music, that represents different types of music
- added a new record MusicEvent(Music), that decides which music shall play
2024-10-09 17:03:12 +02:00
Daniel Grigencha
ec80dd40ce added JavaDocs to the FloatMath class 2024-10-09 02:14:15 +02:00
Daniel Grigencha
046707642f fixed bugs in the JSON validation
- the method isWithinBounds(Battleship ship) couldn't check if the ship is the bounds of the map
2024-10-05 13:20:20 +02:00
Daniel Grigencha
a3b5452fb9 added 3d models for the ships
- added different models (see README.txt of the files)
- added methods to the SeaSynchronizer class to represent different ships sizes with different models
2024-10-05 05:41:12 +02:00
Daniel Grigencha
eda4f06a75 added server-side and client-side validation for JSON files
- added the client-side validation in the EditorState class
- added the server-side validation in the WaitState and ServerGameLogic class
- added Getter in the ShipMapDTO
- added the 'map.invalid' in the properties
2024-10-05 05:32:26 +02:00
Daniel Grigencha
0d2781dbe4 fixed failing tests
- ShipMapTest: notify ItemRemovedEvent when calling the remove method.
- ClientGame1Player1 and ClientGame2Player2Test: Ensure opponent's is retrieved in game over state.
2024-10-05 04:41:24 +02:00
Daniel Grigencha
ef16a3f92b Revert main branch 2024-10-02 13:30:55 +02:00
Felix Koppe
3a2f20e45c Fix ShipMap.remove 2024-10-02 08:21:32 +02:00
111 changed files with 773074 additions and 992410 deletions

View File

@@ -1,17 +1,18 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="BattleshipApp (Mac)" type="Application" factoryName="Application" singleton="false"> <configuration default="false" name="BattleshipApp (Mac)" type="Application" factoryName="Application"
<option name="MAIN_CLASS_NAME" value="pp.battleship.client.BattleshipApp" /> singleton="false">
<module name="Projekte.battleship.client.main" /> <option name="MAIN_CLASS_NAME" value="pp.battleship.client.BattleshipApp"/>
<option name="VM_PARAMETERS" value="-XstartOnFirstThread" /> <module name="Projekte.battleship.client.main"/>
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$" /> <option name="VM_PARAMETERS" value="-XstartOnFirstThread"/>
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/>
<extension name="coverage"> <extension name="coverage">
<pattern> <pattern>
<option name="PATTERN" value="pp.battleship.client.server.*" /> <option name="PATTERN" value="pp.battleship.client.*"/>
<option name="ENABLED" value="true" /> <option name="ENABLED" value="true"/>
</pattern> </pattern>
</extension> </extension>
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true"/>
</method> </method>
</configuration> </configuration>
</component> </component>

View File

@@ -1,19 +1,18 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="BattleshipApp" type="Application" factoryName="Application" singleton="false" nameIsGenerated="true"> <configuration default="false" name="BattleshipApp" type="Application" factoryName="Application" singleton="false"
<option name="ALTERNATIVE_JRE_PATH" value="temurin-20" /> nameIsGenerated="true">
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" /> <option name="MAIN_CLASS_NAME" value="pp.battleship.client.BattleshipApp"/>
<option name="MAIN_CLASS_NAME" value="pp.battleship.client.BattleshipApp" /> <module name="Projekte.battleship.client.main"/>
<module name="Gruppe-01" /> <option name="VM_PARAMETERS" value="-Djava.util.logging.config.file=logging.properties"/>
<option name="VM_PARAMETERS" value="-Djava.util.logging.config.file=logging.properties" /> <option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/>
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$" />
<extension name="coverage"> <extension name="coverage">
<pattern> <pattern>
<option name="PATTERN" value="pp.battleship.client.server.*" /> <option name="PATTERN" value="pp.battleship.client.*"/>
<option name="ENABLED" value="true" /> <option name="ENABLED" value="true"/>
</pattern> </pattern>
</extension> </extension>
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true"/>
</method> </method>
</configuration> </configuration>
</component> </component>

View File

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

View File

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

View File

@@ -1,66 +0,0 @@
{
"width": 10,
"height": 10,
"ships": [
{
"length": 4,
"x": 2,
"y": 8,
"rot": "RIGHT"
},
{
"length": 3,
"x": 2,
"y": 5,
"rot": "DOWN"
},
{
"length": 3,
"x": 5,
"y": 6,
"rot": "RIGHT"
},
{
"length": 2,
"x": 4,
"y": 4,
"rot": "RIGHT"
},
{
"length": 2,
"x": 7,
"y": 4,
"rot": "RIGHT"
},
{
"length": 2,
"x": 7,
"y": 4,
"rot": "RIGHT"
},
{
"length": 1,
"x": 6,
"y": 2,
"rot": "RIGHT"
},
{
"length": 1,
"x": 8,
"y": 2,
"rot": "RIGHT"
},
{
"length": 1,
"x": 6,
"y": 0,
"rot": "RIGHT"
},
{
"length": 1,
"x": 8,
"y": 0,
"rot": "RIGHT"
}
]
}

View File

@@ -1,66 +0,0 @@
{
"width": 10,
"height": 10,
"ships": [
{
"length": 1,
"x": 9,
"y": 0,
"rot": "RIGHT"
},
{
"length": 1,
"x": 9,
"y": 1,
"rot": "RIGHT"
},
{
"length": 1,
"x": 8,
"y": 0,
"rot": "RIGHT"
},
{
"length": 1,
"x": 8,
"y": 1,
"rot": "RIGHT"
},
{
"length": 2,
"x": 8,
"y": 2,
"rot": "RIGHT"
},
{
"length": 2,
"x": 6,
"y": 0,
"rot": "RIGHT"
},
{
"length": 2,
"x": 6,
"y": 1,
"rot": "RIGHT"
},
{
"length": 3,
"x": 5,
"y": 2,
"rot": "RIGHT"
},
{
"length": 3,
"x": 7,
"y": 3,
"rot": "RIGHT"
},
{
"length": 4,
"x": 6,
"y": 4,
"rot": "RIGHT"
}
]
}

View File

@@ -1,95 +1,267 @@
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.AppStateManager;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData.DataType; import com.jme3.audio.AudioData.DataType;
import com.jme3.audio.AudioNode; import com.jme3.audio.AudioNode;
import com.jme3.audio.AudioSource.Status; import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.MusicEvent;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
public class BackgroundMusic { import static pp.util.PreferencesUtils.getPreferences;
private static final String VOLUME_PREF = "volume"; /**
private static final String MUSIC_ENABLED_PREF = "musicEnabled"; * The BackgroundMusic class represents the background music in the Battleship game application.
private Preferences prefs = Preferences.userNodeForPackage(BackgroundMusic.class); * It extends the AbstractAppState class and provides functionalities for playing the menu music,
* game music, victory music, and defeat music.
*/
public class BackgroundMusic extends AbstractAppState implements GameEventListener {
/**
* Logger for the BackgroundMusic class.
*/
private static final Logger LOGGER = System.getLogger(BackgroundMusic.class.getName());
private final AudioNode backgroundMusic; /**
private boolean musicEnabled; * Preferences for storing music settings.
*/
private static final Preferences PREFERENCES = getPreferences(BackgroundMusic.class);
/**
* Preference key for enabling/disabling music.
*/
private static final String ENABLED_PREF = "enabled"; //NON-NLS
/**
* Preference key for storing the volume level.
*/
private static final String VOLUME_PREF = "volume"; //NON-NLS
/**
* Path to the menu music file.
*/
private static final String MENU_MUSIC_PATH = "Sound/Music/menu_music.ogg";
/**
* Path to the game music file.
*/
private static final String GAME_MUSIC_PATH = "Sound/Music/pirates.ogg";
/**
* Path to the victory music file.
*/
private static final String VICTORY_MUSIC_PATH = "Sound/Music/win_the_game.ogg";
/**
* Path to the defeat music file.
*/
private static final String DEFEAT_MUSIC_PATH = "Sound/Music/defeat.ogg";
/**
* AudioNode for the menu music.
*/
private AudioNode menuMusic;
/**
* AudioNode for the game music.
*/
private AudioNode gameMusic;
/**
* AudioNode for the victory music.
*/
private AudioNode victoryMusic;
/**
* AudioNode for the defeat music.
*/
private AudioNode defeatMusic;
/**
* The currently playing AudioNode.
*/
private AudioNode currentMusic;
/**
* The volume level for the background music.
*/
private float volume; private float volume;
/** /**
* Constuctor for the BackgroundMusic * Checks if music is enabled in the preferences.
* @param app for synchronising and saving data with the application *
* @param musicFilePath the filepath for the BackgroundMusic * @return {@code true} if music is enabled, {@code false} otherwise.
*/ */
public BackgroundMusic(Application app, String musicFilePath) { public static boolean enabledInPreferences() {
this.volume = prefs.getFloat(VOLUME_PREF, 1.0f); return PREFERENCES.getBoolean(ENABLED_PREF, true);
this.musicEnabled = prefs.getBoolean(MUSIC_ENABLED_PREF, true);
backgroundMusic = new AudioNode(app.getAssetManager(), musicFilePath, DataType.Stream);
backgroundMusic.setLooping(true);
backgroundMusic.setPositional(false);
backgroundMusic.setVolume(1.0f);
if (musicEnabled) {
play();
}
} }
/** /**
* Checks if the condition is met to play BackgroundMusic * 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.
*/ */
public void play() { @Override
if (musicEnabled && (backgroundMusic.getStatus() == Status.Stopped || backgroundMusic.getStatus() == Status.Paused)) { public void setEnabled(boolean enabled) {
backgroundMusic.play(); if (isEnabled() == enabled) return;
} super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Music enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
playCurrentMusic();
} }
/** /**
* function to stop the BackgroundMusic * Initializes the music for the game.
* Overrides {@link AbstractAppState#initialize(AppStateManager, Application)}
*
* @param stateManager The state manager
* @param app The application
*/ */
public void stop() { @Override
if (backgroundMusic.getStatus() == Status.Playing) { public void initialize(AppStateManager stateManager, Application app) {
backgroundMusic.stop(); LOGGER.log(Level.INFO, "Initializing background music"); //NON-NLS
} super.initialize(stateManager, app);
menuMusic = loadMusic(app, MENU_MUSIC_PATH);
gameMusic = loadMusic(app, GAME_MUSIC_PATH);
victoryMusic = loadMusic(app, VICTORY_MUSIC_PATH);
defeatMusic = loadMusic(app, DEFEAT_MUSIC_PATH);
currentMusic = menuMusic;
playCurrentMusic();
} }
/** /**
* function to toggle the Backgroundmusic also sets the volume to the previous session * Loads a music file and initializes an AudioNode with the specified settings.
*
* @param app The application instance.
* @param name The name of the music file to load.
* @return The initialized AudioNode, or {@code null} if the file could not be loaded.
*/ */
public void toogleMusic() { private AudioNode loadMusic(Application app, String name) {
this.musicEnabled = !this.musicEnabled; try {
if (musicEnabled) { this.volume = PREFERENCES.getFloat(VOLUME_PREF, 0.5f);
play(); final AudioNode music = new AudioNode(app.getAssetManager(), name, DataType.Stream);
music.setLooping(true);
music.setVolume(volume);
music.setPositional(false);
music.setDirectional(false);
return music;
} catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
}
return null;
}
/**
* Plays the current music if the music is enabled.
* Stops the current music if the music is disabled.
*/
private void playCurrentMusic() {
if (isEnabled()) {
if (currentMusic != null) {
LOGGER.log(Level.INFO, "Playing current music"); //NON-NLS
currentMusic.play();
}
} else { } else {
stop(); if (currentMusic != null) {
currentMusic.stop();
}
} }
prefs.putFloat(VOLUME_PREF, volume);
} }
/** /**
* Setter for the Volume also safes the volume from the previous session * Plays the game music.
* @param volume variable for the volume of the BackgroundMusic */
private void gameMusic() {
if (isEnabled() && gameMusic != null) {
stopAll();
LOGGER.log(Level.INFO, "Playing game music"); //NON-NLS
PREFERENCES.putFloat(VOLUME_PREF, volume);
gameMusic.play();
}
}
/**
* Plays the victory music.
*/
private void victoryMusic() {
if (isEnabled() && victoryMusic != null) {
stopAll();
LOGGER.log(Level.INFO, "Playing victory music"); //NON-NLS
PREFERENCES.putFloat(VOLUME_PREF, volume);
victoryMusic.play();
}
}
/**
* Plays the defeat music.
*/
private void defeatMusic() {
if (isEnabled() && defeatMusic != null) {
stopAll();
LOGGER.log(Level.INFO, "Playing defeat music"); //NON-NLS
PREFERENCES.putFloat(VOLUME_PREF, volume);
defeatMusic.play();
}
}
/**
* Stops all music.
*/
private void stopAll() {
if (menuMusic != null) menuMusic.stop();
if (gameMusic != null) gameMusic.stop();
if (victoryMusic != null) victoryMusic.stop();
if (defeatMusic != null) defeatMusic.stop();
}
/**
* Handles the received music event and plays the corresponding music.
*
* @param event The music event to handle.
*/
@Override
public void receivedEvent(MusicEvent event) {
switch (event.music()) {
case GAME_MUSIC -> {
gameMusic();
currentMusic = gameMusic;
}
case VICTORY_MUSIC -> {
victoryMusic();
currentMusic = victoryMusic;
}
case DEFEAT_MUSIC -> {
defeatMusic();
currentMusic = defeatMusic;
}
}
}
/**
* Sets the volume for the background music and updates the preferences.
*
* @param volume The volume level to set.
*/ */
public void setVolume(float volume) { public void setVolume(float volume) {
LOGGER.log(Level.INFO, "Setting volume to {0}", volume); //NON-NLS
this.volume = volume; this.volume = volume;
backgroundMusic.setVolume(volume); currentMusic.setVolume(volume);
prefs.putFloat(VOLUME_PREF, volume); PREFERENCES.putFloat(VOLUME_PREF, volume);
} }
/** /**
* Getter for Volume * Returns the volume level for the background music.
* @return a float value of the volume *
* @return The volume level as a float.
*/ */
public float getVolume() { public float getVolume() {
return volume; return volume;
} }
/**
* Getter for musicEnabled
* @return if the music is enabled return true, false otherwise
*/
public boolean isMusicEnabled() {
return musicEnabled;
}
} }

View File

@@ -121,10 +121,6 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
* Listener for handling actions triggered by the Escape key. * Listener for handling actions triggered by the Escape key.
*/ */
private final ActionListener escapeListener = (name, isPressed, tpf) -> escape(isPressed); private final ActionListener escapeListener = (name, isPressed, tpf) -> escape(isPressed);
/**
*
*/
private BackgroundMusic backgroundMusic;
static { static {
// Configure logging // Configure logging
@@ -132,8 +128,7 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
try { try {
manager.readConfiguration(new FileInputStream("logging.properties")); manager.readConfiguration(new FileInputStream("logging.properties"));
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
} } catch (IOException e) {
catch (IOException e) {
LOGGER.log(Level.INFO, e.getMessage()); LOGGER.log(Level.INFO, e.getMessage());
} }
} }
@@ -145,7 +140,6 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
*/ */
public static void main(String[] args) { public static void main(String[] args) {
new BattleshipApp().start(); new BattleshipApp().start();
} }
/** /**
@@ -230,7 +224,6 @@ public void simpleInitApp() {
setupStates(); setupStates();
setupGui(); setupGui();
serverConnection.connect(); serverConnection.connect();
backgroundMusic = new BackgroundMusic(this, "Sound/Effects/BackgroundMusic/boss_battle_#2_metal_opening.wav");
} }
/** /**
@@ -273,6 +266,7 @@ private void setupStates() {
stateManager.detach(stateManager.getState(DebugKeysAppState.class)); stateManager.detach(stateManager.getState(DebugKeysAppState.class));
attachGameSound(); attachGameSound();
attachBackgroundSound();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState()); stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
} }
@@ -286,6 +280,19 @@ private void attachGameSound() {
stateManager.attach(gameSound); stateManager.attach(gameSound);
} }
/**
* Attaches the background music state and sets its initial enabled state.
* The background music state is responsible for managing the background music
* playback in the game. It listens to the game logic for any changes in the
* background music settings.
*/
private void attachBackgroundSound() {
final BackgroundMusic backgroundMusic = new BackgroundMusic();
logic.addListener(backgroundMusic);
backgroundMusic.setEnabled(BackgroundMusic.enabledInPreferences());
stateManager.attach(backgroundMusic);
}
/** /**
* Updates the application state every frame. * Updates the application state every frame.
* This method is called once per frame during the game loop. * This method is called once per frame during the game loop.
@@ -432,12 +439,4 @@ void errorDialog(String errorMessage) {
.build() .build()
.open(); .open();
} }
/**
* Getter for the BackgroundMusic
* @return the BackgroundMusic
*/
public BackgroundMusic getBackgroundMusic(){
return backgroundMusic;
}
} }

View File

@@ -5,14 +5,20 @@
// (c) Mark Minas (mark.minas@unibw.de) // (c) Mark Minas (mark.minas@unibw.de)
//////////////////////////////////////// ////////////////////////////////////////
package pp.battleship.client.server; package pp.battleship.client;
import com.jme3.network.*; import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection;
import com.jme3.network.Message;
import com.jme3.network.MessageListener;
import com.jme3.network.Network;
import com.jme3.network.Server;
import com.jme3.network.serializing.Serializer; import com.jme3.network.serializing.Serializer;
import pp.battleship.BattleshipConfig; import pp.battleship.BattleshipConfig;
import pp.battleship.game.server.Player; import pp.battleship.game.server.Player;
import pp.battleship.game.server.ServerGameLogic; import pp.battleship.game.server.ServerGameLogic;
import pp.battleship.game.server.ServerSender; import pp.battleship.game.server.ServerSender;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.client.ClientMessage; import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
@@ -37,11 +43,39 @@
* Server implementing the visitor pattern as MessageReceiver for ClientMessages * Server implementing the visitor pattern as MessageReceiver for ClientMessages
*/ */
public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender { public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
/**
* Logger for the BattleshipServer class.
*/
private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName()); private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName());
/**
* Configuration file for the server.
*/
private static final File CONFIG_FILE = new File("server.properties"); private static final File CONFIG_FILE = new File("server.properties");
/**
* Port number for the server.
*/
private final int PORT_NUMBER;
/**
* Configuration settings for the Battleship server.
*/
private final BattleshipConfig config = new BattleshipConfig();
/**
* The server instance.
*/
private Server myServer; private Server myServer;
/**
* Game logic for the server.
*/
private final ServerGameLogic logic; private final ServerGameLogic logic;
/**
* Queue for pending messages to be processed by the server.
*/
private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>(); private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();
static { static {
@@ -50,73 +84,75 @@ public class BattleshipServer implements MessageListener<HostedConnection>, Conn
try { try {
manager.readConfiguration(new FileInputStream("logging.properties")); manager.readConfiguration(new FileInputStream("logging.properties"));
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
} } catch (IOException e) {
catch (IOException e) {
LOGGER.log(Level.INFO, e.getMessage()); LOGGER.log(Level.INFO, e.getMessage());
} }
} }
/** /**
* Creates the server. * Creates the server.
*/ */
public BattleshipServer() { public BattleshipServer(int PORT_NUMBER) {
BattleshipConfig config = new BattleshipConfig();
config.readFromIfExists(CONFIG_FILE); config.readFromIfExists(CONFIG_FILE);
this.PORT_NUMBER = PORT_NUMBER;
LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
logic = new ServerGameLogic(this, config); logic = new ServerGameLogic(this, config);
} }
/** /**
* Starts the server on the given port and continuously processes incoming messages. * Starts the server and processes incoming messages indefinitely.
* @param port the port to start the server on
*/ */
public void run(int port) { public void run() {
startServer(port); startServer();
while (true) while (true)
processNextMessage(); processNextMessage();
} }
/** /**
* Initializes and starts the server on the specified port, handling exceptions if the server fails to start. * Starts the server and initializes necessary components.
* @param port the port to start the server on * This method sets up the server, registers serializable classes,
* starts the server, and registers listeners for incoming connections and messages.
* If the server fails to start, it logs an error and exits the application.
*/ */
private void startServer(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_NUMBER);
initializeSerializables(); initializeSerializables();
myServer.start(); myServer.start();
registerListeners(); registerListeners();
LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS
} } catch (IOException e) {
catch (IOException e) {
LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS
exit(1); exit(1);
} }
} }
/** /**
* Retrieves and processes the next message from the queue, handling interruptions during the wait. * Processes the next message in the queue.
* This method blocks until a message is available, then processes it using the server logic.
* If interrupted while waiting, it logs the interruption and re-interrupts the thread.
*/ */
private void processNextMessage() { private void processNextMessage() {
try { try {
pendingMessages.take().process(logic); pendingMessages.take().process(logic);
} } catch (InterruptedException ex) {
catch (InterruptedException ex) {
LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
} }
/** /**
* Processes the next message from the queue, handling interruptions during message retrieval. * Registers the serializable classes used by the server.
* This method ensures that the necessary classes are registered with the serializer
* so that they can be correctly transmitted over the network.
*/ */
private void initializeSerializables() { private void initializeSerializables() {
Serializer.registerClass(GameDetails.class); Serializer.registerClass(GameDetails.class);
Serializer.registerClass(StartBattleMessage.class); Serializer.registerClass(StartBattleMessage.class);
Serializer.registerClass(MapMessage.class); Serializer.registerClass(MapMessage.class);
Serializer.registerClass(ShootMessage.class); Serializer.registerClass(ShootMessage.class);
Serializer.registerClass(AnimationMessage.class);
Serializer.registerClass(EffectMessage.class); Serializer.registerClass(EffectMessage.class);
Serializer.registerClass(Battleship.class); Serializer.registerClass(Battleship.class);
Serializer.registerClass(IntPoint.class); Serializer.registerClass(IntPoint.class);
@@ -124,18 +160,22 @@ private void initializeSerializables() {
} }
/** /**
* Registers message and connection listeners for the server. * Registers listeners for incoming connections and messages.
* This method adds message listeners for `MapMessage` and `ShootMessage` classes,
* and a connection listener for handling connection events.
*/ */
private void registerListeners() { private void registerListeners() {
myServer.addMessageListener(this, MapMessage.class); myServer.addMessageListener(this, MapMessage.class);
myServer.addMessageListener(this, ShootMessage.class); myServer.addMessageListener(this, ShootMessage.class);
myServer.addMessageListener(this, AnimationMessage.class);
myServer.addConnectionListener(this); myServer.addConnectionListener(this);
} }
/** /**
* Handles received messages, logging the source and adding client messages to the pending queue. * Handles the reception of messages from clients.
* @param source the connection the message was received from *
* @param message the received message * @param source the connection from which the message was received
* @param message the message received from the client
*/ */
@Override @Override
public void messageReceived(HostedConnection source, Message message) { public void messageReceived(HostedConnection source, Message message) {
@@ -145,9 +185,10 @@ public void messageReceived(HostedConnection source, Message message) {
} }
/** /**
* Handles a new connection by logging it and adding a new player to the game logic. * Called when a new connection is added to the server.
* @param server the server receiving the connection *
* @param hostedConnection the newly added connection * @param server the server to which the connection was added
* @param hostedConnection the connection that was added
*/ */
@Override @Override
public void connectionAdded(Server server, HostedConnection hostedConnection) { public void connectionAdded(Server server, HostedConnection hostedConnection) {
@@ -156,9 +197,10 @@ public void connectionAdded(Server server, HostedConnection hostedConnection) {
} }
/** /**
* Handles the removal of a connection by logging it, checking if it belongs to an active player, and exiting if necessary. * Called when a connection is removed from the server.
* @param server the server losing the connection *
* @param hostedConnection the removed connection * @param server the server from which the connection was removed
* @param hostedConnection the connection that was removed
*/ */
@Override @Override
public void connectionRemoved(Server server, HostedConnection hostedConnection) { public void connectionRemoved(Server server, HostedConnection hostedConnection) {
@@ -173,8 +215,10 @@ public void connectionRemoved(Server server, HostedConnection hostedConnection)
} }
/** /**
* Closes all client connections and terminates the server with the given exit value. * Exits the application with the specified exit value.
* @param exitValue the exit code to terminate the application with * Closes all client connections and logs the close request.
*
* @param exitValue the exit value to be used when exiting the application
*/ */
private void exit(int exitValue) { //NON-NLS private void exit(int exitValue) { //NON-NLS
LOGGER.log(Level.INFO, "close request"); //NON-NLS LOGGER.log(Level.INFO, "close request"); //NON-NLS

View File

@@ -34,7 +34,7 @@ public class GameSound extends AbstractAppState implements GameEventListener {
private AudioNode splashSound; private AudioNode splashSound;
private AudioNode shipDestroyedSound; private AudioNode shipDestroyedSound;
private AudioNode explosionSound; private AudioNode explosionSound;
private AudioNode missileLaunch; private AudioNode shellFiredSound;
/** /**
* Checks if sound is enabled in the preferences. * Checks if sound is enabled in the preferences.
@@ -45,6 +45,13 @@ public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true); return PREFERENCES.getBoolean(ENABLED_PREF, true);
} }
/**
* Toggles the game sound on or off.
*/
public void toggleSound() {
setEnabled(!isEnabled());
}
/** /**
* Sets the enabled state of this AppState. * Sets the enabled state of this AppState.
* Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)} * Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
@@ -72,7 +79,7 @@ public void initialize(AppStateManager stateManager, Application app) {
shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS
splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS
explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS
missileLaunch = loadSound(app, "Sound/Effects/missileLaunch.wav"); shellFiredSound = loadSound(app, "Sound/Effects/missle.wav"); //NON-NLS
} }
/** /**
@@ -94,13 +101,6 @@ private AudioNode loadSound(Application app, String name) {
return null; return null;
} }
/**
* Plays the missile launch sound effect.
*/
public void missileLaunch() {
missileLaunch.playInstance();
}
/** /**
* Plays the splash sound effect. * Plays the splash sound effect.
*/ */
@@ -126,16 +126,20 @@ public void shipDestroyed() {
} }
/** /**
* Checks the according case for the soundeffect * Plays sound effect when a shell has been fired.
* @param event the received event
*/ */
public void shellFired() {
if (isEnabled() && shellFiredSound != null)
shellFiredSound.playInstance();
}
@Override @Override
public void receivedEvent(SoundEvent event) { public void receivedEvent(SoundEvent event) {
switch (event.sound()) { switch (event.sound()) {
case EXPLOSION -> explosion(); case EXPLOSION -> explosion();
case SPLASH -> splash(); case SPLASH -> splash();
case DESTROYED_SHIP -> shipDestroyed(); case DESTROYED_SHIP -> shipDestroyed();
case MISSILE_LAUNCH -> missileLaunch(); case SHELL_FIRED -> shellFired();
} }
} }
} }

View File

@@ -9,20 +9,21 @@
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;
import pp.dialog.TextInputDialog; import pp.dialog.TextInputDialog;
import com.simsilica.lemur.DefaultRangedValueModel;
import com.simsilica.lemur.Slider;
import com.simsilica.lemur.core.VersionedReference;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import java.lang.System.Logger;
import static pp.battleship.Resources.lookup; import static pp.battleship.Resources.lookup;
import static pp.util.PreferencesUtils.getPreferences; import static pp.util.PreferencesUtils.getPreferences;
@@ -32,6 +33,7 @@
* returning to the game, and quitting the application. * returning to the game, and quitting the application.
*/ */
class Menu extends Dialog { class Menu extends Dialog {
private static final Logger LOGGER = System.getLogger(Menu.class.getName());
private static final Preferences PREFERENCES = getPreferences(Menu.class); private static final Preferences PREFERENCES = getPreferences(Menu.class);
private static final String LAST_PATH = "last.file.path"; private static final String LAST_PATH = "last.file.path";
private final BattleshipApp app; private final BattleshipApp app;
@@ -48,20 +50,17 @@ public Menu(BattleshipApp app) {
super(app.getDialogManager()); super(app.getDialogManager());
this.app = app; this.app = app;
addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS
addChild(new Checkbox(lookup("menu.sound-enabled"), addChild(new Checkbox(lookup("menu.sound-enabled"),
new StateCheckboxModel(app, GameSound.class))); new StateCheckboxModel(app, GameSound.class)));
Checkbox musicToggle = new Checkbox(lookup("menu.music.toggle")); addChild(new Checkbox(lookup("menu.music-toggle"),
musicToggle.setChecked(app.getBackgroundMusic().isMusicEnabled()); new StateCheckboxModel(app, BackgroundMusic.class)));
musicToggle.addClickCommands(s -> toggleMusic());
addChild(musicToggle);
Slider volumeSlider = new Slider(); Slider volumeSlider = new Slider();
volumeSlider.setModel(new DefaultRangedValueModel(0.0 , 2.0, app.getBackgroundMusic().getVolume())); volumeSlider.setModel(new DefaultRangedValueModel(0.0, 2.0, app.getStateManager().getState(BackgroundMusic.class).getVolume()));
volumeSlider.setDelta(0.1); volumeSlider.setDelta(0.1f);
addChild(volumeSlider); addChild(volumeSlider);
volumeRef = volumeSlider.getModel().createReference(); volumeRef = volumeSlider.getModel().createReference();
addChild(loadButton) addChild(loadButton)
@@ -75,33 +74,6 @@ public Menu(BattleshipApp app) {
update(); update();
} }
/**
* Updates the volume if there is a change and adjusts it accordingly.
* @param tmp unused time parameter
*/
public void update(float tmp){
if(volumeRef.update()) {
double newVolume = volumeRef.get();
adjustVolume(newVolume);
}
}
/**
* Adjusts the background music volume to the specified value.
* @param newVolume the new volume level
*/
private void adjustVolume(double newVolume) {
app.getBackgroundMusic().setVolume((float) newVolume);
}
/**
* Toggles the background music on or off.
*/
private void toggleMusic(){
app.getBackgroundMusic().toogleMusic();
}
/** /**
* 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.
*/ */
@@ -111,6 +83,28 @@ public void update() {
saveButton.setEnabled(app.getGameLogic().maySaveMap()); saveButton.setEnabled(app.getGameLogic().maySaveMap());
} }
/**
* Updates the menu state based on the time per frame (tpf).
* If the volume reference has been updated, adjusts the volume accordingly.
*
* @param tpf the time per frame
*/
@Override
public void update(float tpf) {
if (volumeRef.update()) {
adjustVolume(volumeRef.get());
}
}
/**
* Adjusts the volume of the background music.
*
* @param volume the new volume level to set, as a double
*/
private void adjustVolume(double volume) {
app.getStateManager().getState(BackgroundMusic.class).setVolume((float) volume);
}
/** /**
* As an escape action, this method closes the menu if it is the top dialog. * As an escape action, this method closes the menu if it is the top dialog.
*/ */
@@ -145,8 +139,7 @@ private void handle(FileAction fileAction, TextInputDialog dialog) {
PREFERENCES.put(LAST_PATH, path); PREFERENCES.put(LAST_PATH, path);
fileAction.run(new File(path)); fileAction.run(new File(path));
dialog.close(); dialog.close();
} } catch (IOException e) {
catch (IOException e) {
app.errorDialog(e.getLocalizedMessage()); app.errorDialog(e.getLocalizedMessage());
} }
} }

View File

@@ -7,9 +7,11 @@
package pp.battleship.client; package pp.battleship.client;
import com.simsilica.lemur.*; import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.TextField;
import com.simsilica.lemur.component.SpringGridLayout; import com.simsilica.lemur.component.SpringGridLayout;
import pp.battleship.client.server.BattleshipServer;
import pp.dialog.Dialog; import pp.dialog.Dialog;
import pp.dialog.DialogBuilder; import pp.dialog.DialogBuilder;
import pp.dialog.SimpleDialog; import pp.dialog.SimpleDialog;
@@ -29,6 +31,7 @@ class NetworkDialog extends SimpleDialog {
private static final Logger LOGGER = System.getLogger(NetworkDialog.class.getName()); private static final Logger LOGGER = System.getLogger(NetworkDialog.class.getName());
private static final String LOCALHOST = "localhost"; //NON-NLS private static final String LOCALHOST = "localhost"; //NON-NLS
private static final String DEFAULT_PORT = "1234"; //NON-NLS private static final String DEFAULT_PORT = "1234"; //NON-NLS
private static final int START_SERVER_DELAY = 2000;
private final NetworkSupport network; private final NetworkSupport network;
private final TextField host = new TextField(LOCALHOST); private final TextField host = new TextField(LOCALHOST);
private final TextField port = new TextField(DEFAULT_PORT); private final TextField port = new TextField(DEFAULT_PORT);
@@ -36,10 +39,7 @@ class NetworkDialog extends SimpleDialog {
private int portNumber; private int portNumber;
private Future<Object> connectionFuture; private Future<Object> connectionFuture;
private Dialog progressDialog; private Dialog progressDialog;
private BattleshipServer battleshipServer; private boolean hostServer = false;
private boolean GUI = true;
private Container menu;
private boolean serverToggle = false;
/** /**
* Constructs a new NetworkDialog. * Constructs a new NetworkDialog.
@@ -53,71 +53,33 @@ class NetworkDialog extends SimpleDialog {
host.setPreferredWidth(400f); host.setPreferredWidth(400f);
port.setSingleLine(true); port.setSingleLine(true);
this.menu = new Container(); Checkbox hostCheckbox = new Checkbox(lookup("host.own-server"));
menu(); hostCheckbox.setChecked(false);
addChild(menu); hostCheckbox.addClickCommands(s -> hostServer = !hostServer);
}
/**
* Main Menu creation
*/
private void menu() {
Button connectGame = new Button("Multiplayer");
connectGame.addClickCommands(source -> serverConnectGUI());
Checkbox serverHost = new Checkbox("Host own Game");
serverHost.addClickCommands(source -> toggleServer());
menu.addChild(serverHost);
menu.addChild(connectGame);
}
/**
* Logic for the ServerGUI
*/
private void serverConnectGUI() {
LOGGER.log(Level.INFO, "Hosting Server...");
if (GUI) {
host.setSingleLine(true);
host.setPreferredWidth(400f);
port.setSingleLine(true);
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);
input.addChild(hostCheckbox);
DialogBuilder.simple(app.getDialogManager()) DialogBuilder.simple(app.getDialogManager())
.setTitle(lookup("server.dialog")) .setTitle(lookup("server.dialog"))
.setExtension(d -> d.addChild(input)) .setExtension(d -> d.addChild(input))
.setOkButton(lookup("button.connect"), d -> connect()) .setOkButton(lookup("button.connect"), d -> connectHostServer())
.setNoButton(lookup("button.cancel"), app::closeApp)
.setOkClose(false) .setOkClose(false)
.setNoClose(false)
.build(this); .build(this);
GUI = false;
}
}
/**
* Connect to server if serverToggle is false hosts otherwise
*/
private void connect(){
if (serverToggle) {
hostServer();
} else {
connectServer();
}
} }
/** /**
* Handles the action for the connect button in the connection dialog. * Handles the action for the connect button in the connection dialog.
* Tries to parse the port number and initiate connection to the server. * Tries to parse the port number and initiate connection to the server.
*/ */
private void connectServer() { private void connect() {
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();
@@ -130,39 +92,37 @@ private void connectServer() {
} }
/** /**
* Checks if the Server is running or starts the Server and handles Exceptions * * Connects to the host server. If the `hostServer` flag is set, it starts the server
* before attempting to connect. If the server fails to start, logs an error.
*/ */
private void hostServer() { private void connectHostServer() {
if (battleshipServer == null) { if (hostServer) {
startServer(); // Starts the server in a new thread startServer();
try { try {
Thread.sleep(1000); // Wait for the server to start Thread.sleep(START_SERVER_DELAY);
} catch (InterruptedException e) { } catch (Exception e) {
LOGGER.log(Level.WARNING, e.getMessage(), e); LOGGER.log(Level.ERROR, "Server start failed", e); //NON-NLS
} }
connect();
} else {
connect();
} }
// Proceed with connecting to the server
connectServer();
} }
/** /**
* Toggle Method for Boolean * Starts the game server in a new thread.
*/ * Logs an error if the server fails to start.
private void toggleServer(){
serverToggle = !serverToggle;
}
/**
* Allows Client to Start a Server in a new Thread
*/ */
private void startServer() { private void startServer() {
LOGGER.log(Level.INFO, "start server"); //NON-NLS
new Thread(() -> { new Thread(() -> {
try { try {
// Initialize and run the server LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
battleshipServer = new BattleshipServer(); BattleshipServer server = new BattleshipServer(Integer.parseInt(port.getText()));
battleshipServer.run(Integer.parseInt(port.getText())); LOGGER.log(Level.INFO, "Server started"); //NON-NLS
server.run();
} catch (Exception e) { } catch (Exception e) {
LOGGER.log(Level.ERROR, e); LOGGER.log(Level.ERROR, "Server start failed", e); //NON-NLS
} }
}).start(); }).start();
} }

View File

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

View File

@@ -1,155 +0,0 @@
package pp.battleship.client.gui;
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.client.BattleshipApp;
import pp.battleship.model.Shot;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
/**
* Manages the visual effects for impact events such as hits and misses.
*/
public class ImpactEffectManager {
private final AssetManager assetManager;
private static final Logger LOGGER = System.getLogger(ImpactEffectManager.class.getName());
private Material particleMaterial;
private BattleshipApp app;
/**
* Constructor to initialize the asset manager via the main application.
*
* @param app The main application instance.
*/
public ImpactEffectManager(BattleshipApp app) {
this.app = app;
this.assetManager = app.getAssetManager();
this.particleMaterial = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
}
/**
* Generates a visual effect for a hit at the specified position.
*
* @param battleshipNode The node where the effect should be attached.
* @param shot The details of the shot where the hit occurred.
*/
public void triggerHitEffect(Node battleshipNode, Shot shot) {
ParticleEmitter hitEffect = createParticleEmitter("HitEffect", 50, ColorRGBA.Orange, ColorRGBA.Red, 0.45f, 0.1f, 1f, 2f);
hitEffect.setLocalTranslation(shot.getY() + 0.5f, 0, shot.getX() + 0.5f);
LOGGER.log(Level.DEBUG, "Hit effect created at position: {0}", hitEffect.getLocalTranslation().toString());
// hitEffect.emitAllParticles();
hitEffect.addControl(new EffectCleanupControl(hitEffect));
app.getRootNode().attachChild(hitEffect);
}
/**
* Creates a visual effect for a missed shot at the specified location.
*
* @param shot The details of the missed shot.
* @return The particle emitter representing the miss effect.
*/
public ParticleEmitter triggerMissEffect(Shot shot) {
ParticleEmitter missEffect = createParticleEmitter("MissEffect", 50, ColorRGBA.Blue, ColorRGBA.Cyan, 0.3f, 0.05f, 0.5f, 1.5f);
missEffect.setLocalTranslation(shot.getY() + 0.5f, 0, shot.getX() + 0.5f);
missEffect.addControl(new EffectCleanupControl(missEffect));
return missEffect;
}
/**
* Helper method to create a particle emitter with predefined parameters.
*
* @param name The name of the particle emitter.
* @param count The number of particles to emit.
* @param startColor The initial color of the particles.
* @param endColor The final color of the particles.
* @param startSize The initial size of the particles.
* @param endSize The final size of the particles.
* @param lowLife The minimum lifetime of the particles.
* @param highLife The maximum lifetime of the particles.
* @return The configured ParticleEmitter instance.
*/
private ParticleEmitter createParticleEmitter(String name, int count, ColorRGBA startColor, ColorRGBA endColor, float startSize, float endSize, float lowLife, float highLife) {
ParticleEmitter emitter = new ParticleEmitter(name, Type.Triangle, count);
emitter.setNumParticles(0);
emitter.setMaterial(particleMaterial);
emitter.setImagesX(2);
emitter.setImagesY(2);
emitter.setStartColor(startColor);
emitter.setEndColor(endColor);
emitter.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 1, 0));
emitter.setStartSize(startSize);
emitter.setEndSize(endSize);
emitter.setGravity(0, -0.5f, 0);
emitter.setLowLife(lowLife);
emitter.setHighLife(highLife);
return emitter;
}
/**
* Custom control class to handle the cleanup of particle effects once they are no longer visible.
*/
private static class EffectCleanupControl extends AbstractControl {
private final ParticleEmitter emitter;
private float currentTime = 0;
private final float duration = 3.5f;
/**
* Constructor for managing cleanup of a standalone particle emitter.
*
* @param emitter The particle emitter to manage.
*/
public EffectCleanupControl(ParticleEmitter emitter) {
this.emitter = emitter;
}
/**
* Removes the emitter when all particles are no longer visible.
*
* @param tpf Time per frame.
*/
@Override
protected void controlUpdate(float tpf) {
currentTime += tpf;
if (currentTime <= duration) {
}
if (currentTime >= 1f && currentTime <= 1.1) {
//Start
emitter.setNumParticles(50);
emitter.setParticlesPerSec(50);
}
if (currentTime >= duration){
emitter.setParticlesPerSec(0);
}
if (currentTime >= duration+1f) {
//Ende
spatial.removeFromParent();
emitter.emitAllParticles();
}
}
/**
* No custom rendering needed; particle behavior is handled in controlUpdate.
* @param rm handles rendering, and ViewPort
* @param vp defines the view where the scene is displayed.
*/
@Override
protected void controlRender(com.jme3.renderer.RenderManager rm, com.jme3.renderer.ViewPort vp) {
// No rendering-specific behavior needed for this control.
}
}
}

View File

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

View File

@@ -7,15 +7,19 @@
package pp.battleship.client.gui; package pp.battleship.client.gui;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Sphere;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import pp.battleship.model.Shot; import pp.battleship.model.Shot;
import pp.util.Position; import pp.util.Position;
import java.lang.System.Logger;
import static com.jme3.material.Materials.UNSHADED;
/** /**
* Synchronizes the visual representation of the ship map with the game model. * Synchronizes the visual representation of the ship map with the game model.
@@ -29,8 +33,6 @@ class MapViewSynchronizer extends ShipMapSynchronizer {
private static final float SHIP_DEPTH = 0f; private static final float SHIP_DEPTH = 0f;
private static final float INDENT = 4f; private static final float INDENT = 4f;
private static final float SHELL_DEPTH = 8f;
// 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;
private static final ColorRGBA MISS_COLOR = ColorRGBA.Blue; private static final ColorRGBA MISS_COLOR = ColorRGBA.Blue;
@@ -114,21 +116,22 @@ public Spatial visit(Battleship ship) {
} }
/** /**
* Creates a visual representation (Spatial) for the given shell, attaching a control for its behavior. * Creates a visual representation of a shell on the map.
* @param shell the shell to visit and visualize * The shell is represented as a black ellipse.
* @return the constructed shell node (Spatial) *
* @param shell the Shell object representing the shell in the model
* @return a Spatial representing the shell on the map
*/ */
@Override @Override
public Spatial visit(Shell shell) { public Spatial visit(Shell shell) {
LOGGER.log(Logger.Level.DEBUG, "Visiting {0}", shell); Geometry ellipse = new Geometry("ellipse", new Sphere(50, 50, MapView.getFieldSize() / 2 * 0.8f));
final Node shellNode = new Node("shell"); Material mat = new Material(view.getApp().getAssetManager(), UNSHADED); //NON-NLS
final Position startPosition = view.modelToView(shell.getCurrentPosition().x,shell.getCurrentPosition().z); mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
shellNode.attachChild(view.getApp().getDraw().makeRectangle(startPosition.getX(),startPosition.getY(), SHELL_DEPTH, 30, 30, ColorRGBA.Black)); mat.setColor("Color", ColorRGBA.Black);
ellipse.setMaterial(mat);
shellNode.addControl(new ShellControl(shell, this.view, shell.getLogic())); ellipse.addControl(new ShellMapControl(view, shell));
return shellNode; return ellipse;
} }
/** /**

View File

@@ -7,15 +7,18 @@
package pp.battleship.client.gui; package pp.battleship.client.gui;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode; import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
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;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box; import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Cylinder;
import pp.battleship.client.BattleshipApp; import pp.battleship.client.BattleshipApp;
import pp.battleship.model.*; import pp.battleship.model.*;
@@ -31,21 +34,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 KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o";//NON-NLS private static final String KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o"; //NON-NLS
private static final String DESTROYER = "Models/Destroyer/Destroyer.j3o"; private static final String DESTROYER_MODEL = "Models/Destroyer/Destroyer.j3o"; //NON-NLS
private static final String FERRY = "Models/Ferry/13922_Staten_Island_Ferry_V1_l1.obj"; private static final String DESTROYER_TEXTURE = "Models/Destroyer/BattleshipC.jpg"; //NON-NLS
private static final String SMALL = "Models/Small/10634_SpeedBoat_v01_LOD3.obj"; private static final String TYPE_II_UBOAT_MODEL = "Models/TypeIIUboat/TypeIIUboat.j3o"; //NON-NLS
private static final String BOMB = "Models/Bomb/BombGBU.j3o"; private static final String TYPE_II_UBOAT_TEXTURE = "Models/TypeIIUboat/Type_II_U-boat_diff.jpg"; //NON-NLS
private static final String ATLANTICA_MODEL = "Models/Atlantica/Atlantica.j3o"; //NON-NLS
private static final String ROCKET = "Models/Rocket/Rocket.j3o"; //NON-NLS
private static final String COLOR = "Color"; //NON-NLS private static final String COLOR = "Color"; //NON-NLS
private static final String SHIP = "ship"; //NON-NLS private static final String SHIP = "ship"; //NON-NLS
private static final String SHOT = "shot"; //NON-NLS private static final String SHELL = "shell"; //NON-NLS
private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray; private static final ColorRGBA BOX_COLOR = ColorRGBA.Gray;
private static final ColorRGBA SPLASH_COLOR = new ColorRGBA(0f, 0f, 1f, 0.4f);
private static final ColorRGBA HIT_COLOR = new ColorRGBA(1f, 0f, 0f, 0.4f);
private final ShipMap map; private final ShipMap map;
private final BattleshipApp app; private final BattleshipApp app;
private ImpactEffectManager effectHandler;
/** /**
* Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map. * Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map.
@@ -58,7 +61,6 @@ public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) {
super(app.getGameLogic().getOwnMap(), root); super(app.getGameLogic().getOwnMap(), root);
this.app = app; this.app = app;
this.map = map; this.map = map;
effectHandler = new ImpactEffectManager(app);
addExisting(); addExisting();
} }
@@ -72,7 +74,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) : effectHandler.triggerMissEffect(shot); return shot.isHit() ? handleHit(shot) : handleMiss(shot);
} }
/** /**
@@ -87,32 +89,120 @@ 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");
System.out.println(shot.getX() + " " + shot.getY()); final ParticleEmitter debris = createDebrisEffect(shot);
effectHandler.triggerHitEffect(shipNode, shot); shipNode.attachChild(debris);
final ParticleEmitter fire = createFireEffect(shot, shipNode);
shipNode.attachChild(fire);
return null; return null;
} }
private Spatial handleMiss(Shot shot) {
return createMissEffect(shot);
}
private ParticleEmitter createMissEffect(Shot shot) {
final ParticleEmitter water = new ParticleEmitter("WaterEmitter", ParticleMesh.Type.Triangle, 20);
Material waterMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
waterMaterial.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/flame.png"));
water.setMaterial(waterMaterial);
water.setImagesX(2);
water.setImagesY(2);
water.setStartColor(ColorRGBA.Cyan);
water.setEndColor(ColorRGBA.Blue);
water.getParticleInfluencer().setInitialVelocity(new Vector3f(0.1f, 0.1f, 0.1f));
water.setStartSize(0.4f);
water.setEndSize(0.45f);
water.setGravity(0, -0.5f, 0);
water.setLowLife(1f);
water.setHighLife(1f);
water.setParticlesPerSec(0);
water.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
water.emitAllParticles();
return water;
}
private ParticleEmitter createDebrisEffect(Shot shot) {
final ParticleEmitter debris = new ParticleEmitter("DebrisEmitter", ParticleMesh.Type.Triangle, 2);
Material debrisMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
debrisMaterial.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/Debris.png"));
debris.setMaterial(debrisMaterial);
debris.setImagesX(2);
debris.setImagesY(2);
debris.setStartColor(ColorRGBA.White);
debris.setEndColor(ColorRGBA.White);
debris.getParticleInfluencer().setInitialVelocity(new Vector3f(0.1f, 2f, 0.1f));
debris.setStartSize(0.1f);
debris.setEndSize(0.5f);
debris.setGravity(0, 3f, 0);
debris.getParticleInfluencer().setVelocityVariation(.40f);
debris.setLowLife(1f);
debris.setHighLife(1.5f);
debris.setParticlesPerSec(0);
debris.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
debris.emitAllParticles();
return debris;
}
private ParticleEmitter createFireEffect(Shot shot, Node shipNode) {
ParticleEmitter fire = new ParticleEmitter("FireEmitter", ParticleMesh.Type.Triangle, 100);
Material fireMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
fireMaterial.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/flame.png"));
fire.setMaterial(fireMaterial);
fire.setImagesX(2);
fire.setImagesY(2);
fire.setStartColor(ColorRGBA.Orange);
fire.setEndColor(ColorRGBA.Red);
fire.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 1.5f, 0));
fire.setStartSize(0.2f);
fire.setEndSize(0.05f);
fire.setLowLife(1f);
fire.setHighLife(2f);
fire.getParticleInfluencer().setVelocityVariation(0.2f);
fire.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
fire.getLocalTranslation().subtractLocal(shipNode.getLocalTranslation());
return fire;
}
/** /**
* Creates a cylinder geometry representing the specified shot. * Visits a {@link Shell} and creates a graphical representation of it.
* The appearance of the cylinder depends on whether the shot is a hit or a miss. * The shell is represented as a node with a model attached to it.
* The node is then positioned and controlled by a {@link ShellControl}.
* *
* @param shot the shot to be represented * @param shell the shell to be represented
* @return the geometry representing the shot * @return the node containing the graphical representation of the shell
*/ */
private Geometry createCylinder(Shot shot) { @Override
final ColorRGBA color = shot.isHit() ? HIT_COLOR : SPLASH_COLOR; public Spatial visit(Shell shell) {
final float height = shot.isHit() ? 1.2f : 0.1f; final Node node = new Node(SHELL);
node.attachChild(createShell());
node.setLocalTranslation(shell.getY() + 0.5f, 10f, shell.getX() + 0.5f);
node.addControl(new ShellControl());
return node;
}
final Cylinder cylinder = new Cylinder(2, 20, 0.45f, height, true); /**
final Geometry geometry = new Geometry(SHOT, cylinder); * Creates a graphical representation of a shell.
*
geometry.setMaterial(createColoredMaterial(color)); * @return the spatial representing the shell
geometry.rotate(HALF_PI, 0f, 0f); */
private Spatial createShell() {
geometry.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f); final Spatial model = app.getAssetManager().loadModel(ROCKET);
model.scale(0.0025f);
return geometry; model.rotate(PI, 0f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
} }
/** /**
@@ -135,42 +225,21 @@ public Spatial visit(Battleship ship) {
return node; return node;
} }
@Override
public Spatial visit(Shell shell) {
final Spatial bombModel = app.getAssetManager().loadModel(BOMB);
// Apply transformations to the bomb model (scale, rotate, etc. if needed)
bombModel.scale(0.05f);
bombModel.rotate(-HALF_PI, 0f, 0f);
// Set the position of the bomb at the shell's current position
bombModel.setLocalTranslation(shell.getCurrentPosition());
// Add a control to animate the bomb's movement (similar to the previous cylinder animation)
bombModel.addControl(new ShellControl(shell,shell.getLogic()));
return bombModel;
}
/** /**
* Creates the appropriate graphical representation of the specified battleship. * Creates the appropriate graphical representation of the specified battleship.
* The representation is a detailed model based on the length of the ship. * The representation is either a detailed model or a simple box based on the length of the ship.
* *
* @param ship the battleship to be represented * @param ship the battleship to be represented
* @return the spatial representing the battleship * @return the spatial representing the battleship
*/ */
private Spatial createShip(Battleship ship) { private Spatial createShip(Battleship ship) {
// return ship.getLength() == 4 ? createBattleship(ship) : createBox(ship); return switch (ship.getLength()) {
if (ship.getLength() == 4) { case 1 -> createVessel(ship);
return createBattleship(ship); case 2 -> createSubmarine(ship);
} else if (ship.getLength() == 3) { case 3 -> createDestroyer(ship);
return createBigShip(ship); case 4 -> createBattleship(ship);
} else if (ship.getLength() == 2) { default -> createBox(ship);
return createMediumShip(ship); };
} else if (ship.getLength() == 1) {
return createSmallShip(ship);
}
return createBattleship(ship);
} }
/** /**
@@ -184,7 +253,7 @@ private Spatial createBox(Battleship ship) {
0.3f, 0.3f,
0.5f * (ship.getMaxX() - ship.getMinX()) + 0.3f); 0.5f * (ship.getMaxX() - ship.getMinX()) + 0.3f);
final Geometry geometry = new Geometry(SHIP, box); final Geometry geometry = new Geometry(SHIP, box);
geometry.setMaterial(createColoredMaterial(BOX_COLOR)); geometry.setMaterial(createColoredMaterial());
geometry.setShadowMode(ShadowMode.CastAndReceive); geometry.setShadowMode(ShadowMode.CastAndReceive);
return geometry; return geometry;
@@ -196,16 +265,14 @@ private Spatial createBox(Battleship ship) {
* the material's render state is set to use alpha blending, allowing for * the material's render state is set to use alpha blending, allowing for
* semi-transparent rendering. * semi-transparent rendering.
* *
* @param color the {@link ColorRGBA} to be applied to the material. If the alpha value
* of the color is less than 1, the material will support transparency.
* @return a {@link Material} instance configured with the specified color and, * @return a {@link Material} instance configured with the specified color and,
* if necessary, alpha blending enabled. * if necessary, alpha blending enabled.
*/ */
private Material createColoredMaterial(ColorRGBA color) { private Material createColoredMaterial() {
final Material material = new Material(app.getAssetManager(), UNSHADED); final Material material = new Material(app.getAssetManager(), UNSHADED);
if (color.getAlpha() < 1f) if (SeaSynchronizer.BOX_COLOR.getAlpha() < 1f)
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha); material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
material.setColor(COLOR, color); material.setColor(COLOR, SeaSynchronizer.BOX_COLOR);
return material; return material;
} }
@@ -226,47 +293,62 @@ private Spatial createBattleship(Battleship ship) {
} }
/** /**
* Creates a detailed 3D model to represent a "Big Ship" * Creates a detailed 3D model to represent a destroyer battleship.
* @param ship The Battleship object with position and rotation info. *
* @return the spatial representing the "Big Ship" battleship * @param ship the battleship to be represented
* @return the spatial representing the destroyer battleship
*/ */
private Spatial createBigShip(Battleship ship) { private Spatial createDestroyer(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(DESTROYER); final Spatial model = app.getAssetManager().loadModel(DESTROYER_MODEL);
model.move(0,0.4f,0); Material mat = new Material(app.getAssetManager(), UNSHADED);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()) + HALF_PI, 0f); mat.setTexture("ColorMap", app.getAssetManager().loadTexture(DESTROYER_TEXTURE));
model.scale(0.0001f); mat.getAdditionalRenderState().setBlendMode(BlendMode.Off);
model.setShadowMode(ShadowMode.CastAndReceive); model.setMaterial(mat);
return model; model.setQueueBucket(RenderQueue.Bucket.Opaque);
}
/**
* Creates a detailed 3D model to represent a "Medium Ship"
* @param ship The Battleship object with position and rotation info.
* @return the spatial representing the "Medium Ship" battleship
*/
private Spatial createMediumShip(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(FERRY);
model.move(0,0.25f,0);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f); model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.00025f); model.scale(0.1f);
model.setLocalTranslation(0f, 0.25f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive); model.setShadowMode(ShadowMode.CastAndReceive);
return model; return model;
} }
/** /**
* Creates a detailed 3D model to represent a "Small Ship" * Creates a detailed 3D model to represent a Type II U-boat submarine.
* @param ship The Battleship object with position and rotation info. *
* @return the spatial representing the "Small Ship" battleship * @param ship the battleship to be represented
* @return the spatial representing the Type II U-boat submarine
*/ */
private Spatial createSmallShip(Battleship ship) { private Spatial createSubmarine(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(SMALL); final Spatial model = app.getAssetManager().loadModel(TYPE_II_UBOAT_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()) + HALF_PI, 0f); Material mat = new Material(app.getAssetManager(), UNSHADED);
model.scale(0.0009f); mat.setTexture("ColorMap", app.getAssetManager().loadTexture(TYPE_II_UBOAT_TEXTURE));
model.setMaterial(mat);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.25f);
model.getLocalTranslation().addLocal(0f, -0.15f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/**
* Creates a detailed 3D model to represent a vessel.
*
* @param ship the battleship to be represented
* @return the spatial representing the vessel
*/
private Spatial createVessel(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(ATLANTICA_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.0003f);
model.getLocalTranslation().addLocal(0f, -0.05f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive); model.setShadowMode(ShadowMode.CastAndReceive);
return model; return model;

View File

@@ -1,96 +1,42 @@
////////////////////////////////////////
// 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.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.game.client.ClientGameLogic;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.Shell;
import pp.battleship.notification.Sound;
import pp.util.Position;
/** /**
* Controls the oscillating pitch motion of a battleship model in the game. * Controls the movement and rotation of a shell in the game.
* The ship oscillates to simulate a realistic movement on water, based on its orientation and length. * The shell moves downward at a constant speed and rotates around its Y-axis.
* When the shell reaches a certain Y-coordinate, it is removed from its parent node.
*/ */
class ShellControl extends AbstractControl { public class ShellControl extends AbstractControl {
private final static float SHELL_SPEED = 7.5f;
private final Shell shell; private final static float SHELL_ROTATION_SPEED = 0.5f;
private MapView view; private final static float MIN_HEIGHT = 0.7f;
private final ClientGameLogic logic;
private final EffectMessage msg;
private boolean hasPlayedSound = false;
/** /**
* Constructs a new ShipControl instance for the specified Battleship. * Updates the shell's position and rotation.
* The ship's orientation determines the axis of rotation, while its length influences * If the shell's Y-coordinate is less than or equal to 1.0, it is detached from its parent node.
* the cycle duration and amplitude of the oscillation.
* *
* @param shell the Battleship object to control * @param tpf time per frame, used to ensure consistent movement speed across different frame rates
*/
public ShellControl(Shell shell, ClientGameLogic clientGameLogic) {
this.shell = shell;
this.logic = clientGameLogic;
this.msg = shell.getMsg();
}
/**
* Initializes ShellControl with a shell, map view, and game logic.
* @param shell the shell to be controlled
* @param view the map view to display the shell
* @param clientGameLogic the game logic instance
*/
public ShellControl(Shell shell, MapView view, ClientGameLogic clientGameLogic) {
this.shell = shell;
this.view = view;
this.logic = clientGameLogic;
this.msg = shell.getMsg();
}
/**
* Updates the ship's pitch oscillation each frame. The ship's pitch is adjusted
* to create a continuous tilting motion, simulating the effect of waves.
*
* @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) {
spatial.move(0, -SHELL_SPEED * tpf, 0);
if (spatial == null) spatial.rotate(0, SHELL_ROTATION_SPEED, 0);
return; if (spatial.getLocalTranslation().getY() <= MIN_HEIGHT) {
if (shell.isFinished() && !hasPlayedSound) { spatial.getParent().detachChild(spatial);
if (!msg.getShot().isHit())
logic.playSound(Sound.SPLASH);
else if (msg.getDestroyedShip() == null)
logic.playSound(Sound.EXPLOSION);
else
logic.playSound(Sound.DESTROYED_SHIP);
hasPlayedSound = true;
}
if (view == null) {
shell.updatePosition(tpf);
spatial.setLocalTranslation(shell.getCurrentPosition());
} else {
shell.updatePosition(tpf);
Position pos2d = view.modelToView(shell.getCurrentPosition().x - 0.3f, shell.getCurrentPosition().z - 0.3f);
spatial.setLocalTranslation(pos2d.getY(), pos2d.getX(), 0);
} }
} }
/** /**
* Called during rendering, but no operations are needed as this control only affects spatial transformation. * Renders the shell. This method is currently not used.
* @param rm the RenderManager rendering the spatial *
* @param vp the ViewPort being rendered * @param rm the RenderManager
* @param vp the ViewPort
*/ */
@Override @Override
protected void controlRender(RenderManager rm, ViewPort vp) { protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering logic is needed for this control // nothing to do here
} }
} }

View File

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

View File

@@ -14,10 +14,6 @@
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import static pp.util.FloatMath.DEG_TO_RAD; import static pp.util.FloatMath.DEG_TO_RAD;
import static pp.util.FloatMath.TWO_PI; import static pp.util.FloatMath.TWO_PI;
import static pp.util.FloatMath.sin; import static pp.util.FloatMath.sin;
@@ -51,14 +47,10 @@ class ShipControl extends AbstractControl {
* The current time within the oscillation cycle, used to calculate the ship's pitch angle. * The current time within the oscillation cycle, used to calculate the ship's pitch angle.
*/ */
private float time; private float time;
/**
* The current Ship private final Battleship ship;
*/
private final Battleship battleship; private static final float SINKING_HEIGHT = -0.6f;
/**
* Logger for logging messages related to ShipControl operations.
*/
static final Logger LOGGER = System.getLogger(ShipControl.class.getName());
/** /**
* Constructs a new ShipControl instance for the specified Battleship. * Constructs a new ShipControl instance for the specified Battleship.
@@ -68,7 +60,6 @@ class ShipControl extends AbstractControl {
* @param ship the Battleship object to control * @param ship the Battleship object to control
*/ */
public ShipControl(Battleship ship) { public ShipControl(Battleship ship) {
battleship = ship;
// Determine the axis of rotation based on the ship's orientation // Determine the axis of rotation based on the ship's orientation
axis = switch (ship.getRot()) { axis = switch (ship.getRot()) {
case LEFT, RIGHT -> Vector3f.UNIT_X; case LEFT, RIGHT -> Vector3f.UNIT_X;
@@ -76,8 +67,10 @@ public ShipControl(Battleship ship) {
}; };
// Set the cycle duration and amplitude based on the ship's length // Set the cycle duration and amplitude based on the ship's length
cycle = battleship.getLength() * 2f; cycle = ship.getLength() * 2f;
amplitude = 5f * DEG_TO_RAD / ship.getLength(); amplitude = 5f * DEG_TO_RAD / ship.getLength();
this.ship = ship;
} }
/** /**
@@ -91,25 +84,28 @@ protected void controlUpdate(float tpf) {
// If spatial is null, do nothing // If spatial is null, do nothing
if (spatial == null) return; if (spatial == null) return;
// Update the time within the oscillation cycle // Handle ship sinking by moving it downwards
time = (time + tpf) % cycle; if (ship.isDestroyed()) {
if (battleship.isDestroyed() && spatial.getLocalTranslation().getY() <= -0.6f) { if (spatial.getLocalTranslation().getY() < SINKING_HEIGHT) {
LOGGER.log(Level.INFO, "Ship removed {0}", spatial.getName());
spatial.getParent().detachChild(spatial); spatial.getParent().detachChild(spatial);
} else if (battleship.isDestroyed()) {
spatial.move(0, -0.05f * tpf, 0);
} else { } else {
spatial.move(0, -tpf * 0.1f, 0);
}
}
// Update the time within the oscillation cycle // Update the time within the oscillation cycle
time = (time + tpf) % cycle; time = (time + tpf) % cycle;
// Calculate the current angle of the oscillation
final float angle = amplitude * sin(time * TWO_PI / cycle);
// Update the pitch Quaternion with the new angle
pitch.fromAngleAxis(angle, axis);
// Apply the pitch rotation to the spatial // Apply the pitch rotation to the spatial
spatial.setLocalRotation(pitch); spatial.setLocalRotation(pitch);
} }
spatial.setLocalRotation(pitch);
}
/** /**
* This method is called during the rendering phase, but it does not perform any * 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 * operations in this implementation as the control only influences the spatial's

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

File diff suppressed because it is too large Load Diff

View File

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

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

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +0,0 @@
# Blender MTL File: 'untitletttd.blend'
# Material Count: 1
newmtl None
Ns 0
Ka 0.000000 0.000000 0.000000
Kd 0.8 0.8 0.8
Ks 0.8 0.8 0.8
d 1
illum 2
map_Kd C:\Users\Convidado.Cliente-JMF-PC\Desktop\ghftht.png

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -1,64 +0,0 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 13.03.2012 15:35:28
newmtl metall
Ns 31.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.2431 0.2431 0.2431
Kd 0.2431 0.2431 0.2431
Ks 0.5850 0.5850 0.5850
Ke 0.0000 0.0000 0.0000
newmtl 13922_StatenIslandFerry
Ns 50.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.5850 0.5850 0.5850
Ke 0.0000 0.0000 0.0000
map_Ka 13922_StatenIslandFerry_diffuse.jpg
map_Kd 13922_StatenIslandFerry_diffuse.jpg
newmtl metall2
Ns 50.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.2118 0.2824 0.3451
Kd 0.2118 0.2824 0.3451
Ks 0.5850 0.5850 0.5850
Ke 0.0000 0.0000 0.0000
newmtl boat
Ns 34.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.6588 0.3059 0.1294
Kd 0.6588 0.3059 0.1294
Ks 0.3600 0.3600 0.3600
Ke 0.0000 0.0000 0.0000
newmtl white
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.9686 0.9686 0.9686
Kd 0.9686 0.9686 0.9686
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 717 KiB

View File

@@ -1,28 +0,0 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 08.06.2011 15:26:00
newmtl _10634_SpeedBoat_v01_LOD310634_SpeedBoat_v01
Ns 53.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.5882 0.5882 0.5882
Kd 0.5882 0.5882 0.5882
Ks 0.2000 0.2000 0.2000
Ke 0.0000 0.0000 0.0000
map_Ka 10634_SpeedBoat_v01.jpg
map_Kd 10634_SpeedBoat_v01.jpg
newmtl glass
Ns 80.0000
Ni 1.5000
d 0.2000
Tr 0.8000
Tf 0.2000 0.2000 0.2000
illum 2
Ka 0.5882 0.5882 0.5882
Kd 0.5882 0.5882 0.5882
Ks 0.5000 0.5000 0.5000
Ke 0.0000 0.0000 0.0000

View File

@@ -1,3 +0,0 @@
based on:
https://free3d.com/3d-model/speedboat-v01--840133.html
License: Free Personal Use Only

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@@ -0,0 +1,16 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 29.03.2012 14:25:39
newmtl default
Ns 35.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.5400 0.5400 0.5400
Ke 0.0000 0.0000 0.0000
map_Ka 14084_WWII_ship_German_Type_II_U-boat_diff.jpg
map_Kd 14084_WWII_ship_German_Type_II_U-boat_diff.jpg

View File

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

View File

@@ -41,7 +41,7 @@ public static void main(String[] args) {
*/ */
@Override @Override
public void simpleInitApp() { public void simpleInitApp() {
export("Models/Destroyer/10619_Battleship.obj", "Destroyer.j3o"); //NON-NLS export("Models/KingGeorgeV/King_George_V.obj", "KingGeorgeV.j3o"); //NON-NLS
stop(); stop();
} }

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +0,0 @@
# Blender MTL File: 'untitletttd.blend'
# Material Count: 1
newmtl None
Ns 0
Ka 0.000000 0.000000 0.000000
Kd 0.8 0.8 0.8
Ks 0.8 0.8 0.8
d 1
illum 2
map_Kd C:\Users\Convidado.Cliente-JMF-PC\Desktop\ghftht.png

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

View File

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

View File

@@ -1,64 +0,0 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 13.03.2012 15:35:28
newmtl metall
Ns 31.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.2431 0.2431 0.2431
Kd 0.2431 0.2431 0.2431
Ks 0.5850 0.5850 0.5850
Ke 0.0000 0.0000 0.0000
newmtl 13922_StatenIslandFerry
Ns 50.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.5850 0.5850 0.5850
Ke 0.0000 0.0000 0.0000
map_Ka 13922_StatenIslandFerry_diffuse.jpg
map_Kd 13922_StatenIslandFerry_diffuse.jpg
newmtl metall2
Ns 50.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.2118 0.2824 0.3451
Kd 0.2118 0.2824 0.3451
Ks 0.5850 0.5850 0.5850
Ke 0.0000 0.0000 0.0000
newmtl boat
Ns 34.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.6588 0.3059 0.1294
Kd 0.6588 0.3059 0.1294
Ks 0.3600 0.3600 0.3600
Ke 0.0000 0.0000 0.0000
newmtl white
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.9686 0.9686 0.9686
Kd 0.9686 0.9686 0.9686
Ks 0.0000 0.0000 0.0000
Ke 0.0000 0.0000 0.0000

View File

@@ -0,0 +1,159 @@
package pp.battleship.game.client;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Shell;
import pp.battleship.model.ShipMap;
import pp.battleship.notification.Music;
import pp.battleship.notification.Sound;
/**
* Represents the state of the game during an animation sequence.
* This state handles the progress and completion of the animation,
* updates the game state accordingly, and transitions to the next state.
*/
public class AnimationState extends ClientState {
/**
* Progress of the current animation, ranging from 0 to 1.
*/
private float animationProgress = 0;
/**
* Duration of the animation in seconds.
*/
private final static float ANIMATION_DURATION = 0.375f;
/**
* Speed of the shell in the animation.
*/
private final static float SHELL_SPEED = 0.3f;
/**
* The effect message received from the server.
*/
private final EffectMessage msg;
/**
* The shell involved in the animation.
*/
private final Shell shell;
/**
* Constructs an AnimationState with the specified game logic, effect message, and shell.
*
* @param logic the game logic associated with this state
* @param msg the effect message received from the server
* @param shell the shell involved in the animation
*/
public AnimationState(ClientGameLogic logic, EffectMessage msg, Shell shell) {
super(logic);
this.msg = msg;
this.shell = shell;
}
/**
* Ends the animation state and transitions to the next state:<br>
* - Plays the appropriate sound.<br>
* - Updates the affected map.<br>
* - Adds destroyed ships to the opponent's map.<br>
* - Sends an `AnimationMessage` to the server.<br>
* - If the game is over, transitions to `GameOverState` and plays music.<br>
* - Otherwise, transitions to `BattleState`.
*/
public void endState() {
playSound(msg);
affectedMap(msg).add(msg.getShot());
affectedMap(msg).remove(shell);
if (destroyedOpponentShip(msg))
logic.getOpponentMap().add(msg.getDestroyedShip());
logic.send(new AnimationMessage());
if (msg.isGameOver()) {
for (Battleship ship : msg.getRemainingOpponentShips()) {
logic.getOpponentMap().add(ship);
}
logic.setState(new GameOverState(logic));
if (msg.isOwnShot())
logic.playMusic(Music.VICTORY_MUSIC);
else
logic.playMusic(Music.DEFEAT_MUSIC);
} else {
logic.setState(new BattleState(logic, msg.isMyTurn()));
}
}
/**
* Checks if the battle state should be shown.
*
* @return true if the battle state should be shown, false otherwise
*/
@Override
public boolean showBattle() {
return true;
}
/**
* Determines which map (own or opponent's) should be affected by the shot based on the message.
*
* @param msg the effect message received from the server
* @return the map (either the opponent's or player's own map) that is affected by the shot
*/
private ShipMap affectedMap(EffectMessage msg) {
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap();
}
/**
* Checks if the opponent's ship was destroyed by the player's shot.
*
* @param msg the effect message received from the server
* @return true if the shot destroyed an opponent's ship, false otherwise
*/
private boolean destroyedOpponentShip(EffectMessage msg) {
return msg.getDestroyedShip() != null && msg.isOwnShot();
}
/**
* Plays a sound based on the outcome of the shot. Different sounds are played for a miss, hit,
* or destruction of a ship.
*
* @param msg the effect message containing the result of the shot
*/
private void playSound(EffectMessage msg) {
if (!msg.getShot().isHit())
logic.playSound(Sound.SPLASH);
else if (msg.getDestroyedShip() == null)
logic.playSound(Sound.EXPLOSION);
else
logic.playSound(Sound.DESTROYED_SHIP);
}
/**
* Handles a click on the opponent's map.
*
* @param pos the position where the click occurred
*/
@Override
public void clickOpponentMap(IntPoint pos) {
if (!msg.isMyTurn())
logic.setInfoText("wait.its.not.your.turn");
}
/**
* Updates the state of the animation. This method increments the animationProgress value
* until it exceeds a threshold, at which point the state ends.
*
* @param delta the time elapsed since the last update, in seconds
*/
@Override
public void update(float delta) {
if (animationProgress > ANIMATION_DURATION) {
endState();
} else {
animationProgress += delta * SHELL_SPEED;
}
}
}

View File

@@ -7,7 +7,6 @@
package pp.battleship.game.client; package pp.battleship.game.client;
import com.jme3.math.Vector3f;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
@@ -15,8 +14,6 @@
import pp.battleship.model.ShipMap; import pp.battleship.model.ShipMap;
import pp.battleship.notification.Sound; import pp.battleship.notification.Sound;
import java.lang.System.Logger.Level;
/** /**
* Represents the state of the client where players take turns to attack each other's ships. * Represents the state of the client where players take turns to attack each other's ships.
*/ */
@@ -35,8 +32,9 @@ public BattleState(ClientGameLogic logic, boolean myTurn) {
} }
/** /**
* displays the battle scene. * Checks if the battle state should be shown.
* @return true to show the battle *
* @return true if the battle state should be shown, false otherwise
*/ */
@Override @Override
public boolean showBattle() { public boolean showBattle() {
@@ -44,8 +42,9 @@ public boolean showBattle() {
} }
/** /**
* Handles clicking on the opponent's map. If it's the player's turn and the position is valid, a ShootMessage is sent. * Handles a click on the opponent's map.
* @param pos the clicked position on the opponent's map *
* @param pos the position where the click occurred
*/ */
@Override @Override
public void clickOpponentMap(IntPoint pos) { public void clickOpponentMap(IntPoint pos) {
@@ -62,18 +61,16 @@ else if (logic.getOpponentMap().isValid(pos))
*/ */
@Override @Override
public void receivedEffect(EffectMessage msg) { public void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS ClientGameLogic.LOGGER.log(System.Logger.Level.INFO, "report effect: {0}", msg); //NON-NLS
// Update turn and info text
myTurn = msg.isMyTurn(); myTurn = msg.isMyTurn();
logic.playSound(Sound.MISSILE_LAUNCH);
logic.setInfoText(msg.getInfoTextKey()); logic.setInfoText(msg.getInfoTextKey());
affectedMap(msg).add(msg.getShot()); // Add the shell to the affected map
affectedMap(msg).add(new Shell(new Vector3f(0, 50, 0), new Vector3f(msg.getShot().getY() + 0.5f, -0.4f, msg.getShot().getX() + 0.5f), 1f,msg,logic)); Shell shell = new Shell(msg.getShot());
if (destroyedOpponentShip(msg)) affectedMap(msg).add(shell);
logic.getOpponentMap().add(msg.getDestroyedShip()); // Change state to AnimationState
if (msg.isGameOver()) { logic.playSound(Sound.SHELL_FIRED);
msg.getRemainingOpponentShips().forEach(logic.getOpponentMap()::add); logic.setState(new AnimationState(logic, msg, shell));
logic.setState(new GameOverState(logic));
}
} }
/** /**
@@ -85,14 +82,4 @@ public void receivedEffect(EffectMessage msg) {
private ShipMap affectedMap(EffectMessage msg) { private ShipMap affectedMap(EffectMessage msg) {
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap(); return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap();
} }
/**
* Checks if the opponent's ship was destroyed by the player's shot.
*
* @param msg the effect message received from the server
* @return true if the shot destroyed an opponent's ship, false otherwise
*/
private boolean destroyedOpponentShip(EffectMessage msg) {
return msg.getDestroyedShip() != null && msg.isOwnShot();
}
} }

View File

@@ -15,13 +15,7 @@
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap; import pp.battleship.model.ShipMap;
import pp.battleship.model.dto.ShipMapDTO; import pp.battleship.model.dto.ShipMapDTO;
import pp.battleship.notification.ClientStateEvent; import pp.battleship.notification.*;
import pp.battleship.notification.GameEvent;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.battleship.notification.Sound;
import pp.battleship.notification.SoundEvent;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -258,6 +252,15 @@ public void playSound(Sound sound) {
notifyListeners(new SoundEvent(sound)); notifyListeners(new SoundEvent(sound));
} }
/**
* Emits an event to play the specified music.
*
* @param music the music to be played.
*/
public void playMusic(Music music) {
notifyListeners(new MusicEvent(music));
}
/** /**
* Loads a map from the specified file. * Loads a map from the specified file.
* *

View File

@@ -227,7 +227,7 @@ private ShipMap harbor() {
} }
/** /**
* Loads a map from the specified file. Also Checks if the map is valid * Loads a map from the specified file.
* *
* @param file the file to load the map from * @param file the file to load the map from
* @throws IOException if the map cannot be loaded * @throws IOException if the map cannot be loaded
@@ -237,9 +237,8 @@ public void loadMap(File file) throws IOException {
final ShipMapDTO dto = ShipMapDTO.loadFrom(file); final ShipMapDTO dto = ShipMapDTO.loadFrom(file);
if (!dto.fits(logic.getDetails())) if (!dto.fits(logic.getDetails()))
throw new IOException(lookup("map.doesnt.fit")); throw new IOException(lookup("map.doesnt.fit"));
if(!validMap(dto)){ if (!validMap(dto))
throw new IOException(lookup("map.is.invalid")); throw new IOException(lookup("map.invalid"));
}
ownMap().clear(); ownMap().clear();
dto.getShips().forEach(ownMap()::add); dto.getShips().forEach(ownMap()::add);
harbor().clear(); harbor().clear();
@@ -268,49 +267,68 @@ public boolean maySaveMap() {
} }
/** /**
* Checks if the map is valid in terms of overlapping and if the ship is within the boundries of the map * Validates the given ShipMapDTO by checking if all ships are within bounds
* @param dto DataTransferObject is the loaded json file * and do not overlap with each other.
* @return returns true if the map is valid, false otherwhise *
* @param dto the ShipMapDTO to validate
* @return true if the map is valid, false otherwise
*/ */
private boolean validMap(ShipMapDTO dto) { private boolean validMap(ShipMapDTO dto) {
return inBoundsClient(dto) && overLapClient(dto); return inBounds(dto) && !overlaps(dto);
} }
/** /**
* Checks if the Ships overlap on the map * Checks if all ships in the given ShipMapDTO are within the bounds of the map.
* @param dto DataTransferObject is the loaded json file *
* @return returns true if the ships arent overlapping, false otherwhise * @param dto the ShipMapDTO to validate
* @return true if all ships are within bounds, false otherwise
*/ */
private boolean overLapClient(ShipMapDTO dto) { private boolean inBounds(ShipMapDTO dto) {
List<Battleship> battleshipList = dto.getShips(); List<Battleship> ships = dto.getShips();
for (Battleship ship : ships) {
if (!isWithinBounds(ship, dto.getWidth(), dto.getHeight())) {
return false;
}
}
return true;
}
for (int i = 0; i < battleshipList.size(); i++) { /**
Battleship ship1 = battleshipList.get(i); * Checks if the given ship is within the bounds of the map.
for (int j = i + 1; j < battleshipList.size(); j++) { *
Battleship ship2 = battleshipList.get(j); * @param ship the Battleship to check
* @param width the width of the map
* @param height the height of the map
* @return true if the ship is within bounds, false otherwise
*/
private boolean isWithinBounds(Battleship ship, int width, int height) {
int minX = ship.getMinX();
int maxX = ship.getMaxX();
int minY = ship.getMinY();
int maxY = ship.getMaxY();
return minX >= 0 && minX < width &&
minY >= 0 && minY < height &&
maxX >= 0 && maxX < width &&
maxY >= 0 && maxY < height;
}
/**
* Checks if any ships in the given ShipMapDTO overlap with each other.
*
* @param dto the ShipMapDTO to validate
* @return true if any ships overlap, false otherwise
*/
private boolean overlaps(ShipMapDTO dto) {
List<Battleship> ships = dto.getShips();
for (int i = 0; i < ships.size(); i++) {
Battleship ship1 = ships.get(i);
for (int j = i + 1; j < ships.size(); j++) {
Battleship ship2 = ships.get(j);
if (ship1.collidesWith(ship2)) { if (ship1.collidesWith(ship2)) {
return true; // Collision detected
}
}
}
return false; return false;
} }
}
}
return true;
}
/**
* Checks if the Ship is in the map Boundries
* @param dto DataTransferObject is the loaded json file
* @return true if the ship is in the maps boundriess, false otherwhise
*/
private boolean inBoundsClient(ShipMapDTO dto) {
int widht = dto.getWidth();
int height = dto.getHeight();
for (int i = 0; i < dto.getShips().size(); i++) {
Battleship localShip = dto.getShips().get(i);
if (!(localShip.getMaxX() < widht && localShip.getMaxY() < height && localShip.getMinY() >= 0 && localShip.getMinX() >= 0)) {
return false;
}
}
return true;
}
} }

View File

@@ -7,7 +7,9 @@
package pp.battleship.game.client; package pp.battleship.game.client;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage; import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.notification.Music;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
@@ -37,5 +39,19 @@ public void receivedStartBattle(StartBattleMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "start battle, {0} turn", msg.isMyTurn() ? "my" : "other's"); //NON-NLS ClientGameLogic.LOGGER.log(Level.INFO, "start battle, {0} turn", msg.isMyTurn() ? "my" : "other's"); //NON-NLS
logic.setInfoText(msg.getInfoTextKey()); logic.setInfoText(msg.getInfoTextKey());
logic.setState(new BattleState(logic, msg.isMyTurn())); logic.setState(new BattleState(logic, msg.isMyTurn()));
logic.playMusic(Music.GAME_MUSIC);
}
/**
* Handles the GameDetails message received from the server.
* If the map is invalid, the editor state is set.
*
* @param msg the GameDetails message received
*/
@Override
public void receivedGameDetails(GameDetails msg) {
ClientGameLogic.LOGGER.log(Level.WARNING, "Invalid Map"); //NON-NLS
logic.setInfoText("map.invalid");
logic.setState(new EditorState(logic));
} }
} }

View File

@@ -8,6 +8,7 @@
package pp.battleship.game.server; package pp.battleship.game.server;
import pp.battleship.BattleshipConfig; import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
@@ -35,6 +36,7 @@ public class ServerGameLogic implements ClientInterpreter {
private final BattleshipConfig config; private final BattleshipConfig config;
private final List<Player> players = new ArrayList<>(2); private final List<Player> players = new ArrayList<>(2);
private final Set<Player> readyPlayers = new HashSet<>(); private final Set<Player> readyPlayers = new HashSet<>();
private final Set<Player> finishedAnimation = new HashSet<>();
private final ServerSender serverSender; private final ServerSender serverSender;
private Player activePlayer; private Player activePlayer;
private ServerState state = ServerState.WAIT; private ServerState state = ServerState.WAIT;
@@ -54,7 +56,6 @@ public ServerGameLogic(ServerSender serverSender, BattleshipConfig config) {
* Returns the state of the game. * Returns the state of the game.
*/ */
ServerState getState() { ServerState getState() {
return state; return state;
} }
@@ -134,7 +135,7 @@ public Player addPlayer(int id) {
} }
/** /**
* Handles the reception of a MapMessage. Also Checks if the given map is valid or invalid * Handles the reception of a MapMessage.
* *
* @param msg the received MapMessage * @param msg the received MapMessage
* @param from the ID of the sender client * @param from the ID of the sender client
@@ -143,64 +144,78 @@ public Player addPlayer(int id) {
public void received(MapMessage msg, int from) { public void received(MapMessage msg, int from) {
if (state != ServerState.SET_UP) if (state != ServerState.SET_UP)
LOGGER.log(Level.ERROR, "playerReady not allowed in {0}", state); //NON-NLS LOGGER.log(Level.ERROR, "playerReady not allowed in {0}", state); //NON-NLS
else if(!validMap(msg,from)){ else if (validMap(msg))
LOGGER.log(Level.ERROR, "Map message not valid",state);
}
else
playerReady(getPlayerById(from), msg.getShips()); playerReady(getPlayerById(from), msg.getShips());
else {
LOGGER.log(Level.ERROR, "map does not fit game details"); //NON-NLS
send(getPlayerById(from), new GameDetails(config));
}
} }
/** /**
* Checks if the map is Valid in terms of boundries and overlaps * Validates the received map message by checking if all ships are within bounds
* * * and do not overlap with each other.
* @param msg the received MapMessage
* @param id the ID of the sender client
* @return returns true if the map is valid, false otherwhise
*/
private boolean validMap(MapMessage msg, int id) {
return inBounds(msg, id) && overLap(msg);
}
/**
* Checks if the Ships overLap
* *
* @param msg the received MapMessage * @param msg the received MapMessage containing the ships
* @return returns true if the ships arent overlapping, false otherwhise * @return true if the map is valid, false otherwise
*/ */
private boolean overLap(MapMessage msg) { private boolean validMap(MapMessage msg) {
List<Battleship> battleshipList = msg.getShips(); List<Battleship> ships = msg.getShips();
return inBounds(ships) && !overlaps(ships);
}
for (int i = 0; i < battleshipList.size(); i++) { /**
Battleship ship1 = battleshipList.get(i); * Checks if all ships in the given list are within the bounds of the map.
for (int j = i + 1; j < battleshipList.size(); j++) { *
Battleship ship2 = battleshipList.get(j); * @param ships the list of Battleships to validate
* @return true if all ships are within bounds, false otherwise
*/
private boolean inBounds(List<Battleship> ships) {
for (Battleship ship : ships) {
if (!isWithinBounds(ship)) {
return false;
}
}
return true;
}
/**
* Checks if the given ship is within the bounds of the map.
*
* @param ship the Battleship to check
* @return true if the ship is within bounds, false otherwise
*/
private boolean isWithinBounds(Battleship ship) {
int minX = ship.getMinX();
int maxX = ship.getMaxX();
int minY = ship.getMinY();
int maxY = ship.getMaxY();
int width = config.getMapWidth();
int height = config.getMapHeight();
return minX >= 0 && minX < width &&
minY >= 0 && minY < height &&
maxX >= 0 && maxX < width &&
maxY >= 0 && maxY < height;
}
/**
* Checks if any ships in the given list overlap with each other.
*
* @param ships the list of Battleships to validate
* @return true if any ships overlap, false otherwise
*/
private boolean overlaps(List<Battleship> ships) {
for (int i = 0; i < ships.size(); i++) {
Battleship ship1 = ships.get(i);
for (int j = i + 1; j < ships.size(); j++) {
Battleship ship2 = ships.get(j);
if (ship1.collidesWith(ship2)) { if (ship1.collidesWith(ship2)) {
return true; // Collision detected
}
}
}
return false; return false;
} }
}
}
return true;
}
/**
* Checks if the Ship is placed in Bounds with the Map
*
* @param msg the received MapMessage
* @param id the ID of the sender client
* @return returns true if the ship is within the maps boundries, false otherwhise
*/
private boolean inBounds(MapMessage msg, int id) {
int widht = getPlayerById(id).getMap().getWidth();
int height = getPlayerById(id).getMap().getHeight();
for (int i = 0; i < msg.getShips().size(); i++) {
Battleship localShip = msg.getShips().get(i);
if (!(localShip.getMaxX() < widht && localShip.getMaxY() < height && localShip.getMinY() >= 0 && localShip.getMinX() >= 0)) {
return false;
}
}
return true;
}
/** /**
* Handles the reception of a ShootMessage. * Handles the reception of a ShootMessage.
@@ -212,9 +227,41 @@ private boolean inBounds(MapMessage msg, int id) {
public void received(ShootMessage msg, int from) { public void received(ShootMessage msg, int from) {
if (state != ServerState.BATTLE) if (state != ServerState.BATTLE)
LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS
else else {
setState(ServerState.ANIMATION);
shoot(getPlayerById(from), msg.getPosition()); shoot(getPlayerById(from), msg.getPosition());
} }
}
/**
* Handles the reception of an {@link AnimationMessage}.
* Marks the player's animation as finished and transitions the game state if necessary.
*
* @param msg the received {@code AnimationMessage}
* @param from the ID of the sender client
*/
@Override
public void received(AnimationMessage msg, int from) {
if (state != ServerState.ANIMATION)
LOGGER.log(Level.ERROR, "animation not allowed in {0}", state); //NON-NLS
else
finishedAnimation(getPlayerById(from));
}
/**
* Marks the player's animation as finished and transitions the game state if necessary.
*
* @param player the player whose animation is finished
*/
private void finishedAnimation(Player player) {
if (!finishedAnimation.add(player)) {
LOGGER.log(Level.ERROR, "{0}'s animation was already finished", player);
}
if (finishedAnimation.size() == 2) {
finishedAnimation.clear();
setState(ServerState.BATTLE);
}
}
/** /**
* Marks the player as ready and sets their ships. * Marks the player as ready and sets their ships.

View File

@@ -26,6 +26,11 @@ enum ServerState {
*/ */
BATTLE, BATTLE,
/**
* The server is waiting for clients to finish their animations.
*/
ANIMATION,
/** /**
* The game has ended because all the ships of one player have been destroyed. * The game has ended because all the ships of one player have been destroyed.
*/ */

View File

@@ -7,10 +7,7 @@
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.*;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
/** /**
@@ -63,6 +60,19 @@ public void received(MapMessage msg, int from) {
copiedMessage = new MapMessage(msg.getShips().stream().map(Copycat::copy).toList()); copiedMessage = new MapMessage(msg.getShips().stream().map(Copycat::copy).toList());
} }
/**
* Handles the reception of a {@link AnimationMessage}.
* Since a {@code AnimationMessage} does not need to be copied, it is directly assigned.
*
* @param msg the received {@code AnimationMessage}
* @param from the identifier of the sender
*/
@Override
public void received(AnimationMessage msg, int from) {
// copying is not necessary
copiedMessage = msg;
}
/** /**
* Creates a copy of the provided {@link Battleship}. * Creates a copy of the provided {@link Battleship}.
* *

View File

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

View File

@@ -0,0 +1,27 @@
package pp.battleship.message.client;
import com.jme3.network.serializing.Serializable;
/**
* A message indicating an animation event is finished in the game. (Client &#8594; Server)
*/
@Serializable
public class AnimationMessage extends ClientMessage {
/**
* Constructs a new AnimationMessage instance.
*/
public AnimationMessage() {
super();
}
/**
* Accepts a visitor for processing this message.
*
* @param interpreter the visitor to be used for processing
* @param from the connection ID of the sender
*/
@Override
public void accept(ClientInterpreter interpreter, int from) {
interpreter.received(this, from);
}
}

View File

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

View File

@@ -1,103 +1,32 @@
package pp.battleship.model; package pp.battleship.model;
import com.jme3.math.Vector3f; /**
import pp.battleship.game.client.ClientGameLogic; * Represents a shell in the Battleship game.
import pp.battleship.message.server.EffectMessage; */
import pp.util.FloatMath;
public class Shell implements Item { public class Shell implements Item {
private final Vector3f startPosition; // Startposition des Geschosses private final int x;
private final Vector3f targetPosition; // Zielposition des Geschosses0 private final int y;
private final Vector3f currentPosition; // Aktuelle Position des Geschosses
private final float speed; // Geschwindigkeit des Geschosses
private final EffectMessage msg;
private final ClientGameLogic logic;
private float progress; public Shell(Shot shot) {
this.x = shot.getX();
/** this.y = shot.getY();
* Initializes a Shell with start position, target position, speed, message, and game logic.
* @param startPosition initial position of the shell
* @param targetPosition target position of the shell
* @param speed movement speed of the shell
* @param msg effect message related to the shell
* @param logic game logic instance
*/
public Shell(Vector3f startPosition, Vector3f targetPosition, float speed, EffectMessage msg, ClientGameLogic logic) {
this.startPosition = startPosition;
this.targetPosition = targetPosition;
this.currentPosition = new Vector3f(startPosition); // Initiale Position ist die Startposition
this.speed = speed;
this.msg = msg;
this.logic = logic;
} }
/** public int getX() {
* Updates the shell's position based on elapsed time, using eased interpolation between start and target positions. return x;
* @param deltaTime time elapsed since last update
*/
public void updatePosition(float deltaTime) {
progress += deltaTime * speed;
progress = FloatMath.clamp(progress, 0.0f, 1.0f);
float t = FloatMath.easeInOutElastic(progress);
// Interpoliere die Position zwischen Start- und Zielposition basierend auf dem Fortschritt
currentPosition.y = FloatMath.extrapolateLinear(t, startPosition.y, targetPosition.y);
currentPosition.x = FloatMath.extrapolateLinear(t, startPosition.x, targetPosition.x);
currentPosition.z = FloatMath.extrapolateLinear(t, startPosition.z, targetPosition.z);
} }
/** public int getY() {
* getter for Current Position return y;
* @return current position
*/
public Vector3f getCurrentPosition() {
return currentPosition;
} }
/**
* @param visitor the visitor performing operations on the item
* @param <T> generic type
* @return shell
*/
@Override @Override
public <T> T accept(Visitor<T> visitor) { public <T> T accept(Visitor<T> visitor) {
return visitor.visit(this); return visitor.visit(this);
} }
/**
* Visitor pattern
* @param visitor the visitor to accept
*/
@Override @Override
public void accept(VoidVisitor visitor) { public void accept(VoidVisitor visitor) {
visitor.visit(this); visitor.visit(this);
} }
/**
* getter for the isFinished boolean
* @return true if progress lesser than 1, false otherwise
*/
public boolean isFinished() {
return progress >= 1;
}
/**
* getter for EffectMessage
* @return EffectMessage
*/
public EffectMessage getMsg() {
return msg;
}
/**
* getter for Client logic
* @return Client logic
*/
public ClientGameLogic getLogic() {
return logic;
}
} }

View File

@@ -78,14 +78,6 @@ public void add(Battleship ship) {
addItem(ship); addItem(ship);
} }
/**
*
* @param shell
*/
public void add(Shell shell){
addItem(shell);
}
/** /**
* Registers a shot on the map, updates the state of the affected ship (if any), * Registers a shot on the map, updates the state of the affected ship (if any),
* and triggers an item addition event. * and triggers an item addition event.
@@ -99,6 +91,15 @@ public void add(Shot shot) {
addItem(shot); addItem(shot);
} }
/**
* Adds a shell to the map and triggers an item addition event.
*
* @param shell the shell to be added to the map
*/
public void add(Shell shell) {
addItem(shell);
}
/** /**
* Removes an item from the map and triggers an item removal event. * Removes an item from the map and triggers an item removal event.
* *

View File

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

View File

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

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