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 594905 additions and 108355 deletions

View File

@@ -9,11 +9,13 @@ implementation project(":jme-common")
implementation project(":battleship:model")
implementation libs.jme3.desktop
implementation libs.jme3.effects
runtimeOnly libs.jme3.awt.dialogs
runtimeOnly libs.jme3.plugins
runtimeOnly libs.jme3.jogg
runtimeOnly libs.jme3.testdata
}
application {

View File

@@ -26,13 +26,10 @@ map.own=maps/map1.json
robot.targets=2, 0,\
2, 1,\
2, 2,\
2, 3,\
2, 4,\
2, 5,\
2, 6
2, 3
#
# Delay in milliseconds between each shot fired by the RobotClient.
robot.delay=4000
robot.delay=2000
#
# 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.

View File

@@ -0,0 +1,267 @@
package pp.battleship.client;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData.DataType;
import com.jme3.audio.AudioNode;
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 static pp.util.PreferencesUtils.getPreferences;
/**
* The BackgroundMusic class represents the background music in the Battleship game application.
* It extends the AbstractAppState class and provides functionalities for playing the menu music,
* game music, victory music, and defeat music.
*/
public class BackgroundMusic extends AbstractAppState implements GameEventListener {
/**
* Logger for the BackgroundMusic class.
*/
private static final Logger LOGGER = System.getLogger(BackgroundMusic.class.getName());
/**
* 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;
/**
* Checks if music is enabled in the preferences.
*
* @return {@code true} if music is enabled, {@code false} otherwise.
*/
public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true);
}
/**
* Sets the enabled state of this AppState.
* Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
*
* @param enabled {@code true} to enable the AppState, {@code false} to disable it.
*/
@Override
public void setEnabled(boolean enabled) {
if (isEnabled() == enabled) return;
super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Music enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
playCurrentMusic();
}
/**
* Initializes the music for the game.
* Overrides {@link AbstractAppState#initialize(AppStateManager, Application)}
*
* @param stateManager The state manager
* @param app The application
*/
@Override
public void initialize(AppStateManager stateManager, Application app) {
LOGGER.log(Level.INFO, "Initializing background music"); //NON-NLS
super.initialize(stateManager, app);
menuMusic = loadMusic(app, MENU_MUSIC_PATH);
gameMusic = loadMusic(app, GAME_MUSIC_PATH);
victoryMusic = loadMusic(app, VICTORY_MUSIC_PATH);
defeatMusic = loadMusic(app, DEFEAT_MUSIC_PATH);
currentMusic = menuMusic;
playCurrentMusic();
}
/**
* 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.
*/
private AudioNode loadMusic(Application app, String name) {
try {
this.volume = PREFERENCES.getFloat(VOLUME_PREF, 0.5f);
final AudioNode music = new AudioNode(app.getAssetManager(), name, DataType.Stream);
music.setLooping(true);
music.setVolume(volume);
music.setPositional(false);
music.setDirectional(false);
return music;
} catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
}
return null;
}
/**
* Plays the current music if the music is enabled.
* 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 {
if (currentMusic != null) {
currentMusic.stop();
}
}
}
/**
* Plays the game music.
*/
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) {
LOGGER.log(Level.INFO, "Setting volume to {0}", volume); //NON-NLS
this.volume = volume;
currentMusic.setVolume(volume);
PREFERENCES.putFloat(VOLUME_PREF, volume);
}
/**
* Returns the volume level for the background music.
*
* @return The volume level as a float.
*/
public float getVolume() {
return volume;
}
}

View File

@@ -122,16 +122,13 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
*/
private final ActionListener escapeListener = (name, isPressed, tpf) -> escape(isPressed);
private EffectHandler effectHandler;
static {
// Configure logging
LogManager manager = LogManager.getLogManager();
try {
manager.readConfiguration(new FileInputStream("logging.properties"));
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
}
catch (IOException e) {
} catch (IOException e) {
LOGGER.log(Level.INFO, e.getMessage());
}
}
@@ -157,7 +154,6 @@ private BattleshipApp() {
logic.addListener(this);
setShowSettings(config.getShowSettings());
setSettings(makeSettings());
effectHandler = null;
}
/**
@@ -227,7 +223,6 @@ public void simpleInitApp() {
setupInput();
setupStates();
setupGui();
effectHandler = new EffectHandler(this);
serverConnection.connect();
}
@@ -269,18 +264,10 @@ private void setupStates() {
flyCam.setEnabled(false);
stateManager.detach(stateManager.getState(StatsAppState.class));
stateManager.detach(stateManager.getState(DebugKeysAppState.class));
attachGameMusic();
attachGameSound();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
}
/**
* Attaches the game sound state and sets its initial enabled state.
*/
private void attachGameMusic() {
final GameMusic gameMusic = new GameMusic();
gameMusic.setEnabled(GameMusic.enabledInPreferences());
stateManager.attach(gameMusic);
attachGameSound();
attachBackgroundSound();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
}
/**
@@ -293,6 +280,19 @@ private void attachGameSound() {
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.
* This method is called once per frame during the game loop.
@@ -418,12 +418,12 @@ public void stop(boolean waitFor) {
*/
void confirmDialog(String question, Runnable yesAction) {
DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.question"))
.setText(question)
.setOkButton(lookup("button.yes"), yesAction)
.setNoButton(lookup("button.no"))
.build()
.open();
.setTitle(lookup("dialog.question"))
.setText(question)
.setOkButton(lookup("button.yes"), yesAction)
.setNoButton(lookup("button.no"))
.build()
.open();
}
/**
@@ -433,14 +433,10 @@ void confirmDialog(String question, Runnable yesAction) {
*/
void errorDialog(String errorMessage) {
DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.error"))
.setText(errorMessage)
.setOkButton(lookup("button.ok"))
.build()
.open();
}
public EffectHandler getEffectHandler() {
return effectHandler;
.setTitle(lookup("dialog.error"))
.setText(errorMessage)
.setOkButton(lookup("button.ok"))
.build()
.open();
}
}

View File

@@ -5,7 +5,7 @@
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.clienthost;
package pp.battleship.client;
import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection;
@@ -18,7 +18,7 @@
import pp.battleship.game.server.Player;
import pp.battleship.game.server.ServerGameLogic;
import pp.battleship.game.server.ServerSender;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
@@ -42,14 +42,41 @@
/**
* Server implementing the visitor pattern as MessageReceiver for ClientMessages
*/
public class BattleshipServerClient implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
private static final Logger LOGGER = System.getLogger(BattleshipServerClient.class.getName());
public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
/**
* Logger for the BattleshipServer class.
*/
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");
/**
* 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;
/**
* Game logic for the server.
*/
private final ServerGameLogic logic;
private final BlockingQueue<ReceivedMessageClient> pendingMessages = new LinkedBlockingQueue<>();
/**
* Queue for pending messages to be processed by the server.
*/
private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();
static {
// Configure logging
@@ -57,53 +84,45 @@ public class BattleshipServerClient implements MessageListener<HostedConnection>
try {
manager.readConfiguration(new FileInputStream("logging.properties"));
LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
}
catch (IOException e) {
} catch (IOException e) {
LOGGER.log(Level.INFO, e.getMessage());
}
}
/**
* Creates the server and reads the configuration from the specified file.
* Initializes the game logic and sets up logging.
* Creates the server.
*/
public BattleshipServerClient() {
public BattleshipServer(int PORT_NUMBER) {
config.readFromIfExists(CONFIG_FILE);
this.PORT_NUMBER = PORT_NUMBER;
LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
logic = new ServerGameLogic(this, config);
}
/**
* Checks if the server is ready.
*
* @return true if the server is running, false otherwise
* Starts the server and processes incoming messages indefinitely.
*/
public boolean isReady() {
return myServer != null && myServer.isRunning();
}
/**
* Starts the server and continuously processes incoming messages.
*/
public void run(int port) {
startServer(port);
public void run() {
startServer();
while (true)
processNextMessage();
}
/**
* Starts the server by creating a network server on the specified port.
* Starts the server and initializes necessary components.
* This method sets up the server, registers serializable classes,
* starts the server, and registers listeners for incoming connections and messages.
* If the server fails to start, it logs an error and exits the application.
*/
private void startServer(int port) {
private void startServer() {
try {
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
myServer = Network.createServer(port);
myServer = Network.createServer(PORT_NUMBER);
initializeSerializables();
myServer.start();
registerListeners();
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
exit(1);
}
@@ -111,55 +130,78 @@ private void startServer(int port) {
/**
* 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() {
try {
pendingMessages.take().process(logic);
}
catch (InterruptedException ex) {
} catch (InterruptedException ex) {
LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS
Thread.currentThread().interrupt();
}
}
/**
* Registers all serializable message classes for network transmission.
* 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() {
Serializer.registerClass(GameDetails.class);
Serializer.registerClass(StartBattleMessage.class);
Serializer.registerClass(MapMessage.class);
Serializer.registerClass(ShootMessage.class);
Serializer.registerClass(AnimationMessage.class);
Serializer.registerClass(EffectMessage.class);
Serializer.registerClass(AnimationFinishedMessage.class);
Serializer.registerClass(Battleship.class);
Serializer.registerClass(IntPoint.class);
Serializer.registerClass(Shot.class);
}
/**
* Registers the 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() {
myServer.addMessageListener(this, MapMessage.class);
myServer.addMessageListener(this, ShootMessage.class);
myServer.addMessageListener(this, AnimationFinishedMessage.class);
myServer.addMessageListener(this, AnimationMessage.class);
myServer.addConnectionListener(this);
}
/**
* Handles the reception of messages from clients.
*
* @param source the connection from which the message was received
* @param message the message received from the client
*/
@Override
public void messageReceived(HostedConnection source, Message message) {
LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS
if (message instanceof ClientMessage clientMessage)
pendingMessages.add(new ReceivedMessageClient(clientMessage, source.getId()));
pendingMessages.add(new ReceivedMessage(clientMessage, source.getId()));
}
/**
* Called when a new connection is added to the server.
*
* @param server the server to which the connection was added
* @param hostedConnection the connection that was added
*/
@Override
public void connectionAdded(Server server, HostedConnection hostedConnection) {
LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS
logic.addPlayer(hostedConnection.getId());
}
/**
* Called when a connection is removed from the server.
*
* @param server the server from which the connection was removed
* @param hostedConnection the connection that was removed
*/
@Override
public void connectionRemoved(Server server, HostedConnection hostedConnection) {
LOGGER.log(Level.INFO, "connection closed: {0}", hostedConnection); //NON-NLS
@@ -173,9 +215,10 @@ public void connectionRemoved(Server server, HostedConnection hostedConnection)
}
/**
* Shuts down the server and closes all active connections.
* Exits the application with the specified exit value.
* Closes all client connections and logs the close request.
*
* @param exitValue the exit code to terminate the program with
* @param exitValue the exit value to be used when exiting the application
*/
private void exit(int exitValue) { //NON-NLS
LOGGER.log(Level.INFO, "close request"); //NON-NLS

View File

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

View File

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

View File

@@ -10,6 +10,9 @@
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;
import com.jme3.audio.AudioNode;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.SoundEvent;
@@ -18,7 +21,6 @@
import java.lang.System.Logger.Level;
import java.util.prefs.Preferences;
import static pp.JmeUtil.loadSound;
import static pp.util.PreferencesUtils.getPreferences;
/**
@@ -32,7 +34,7 @@ public class GameSound extends AbstractAppState implements GameEventListener {
private AudioNode splashSound;
private AudioNode shipDestroyedSound;
private AudioNode explosionSound;
private AudioNode shellFlyingSound;
private AudioNode shellFiredSound;
/**
* Checks if sound is enabled in the preferences.
@@ -77,16 +79,26 @@ public void initialize(AppStateManager stateManager, Application app) {
shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS
splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS
explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS
shellFlyingSound = loadSound(app, "Sound/Effects/shell_flying.wav");
shellFiredSound = loadSound(app, "Sound/Effects/missle.wav"); //NON-NLS
}
/**
* Plays the shell flying sound effect.
* Loads a sound from the specified file.
*
* @param app The application
* @param name The name of the sound file.
* @return The loaded AudioNode.
*/
public void shellFly() {
if (isEnabled() && shellFlyingSound != null) {
shellFlyingSound.playInstance();
private AudioNode loadSound(Application app, String name) {
try {
final AudioNode sound = new AudioNode(app.getAssetManager(), name, AudioData.DataType.Buffer);
sound.setLooping(false);
sound.setPositional(false);
return sound;
} catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
}
return null;
}
/**
@@ -113,13 +125,21 @@ public void shipDestroyed() {
shipDestroyedSound.playInstance();
}
/**
* Plays sound effect when a shell has been fired.
*/
public void shellFired() {
if (isEnabled() && shellFiredSound != null)
shellFiredSound.playInstance();
}
@Override
public void receivedEvent(SoundEvent event) {
switch (event.sound()) {
case EXPLOSION -> explosion();
case SPLASH -> splash();
case DESTROYED_SHIP -> shipDestroyed();
case SHELL_FLYING -> shellFly();
case SHELL_FIRED -> shellFired();
}
}
}

View File

@@ -9,7 +9,10 @@
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.DefaultRangedValueModel;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.Slider;
import com.simsilica.lemur.core.VersionedReference;
import com.simsilica.lemur.style.ElementId;
import pp.dialog.Dialog;
import pp.dialog.StateCheckboxModel;
@@ -19,6 +22,8 @@
import java.io.IOException;
import java.util.prefs.Preferences;
import java.lang.System.Logger;
import static pp.battleship.Resources.lookup;
import static pp.util.PreferencesUtils.getPreferences;
@@ -28,12 +33,13 @@
* returning to the game, and quitting the application.
*/
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 String LAST_PATH = "last.file.path";
private final BattleshipApp app;
private final Button loadButton = new Button(lookup("menu.map.load"));
private final Button saveButton = new Button(lookup("menu.map.save"));
private final VolumeSlider volumeSlider;
private final VersionedReference<Double> volumeRef;
/**
* Constructs the Menu dialog for the Battleship application.
@@ -43,13 +49,19 @@ class Menu extends Dialog {
public Menu(BattleshipApp app) {
super(app.getDialogManager());
this.app = app;
volumeSlider = new VolumeSlider(app.getStateManager().getState(GameMusic.class));
addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS
addChild(new Checkbox(lookup("menu.sound-enabled"),
new StateCheckboxModel(app, GameSound.class)));
addChild(new Checkbox(lookup("menu.music-enabled"), new StateCheckboxModel(app, GameMusic.class)));
new StateCheckboxModel(app, GameSound.class)));
addChild(new Checkbox(lookup("menu.music-toggle"),
new StateCheckboxModel(app, BackgroundMusic.class)));
Slider volumeSlider = new Slider();
volumeSlider.setModel(new DefaultRangedValueModel(0.0, 2.0, app.getStateManager().getState(BackgroundMusic.class).getVolume()));
volumeSlider.setDelta(0.1f);
addChild(volumeSlider);
volumeRef = volumeSlider.getModel().createReference();
addChild(loadButton)
.addClickCommands(s -> ifTopDialog(this::loadDialog));
@@ -71,9 +83,26 @@ public void update() {
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 delta) {
volumeSlider.update();
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);
}
/**
@@ -110,8 +139,7 @@ private void handle(FileAction fileAction, TextInputDialog dialog) {
PREFERENCES.put(LAST_PATH, path);
fileAction.run(new File(path));
dialog.close();
}
catch (IOException e) {
} catch (IOException e) {
app.errorDialog(e.getLocalizedMessage());
}
}
@@ -125,13 +153,13 @@ private void handle(FileAction fileAction, TextInputDialog dialog) {
private void fileDialog(FileAction fileAction, String label) {
final TextInputDialog dialog =
TextInputDialog.builder(app.getDialogManager())
.setLabel(lookup("label.file"))
.setFocus(TextInputDialog::getInput)
.setTitle(label)
.setOkButton(lookup("button.ok"), d -> handle(fileAction, d))
.setNoButton(lookup("button.cancel"))
.setOkClose(false)
.build();
.setLabel(lookup("label.file"))
.setFocus(TextInputDialog::getInput)
.setTitle(label)
.setOkButton(lookup("button.ok"), d -> handle(fileAction, d))
.setNoButton(lookup("button.cancel"))
.setOkClose(false)
.build();
final String path = PREFERENCES.get(LAST_PATH, null);
if (path != null)
dialog.getInput().setText(path.trim());

View File

@@ -12,7 +12,6 @@
import com.simsilica.lemur.Label;
import com.simsilica.lemur.TextField;
import com.simsilica.lemur.component.SpringGridLayout;
import pp.battleship.client.clienthost.BattleshipServerClient;
import pp.dialog.Dialog;
import pp.dialog.DialogBuilder;
import pp.dialog.SimpleDialog;
@@ -32,16 +31,15 @@ class NetworkDialog extends SimpleDialog {
private static final Logger LOGGER = System.getLogger(NetworkDialog.class.getName());
private static final String LOCALHOST = "localhost"; //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 TextField host = new TextField(LOCALHOST);
private final TextField port = new TextField(DEFAULT_PORT);
private String hostname;
private int portNumber;
private Future<Object> connectionFuture;
private Future<Object> serverFuture;
private Dialog progressDialog;
private BattleshipServerClient server;
private final Checkbox clientHostCheckbox;
private boolean hostServer = false;
/**
* Constructs a new NetworkDialog.
@@ -55,22 +53,26 @@ class NetworkDialog extends SimpleDialog {
host.setPreferredWidth(400f);
port.setSingleLine(true);
Checkbox hostCheckbox = new Checkbox(lookup("host.own-server"));
hostCheckbox.setChecked(false);
hostCheckbox.addClickCommands(s -> hostServer = !hostServer);
final BattleshipApp app = network.getApp();
final Container input = new Container(new SpringGridLayout());
input.addChild(new Label(lookup("host.name") + ": "));
input.addChild(host, 1);
input.addChild(new Label(lookup("port.number") + ": "));
input.addChild(port, 1);
clientHostCheckbox = new Checkbox("Host Server");
input.addChild(clientHostCheckbox);
input.addChild(hostCheckbox);
DialogBuilder.simple(app.getDialogManager())
.setTitle(lookup("server.dialog"))
.setExtension(d -> d.addChild(input))
.setOkButton(lookup("button.connect"), d -> connect())
.setNoButton(lookup("button.cancel"), app::closeApp)
.setOkClose(false)
.setNoClose(false)
.build(this);
.setTitle(lookup("server.dialog"))
.setExtension(d -> d.addChild(input))
.setOkButton(lookup("button.connect"), d -> connectHostServer())
.setNoButton(lookup("button.cancel"), app::closeApp)
.setOkClose(false)
.setNoClose(false)
.build(this);
}
/**
@@ -83,31 +85,55 @@ private void connect() {
hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText();
portNumber = Integer.parseInt(port.getText());
openProgressDialog();
if (clientHostCheckbox.isChecked()) {
serverFuture = network.getApp().getExecutor().submit(this::initServer);
while (server == null || !server.isReady()) {
Thread.sleep(100);
}
}
connectionFuture = network.getApp().getExecutor().submit(this::initNetwork);
}
catch (NumberFormatException e) {
} catch (NumberFormatException e) {
network.getApp().errorDialog(lookup("port.must.be.integer"));
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
/**
* 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 connectHostServer() {
if (hostServer) {
startServer();
try {
Thread.sleep(START_SERVER_DELAY);
} catch (Exception e) {
LOGGER.log(Level.ERROR, "Server start failed", e); //NON-NLS
}
connect();
} else {
connect();
}
}
/**
* Starts the game server in a new thread.
* Logs an error if the server fails to start.
*/
private void startServer() {
LOGGER.log(Level.INFO, "start server"); //NON-NLS
new Thread(() -> {
try {
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
BattleshipServer server = new BattleshipServer(Integer.parseInt(port.getText()));
LOGGER.log(Level.INFO, "Server started"); //NON-NLS
server.run();
} catch (Exception e) {
LOGGER.log(Level.ERROR, "Server start failed", e); //NON-NLS
}
}).start();
}
/**
* Creates a dialog indicating that the connection is in progress.
*/
private void openProgressDialog() {
progressDialog = DialogBuilder.simple(network.getApp().getDialogManager())
.setText(lookup("label.connecting"))
.build();
.setText(lookup("label.connecting"))
.build();
progressDialog.open();
}
@@ -120,25 +146,7 @@ private Object initNetwork() {
try {
network.initNetwork(hostname, portNumber);
return null;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Tries to initialize the server hosted by the client.
*
* @throws RuntimeException If an error occurs when starting the server.
*/
private Object initServer() {
try {
server = new BattleshipServerClient();
server.run(Integer.parseInt(port.getText()));
return null;
}
catch (Exception e) {
LOGGER.log(Level.ERROR, "Error while starting server", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@@ -153,27 +161,12 @@ public void update(float delta) {
try {
connectionFuture.get();
success();
}
catch (ExecutionException e) {
} catch (ExecutionException e) {
failure(e.getCause());
}
catch (InterruptedException e) {
} catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS
Thread.currentThread().interrupt();
}
if (serverFuture != null && serverFuture.isDone()) {
try {
serverFuture.get();
}
catch (ExecutionException e) {
LOGGER.log(Level.ERROR, "Failed to start server", e.getCause());
}
catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Server thread was interrupted", e);
Thread.currentThread().interrupt();
}
}
}
/**

View File

@@ -5,12 +5,23 @@
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.clienthost;
package pp.battleship.client;
import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.ClientMessage;
record ReceivedMessageClient(ClientMessage message, int from) {
/**
* 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) {
/**
* Processes the received message using the specified interpreter.
*
* @param interpreter the client interpreter
*/
void process(ClientInterpreter interpreter) {
message.accept(interpreter, from);
}

View File

@@ -1,38 +0,0 @@
package pp.battleship.client;
import com.simsilica.lemur.Slider;
/**
* Represents a volume slider for controlling the background music volume in the Battleship game.
* This class extends the {@link Slider} class and interfaces with the {@link GameMusic} instance
* to adjust the volume settings based on user input.
*/
public class VolumeSlider extends Slider {
private final GameMusic gameMusic;
private float volume;
/**
* Constructs a new VolumeSlider instance and initializes it with the current volume level
* from the game music preferences.
*
* @param gameMusic the instance of {@link GameMusic} to control music volume
*/
public VolumeSlider(GameMusic gameMusic) {
super();
this.gameMusic = gameMusic;
volume = gameMusic.getVolume();
getModel().setPercent(volume);
}
/**
* Updates the volume setting based on the current slider position.
* If the slider's percent value has changed, it updates the music volume
* in the associated {@link GameMusic} instance.
*/
public void update() {
if (getModel().getPercent() != volume) {
this.volume = (float) getModel().getPercent();
gameMusic.setMusicVolume(volume);
}
}
}

View File

@@ -28,7 +28,7 @@
* and interaction between the model and the view.
*/
class MapView {
public static final float FIELD_SIZE = 40f;
private static final float FIELD_SIZE = 40f;
private static final float GRID_LINE_WIDTH = 2f;
private static final float BACKGROUND_DEPTH = -4f;
private static final float GRID_DEPTH = -1f;
@@ -143,6 +143,10 @@ public float getHeight() {
return FIELD_SIZE * map.getHeight();
}
public static float getFieldSize() {
return FIELD_SIZE;
}
/**
* Converts coordinates from view coordinates to model coordinates.
*

View File

@@ -9,7 +9,6 @@
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
@@ -32,7 +31,6 @@ class MapViewSynchronizer extends ShipMapSynchronizer {
private static final float SHIP_LINE_WIDTH = 6f;
private static final float SHOT_DEPTH = -2f;
private static final float SHIP_DEPTH = 0f;
private static final float SHELL_DEPTH = 1f;
private static final float INDENT = 4f;
// Colors used for different visual elements
@@ -73,9 +71,9 @@ public Spatial visit(Shot shot) {
// Create and return a rectangle representing the shot
return view.getApp().getDraw().makeRectangle(p1.getX(), p1.getY(),
SHOT_DEPTH,
p2.getX() - p1.getX(), p2.getY() - p1.getY(),
color);
SHOT_DEPTH,
p2.getX() - p1.getX(), p2.getY() - p1.getY(),
color);
}
/**
@@ -118,22 +116,21 @@ public Spatial visit(Battleship ship) {
}
/**
* Creates and returns a Spatial representation of the given {@code Shell} object
* for 2D visualization in the game. The shell is represented as a circle.
* Creates a visual representation of a shell on the map.
* The shell is represented as a black ellipse.
*
* @param shell The {@code Shell} object to be visualized.
* @return A {@code Spatial} object representing the shell on the map.
* @param shell the Shell object representing the shell in the model
* @return a Spatial representing the shell on the map
*/
@Override
public Spatial visit(Shell shell) {
final ColorRGBA color = ColorRGBA.Black;
Geometry ellipse = new Geometry("ellipse", new Sphere(50, 50, MapView.FIELD_SIZE / 2 * 0.8f));
Geometry ellipse = new Geometry("ellipse", new Sphere(50, 50, MapView.getFieldSize() / 2 * 0.8f));
Material mat = new Material(view.getApp().getAssetManager(), UNSHADED); //NON-NLS
mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mat.setColor("Color", color);
mat.setColor("Color", ColorRGBA.Black);
ellipse.setMaterial(mat);
ellipse.addControl(new Shell2DControl(view, shell));
ellipse.addControl(new ShellMapControl(view, shell));
return ellipse;
}

View File

@@ -7,27 +7,22 @@
package pp.battleship.client.gui;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
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.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Cylinder;
import pp.battleship.client.BattleshipApp;
import pp.battleship.model.Battleship;
import pp.battleship.model.Rotation;
import pp.battleship.model.Shell;
import pp.battleship.model.ShipMap;
import pp.battleship.model.Shot;
import java.util.Timer;
import java.util.TimerTask;
import pp.battleship.model.*;
import static java.util.Objects.requireNonNull;
import static pp.JmeUtil.mapToWorldCord;
import static pp.util.FloatMath.HALF_PI;
import static pp.util.FloatMath.PI;
@@ -39,13 +34,18 @@
*/
class SeaSynchronizer extends ShipMapSynchronizer {
private static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md"; //NON-NLS
private static final String LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
private static final String KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o"; //NON-NLS
private static final String DESTROYER_MODEL = "Models/Destroyer/Destroyer.j3o"; //NON-NLS
private static final String DESTROYER_TEXTURE = "Models/Destroyer/BattleshipC.jpg"; //NON-NLS
private static final String TYPE_II_UBOAT_MODEL = "Models/TypeIIUboat/TypeIIUboat.j3o"; //NON-NLS
private static final String TYPE_II_UBOAT_TEXTURE = "Models/TypeIIUboat/Type_II_U-boat_diff.jpg"; //NON-NLS
private static final String 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 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 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 BattleshipApp app;
@@ -77,10 +77,6 @@ public Spatial visit(Shot shot) {
return shot.isHit() ? handleHit(shot) : handleMiss(shot);
}
private Spatial handleMiss(Shot shot) {
return app.getEffectHandler().waterSplash(mapToWorldCord(shot.getX(), shot.getY()));
}
/**
* Handles a hit by attaching its representation to the node that
* contains the ship model as a child so that it moves with the ship.
@@ -92,44 +88,121 @@ private Spatial handleMiss(Shot shot) {
private Spatial handleHit(Shot shot) {
final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship");
final Node shipNode = requireNonNull((Node) getSpatial(ship), "Missing ship node");
shipNode.getControl(ShipControl.class).hit(shot);
if (ship.isDestroyed()) {
shipNode.getControl(ShipControl.class).destroyed();
new Timer().schedule(new TimerTask() {
@Override
public void run() {
handleShipDestroy(shipNode);
}
}, 10000);
}
final ParticleEmitter debris = createDebrisEffect(shot);
shipNode.attachChild(debris);
final ParticleEmitter fire = createFireEffect(shot, shipNode);
shipNode.attachChild(fire);
return null;
}
private void handleShipDestroy(Node shipNode) {
shipNode.getParent().detachChild(shipNode);
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.
* The appearance of the cylinder depends on whether the shot is a hit or a miss.
* Visits a {@link Shell} and creates a graphical representation of it.
* The shell is represented as a node with a model attached to it.
* The node is then positioned and controlled by a {@link ShellControl}.
*
* @param shot the shot to be represented
* @return the geometry representing the shot
* @param shell the shell to be represented
* @return the node containing the graphical representation of the shell
*/
private Geometry createCylinder(Shot shot) {
final ColorRGBA color = shot.isHit() ? HIT_COLOR : SPLASH_COLOR;
final float height = shot.isHit() ? 1.2f : 0.1f;
@Override
public Spatial visit(Shell shell) {
final Node node = new Node(SHELL);
node.attachChild(createShell());
node.setLocalTranslation(shell.getY() + 0.5f, 10f, shell.getX() + 0.5f);
node.addControl(new ShellControl());
return node;
}
final Cylinder cylinder = new Cylinder(2, 20, 0.45f, height, true);
final Geometry geometry = new Geometry(SHOT, cylinder);
geometry.setMaterial(createColoredMaterial(color));
geometry.rotate(HALF_PI, 0f, 0f);
// compute the center of the shot in world coordinates
geometry.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
return geometry;
/**
* Creates a graphical representation of a shell.
*
* @return the spatial representing the shell
*/
private Spatial createShell() {
final Spatial model = app.getAssetManager().loadModel(ROCKET);
model.scale(0.0025f);
model.rotate(PI, 0f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/**
@@ -148,31 +221,10 @@ public Spatial visit(Battleship ship) {
final float x = 0.5f * (ship.getMinY() + ship.getMaxY() + 1f);
final float z = 0.5f * (ship.getMinX() + ship.getMaxX() + 1f);
node.setLocalTranslation(x, 0f, z);
node.addControl(new ShipControl(ship, node, app.getEffectHandler()));
node.addControl(new ShipControl(ship));
return node;
}
/**
* Creates and returns a 3D model representation of the given {@code Shell} object
* for visualization in the game.
*
* @param shell The {@code Shell} object to be visualized.
* @return A {@code Spatial} object representing the 3D model of the shell.
*/
@Override
public Spatial visit(Shell shell) {
final Spatial model = app.getAssetManager().loadModel("Models/Shell/shell.j3o");
model.setLocalScale(.05f);
model.setShadowMode(ShadowMode.CastAndReceive);
Material mat = new Material(app.getAssetManager(), LIGHTING);
mat.setTexture("DiffuseMap", app.getAssetManager().loadTexture("Models/Shell/shell_color.png"));
mat.setReceivesShadows(true);
model.setMaterial(mat);
model.addControl(new ShellControl(shell));
return model;
}
/**
* Creates the appropriate graphical representation of the specified battleship.
* The representation is either a detailed model or a simple box based on the length of the ship.
@@ -182,10 +234,10 @@ public Spatial visit(Shell shell) {
*/
private Spatial createShip(Battleship ship) {
return switch (ship.getLength()) {
case 1 -> createBattleship(ship, ShipModel.SHIP1);
case 2 -> createBattleship(ship, ShipModel.SHIP2);
case 3 -> createBattleship(ship, ShipModel.SHIP3);
case 4 -> createBattleship(ship, ShipModel.SHIP4);
case 1 -> createVessel(ship);
case 2 -> createSubmarine(ship);
case 3 -> createDestroyer(ship);
case 4 -> createBattleship(ship);
default -> createBox(ship);
};
}
@@ -198,10 +250,10 @@ private Spatial createShip(Battleship ship) {
*/
private Spatial createBox(Battleship ship) {
final Box box = new Box(0.5f * (ship.getMaxY() - ship.getMinY()) + 0.3f,
0.3f,
0.5f * (ship.getMaxX() - ship.getMinX()) + 0.3f);
0.3f,
0.5f * (ship.getMaxX() - ship.getMinX()) + 0.3f);
final Geometry geometry = new Geometry(SHIP, box);
geometry.setMaterial(createColoredMaterial(BOX_COLOR));
geometry.setMaterial(createColoredMaterial());
geometry.setShadowMode(ShadowMode.CastAndReceive);
return geometry;
@@ -213,16 +265,14 @@ private Spatial createBox(Battleship ship) {
* the material's render state is set to use alpha blending, allowing for
* semi-transparent rendering.
*
* @param color the {@link ColorRGBA} to be applied to the material. If the alpha value
* of the color is less than 1, the material will support transparency.
* @return a {@link Material} instance configured with the specified color and,
* if necessary, alpha blending enabled.
*/
private Material createColoredMaterial(ColorRGBA color) {
private Material createColoredMaterial() {
final Material material = new Material(app.getAssetManager(), UNSHADED);
if (color.getAlpha() < 1f)
if (SeaSynchronizer.BOX_COLOR.getAlpha() < 1f)
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
material.setColor(COLOR, color);
material.setColor(COLOR, SeaSynchronizer.BOX_COLOR);
return material;
}
@@ -232,23 +282,75 @@ private Material createColoredMaterial(ColorRGBA color) {
* @param ship the battleship to be represented
* @return the spatial representing the "King George V" battleship
*/
private Spatial createBattleship(Battleship ship, ShipModel shipModel) {
final Spatial model = app.getAssetManager().loadModel(shipModel.getPath());
private Spatial createBattleship(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(KING_GEORGE_V_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(shipModel.getScale());
model.setLocalTranslation(shipModel.getTranslation());
model.scale(1.48f);
model.setShadowMode(ShadowMode.CastAndReceive);
Material mat = new Material(app.getAssetManager(), LIGHTING);
return model;
}
String colorPath = shipModel.getColorPath();
String bumpPath = shipModel.getBumpPath();
if (colorPath != null) mat.setTexture("DiffuseMap", app.getAssetManager().loadTexture(colorPath));
if (bumpPath != null) mat.setTexture("NormalMap", app.getAssetManager().loadTexture(bumpPath));
/**
* Creates a detailed 3D model to represent a destroyer battleship.
*
* @param ship the battleship to be represented
* @return the spatial representing the destroyer battleship
*/
private Spatial createDestroyer(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(DESTROYER_MODEL);
mat.setReceivesShadows(true);
Material mat = new Material(app.getAssetManager(), UNSHADED);
mat.setTexture("ColorMap", app.getAssetManager().loadTexture(DESTROYER_TEXTURE));
mat.getAdditionalRenderState().setBlendMode(BlendMode.Off);
model.setMaterial(mat);
model.setQueueBucket(RenderQueue.Bucket.Opaque);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.1f);
model.setLocalTranslation(0f, 0.25f, 0f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/**
* Creates a detailed 3D model to represent a Type II U-boat submarine.
*
* @param ship the battleship to be represented
* @return the spatial representing the Type II U-boat submarine
*/
private Spatial createSubmarine(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(TYPE_II_UBOAT_MODEL);
Material mat = new Material(app.getAssetManager(), UNSHADED);
mat.setTexture("ColorMap", app.getAssetManager().loadTexture(TYPE_II_UBOAT_TEXTURE));
model.setMaterial(mat);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.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);
return model;
}

View File

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

View File

@@ -1,51 +1,42 @@
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 static pp.JmeUtil.mapToWorldCord;
import static pp.util.FloatMath.PI;
/**
* Controls the 3D representation of a {@code Shell} in the game, updating its position
* and rotation based on the shell's current state in the game model. The {@code ShellControl}
* class ensures that the spatial associated with the shell is positioned and oriented correctly
* within the world.
* Controls the movement and rotation of a shell in the game.
* The shell moves downward at a constant speed and rotates around its Y-axis.
* When the shell reaches a certain Y-coordinate, it is removed from its parent node.
*/
public class ShellControl extends AbstractControl {
private final Shell shell;
private final static float SHELL_SPEED = 7.5f;
private final static float SHELL_ROTATION_SPEED = 0.5f;
private final static float MIN_HEIGHT = 0.7f;
/**
* Constructs a new {@code ShellControl} to manage the 3D visualization of the given {@code Shell}.
* Updates the shell's position and rotation.
* If the shell's Y-coordinate is less than or equal to 1.0, it is detached from its parent node.
*
* @param shell The {@code Shell} being visualized and controlled.
*/
public ShellControl(Shell shell){
super();
this.shell = shell;
}
/**
* Updates the 3D position and rotation of the shell based on its current state.
* Converts map coordinates to world coordinates and applies the shell's orientation.
*
* @param tpf Time per frame, representing the elapsed time since the last update.
* @param tpf time per frame, used to ensure consistent movement speed across different frame rates
*/
@Override
protected void controlUpdate(float tpf) {
Vector3f pos = shell.getPosition();
Vector3f fixed = mapToWorldCord(pos.x, pos.z);
fixed.setY(pos.y);
spatial.setLocalTranslation(fixed);
spatial.setLocalRotation(shell.getRotation());
spatial.rotate(PI/2,0,0);
spatial.move(0, -SHELL_SPEED * tpf, 0);
spatial.rotate(0, SHELL_ROTATION_SPEED, 0);
if (spatial.getLocalTranslation().getY() <= MIN_HEIGHT) {
spatial.getParent().detachChild(spatial);
}
}
/**
* Renders the shell. This method is currently not used.
*
* @param rm the RenderManager
* @param vp the ViewPort
*/
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering-specific behavior required 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

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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: 43 KiB

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

View File

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

View File

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

View File

@@ -6,10 +6,11 @@ newmtl default
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 ship3_color.jpg
map_Kd ship3_color.jpg
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

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

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
public void simpleInitApp() {
export("shell.obj", "shell.j3o"); //NON-NLS
export("Models/KingGeorgeV/King_George_V.obj", "KingGeorgeV.j3o"); //NON-NLS
stop();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

@@ -14,8 +14,6 @@
import pp.battleship.model.ShipMap;
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.
*/
@@ -33,11 +31,21 @@ public BattleState(ClientGameLogic logic, boolean myTurn) {
this.myTurn = myTurn;
}
/**
* 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;
}
/**
* Handles a click on the opponent's map.
*
* @param pos the position where the click occurred
*/
@Override
public void clickOpponentMap(IntPoint pos) {
if (!myTurn)
@@ -53,14 +61,16 @@ else if (logic.getOpponentMap().isValid(pos))
*/
@Override
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();
logic.setInfoText(msg.getInfoTextKey());
// Add the shell to the affected map
Shell shell = new Shell(msg.getShot());
affectedMap(msg).add(shell);
logic.playSound(Sound.SHELL_FLYING);
logic.setState(new ShootingState(logic, shell, myTurn, msg));
// Change state to AnimationState
logic.playSound(Sound.SHELL_FIRED);
logic.setState(new AnimationState(logic, msg, shell));
}
/**

View File

@@ -15,13 +15,7 @@
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.model.dto.ShipMapDTO;
import pp.battleship.notification.ClientStateEvent;
import pp.battleship.notification.GameEvent;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.battleship.notification.Sound;
import pp.battleship.notification.SoundEvent;
import pp.battleship.notification.*;
import java.io.File;
import java.io.IOException;
@@ -258,6 +252,15 @@ public void playSound(Sound 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.
*

View File

@@ -16,6 +16,7 @@
import java.io.File;
import java.io.IOException;
import java.lang.System.Logger.Level;
import java.util.List;
import static pp.battleship.Resources.lookup;
import static pp.battleship.model.Battleship.Status.INVALID_PREVIEW;
@@ -111,8 +112,7 @@ private void placeShip(IntPoint cursor) {
harbor().remove(selectedInHarbor);
preview = null;
selectedInHarbor = null;
}
else {
} else {
preview.setStatus(INVALID_PREVIEW);
ownMap().add(preview);
}
@@ -135,8 +135,7 @@ public void clickHarbor(IntPoint pos) {
harbor().add(selectedInHarbor);
preview = null;
selectedInHarbor = null;
}
else if (shipAtCursor != null) {
} else if (shipAtCursor != null) {
selectedInHarbor = shipAtCursor;
selectedInHarbor.setStatus(VALID_PREVIEW);
harbor().remove(selectedInHarbor);
@@ -238,6 +237,8 @@ public void loadMap(File file) throws IOException {
final ShipMapDTO dto = ShipMapDTO.loadFrom(file);
if (!dto.fits(logic.getDetails()))
throw new IOException(lookup("map.doesnt.fit"));
if (!validMap(dto))
throw new IOException(lookup("map.invalid"));
ownMap().clear();
dto.getShips().forEach(ownMap()::add);
harbor().clear();
@@ -264,4 +265,70 @@ public boolean mayLoadMap() {
public boolean maySaveMap() {
return harbor().getItems().isEmpty();
}
/**
* Validates the given ShipMapDTO by checking if all ships are within bounds
* and do not overlap with each other.
*
* @param dto the ShipMapDTO to validate
* @return true if the map is valid, false otherwise
*/
private boolean validMap(ShipMapDTO dto) {
return inBounds(dto) && !overlaps(dto);
}
/**
* Checks if all ships in the given ShipMapDTO are within the bounds of the map.
*
* @param dto the ShipMapDTO to validate
* @return true if all ships are within bounds, false otherwise
*/
private boolean inBounds(ShipMapDTO dto) {
List<Battleship> ships = dto.getShips();
for (Battleship ship : ships) {
if (!isWithinBounds(ship, dto.getWidth(), dto.getHeight())) {
return false;
}
}
return true;
}
/**
* Checks if the given ship is within the bounds of the map.
*
* @param ship the Battleship to check
* @param width the width of the map
* @param height the height of the map
* @return true if the ship is within bounds, false otherwise
*/
private boolean isWithinBounds(Battleship ship, int width, int height) {
int minX = ship.getMinX();
int maxX = ship.getMaxX();
int minY = ship.getMinY();
int maxY = ship.getMaxY();
return minX >= 0 && minX < width &&
minY >= 0 && minY < height &&
maxX >= 0 && maxX < width &&
maxY >= 0 && maxY < height;
}
/**
* Checks if any ships in the given ShipMapDTO overlap with each other.
*
* @param dto the ShipMapDTO to validate
* @return true if any ships overlap, false otherwise
*/
private boolean overlaps(ShipMapDTO dto) {
List<Battleship> ships = dto.getShips();
for (int i = 0; i < ships.size(); i++) {
Battleship ship1 = ships.get(i);
for (int j = i + 1; j < ships.size(); j++) {
Battleship ship2 = ships.get(j);
if (ship1.collidesWith(ship2)) {
return true; // Collision detected
}
}
}
return false;
}
}

View File

@@ -1,114 +0,0 @@
package pp.battleship.game.client;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.Shell;
import pp.battleship.model.ShipMap;
import pp.battleship.notification.Sound;
/**
* Represents the shooting state of the game where a shell is fired at the opponent.
*/
public class ShootingState extends ClientState {
private float shootValue;
private final static float SHELL_SPEED = 0.3f;
private final Shell shell;
private final boolean myTurn;
private final EffectMessage msg;
/**
* Constructs a shooting state with the specified game logic.
*
* @param logic the game logic
* @param shell the shell being shot
* @param myTurn indicates if it is the player's turn
* @param msg the effect message associated with the shooting action
*/
public ShootingState(ClientGameLogic logic, Shell shell, boolean myTurn, EffectMessage msg) {
super(logic);
this.msg = msg;
this.myTurn = myTurn;
this.shell = shell;
this.shootValue = 0;
shell.move(shootValue);
}
@Override
public boolean showBattle() {
return true;
}
/**
* Updates the shooting state by moving the shell based on the elapsed time.
*
* @param delta the time in seconds since the last update
*/
@Override
void update(float delta) {
super.update(delta);
if (shootValue > 1) {
endState();
}
else {
shootValue += delta * SHELL_SPEED;
shell.move(shootValue);
}
}
/**
* Ends the shooting state and processes the effects of the shot.
*/
private void endState() {
playSound(msg);
affectedMap(msg).add(msg.getShot());
affectedMap(msg).remove(shell);
if (destroyedOpponentShip(msg))
logic.getOpponentMap().add(msg.getDestroyedShip());
if (msg.isGameOver()) {
for (Battleship ship : msg.getRemainingOpponentShips()) {
logic.getOpponentMap().add(ship);
}
logic.setState(new GameOverState(logic));
return;
}
logic.send(new AnimationFinishedMessage());
logic.setState(new BattleState(logic, myTurn));
}
/**
* Checks if an opponent's ship was destroyed by the shot.
*
* @param msg the effect message containing the shot details
* @return true if an opponent's ship was destroyed, false otherwise
*/
private boolean destroyedOpponentShip(EffectMessage msg) {
return msg.getDestroyedShip() != null && msg.isOwnShot();
}
/**
* Retrieves the affected map based on whether the shot was owned by the player or the opponent.
*
* @param msg the effect message containing shot details
* @return the ShipMap that was affected by the shot
*/
private ShipMap affectedMap(EffectMessage msg) {
return msg.isOwnShot() ? logic.getOpponentMap() : logic.getOwnMap();
}
/**
* Plays a sound based on the outcome of the shot. Different sounds are played for a miss, hit,
* or destruction of a ship.
*
* @param msg the effect message containing the result of the shot
*/
private void playSound(EffectMessage msg) {
if (!msg.getShot().isHit())
logic.playSound(Sound.SPLASH);
else if (msg.getDestroyedShip() == null)
logic.playSound(Sound.EXPLOSION);
else
logic.playSound(Sound.DESTROYED_SHIP);
}
}

View File

@@ -9,6 +9,7 @@
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.notification.Music;
import java.lang.System.Logger.Level;
@@ -38,12 +39,19 @@ public void receivedStartBattle(StartBattleMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "start battle, {0} turn", msg.isMyTurn() ? "my" : "other's"); //NON-NLS
logic.setInfoText(msg.getInfoTextKey());
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 details) {
public void receivedGameDetails(GameDetails msg) {
ClientGameLogic.LOGGER.log(Level.WARNING, "Invalid Map"); //NON-NLS
logic.setInfoText("invalid.map");
logic.setInfoText("map.invalid");
logic.setState(new EditorState(logic));
}
}

View File

@@ -8,7 +8,7 @@
package pp.battleship.game.server;
import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
@@ -36,10 +36,10 @@ public class ServerGameLogic implements ClientInterpreter {
private final BattleshipConfig config;
private final List<Player> players = new ArrayList<>(2);
private final Set<Player> readyPlayers = new HashSet<>();
private final Set<Player> finishedAnimation = new HashSet<>();
private final ServerSender serverSender;
private Player activePlayer;
private ServerState state = ServerState.WAIT;
private Set<Player> waitPlayers = new HashSet<>();
/**
* Constructs a ServerGameLogic with the specified sender and configuration.
@@ -144,75 +144,79 @@ public Player addPlayer(int id) {
public void received(MapMessage msg, int from) {
if (state != ServerState.SET_UP)
LOGGER.log(Level.ERROR, "playerReady not allowed in {0}", state); //NON-NLS
else if (validMap(msg))
playerReady(getPlayerById(from), msg.getShips());
else {
if (checkMap(msg.getShips())) {
playerReady(getPlayerById(from), msg.getShips());
}
else {
LOGGER.log(Level.WARNING, "Invalid Map sent from player {0}", from); //NON-NLS
send(players.get(from), new GameDetails(config));
}
LOGGER.log(Level.ERROR, "map does not fit game details"); //NON-NLS
send(getPlayerById(from), new GameDetails(config));
}
}
/**
* Handles the reception of a AnimationFinishedMessage.
* 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 from the ID of the sender client
* @param msg the received MapMessage containing the ships
* @return true if the map is valid, false otherwise
*/
@Override
public void received(AnimationFinishedMessage msg, int from) {
if (state != ServerState.ANIMATION) {
LOGGER.log(Level.ERROR, "animation finished not allowed in {0}", state);
}
else {
LOGGER.log(Level.DEBUG, "anim received from {0}", getPlayerById(from));
Player player = getPlayerById(from);
if (!waitPlayers.add(player)) {
LOGGER.log(Level.ERROR, "{0} already sent animation finished", player); //NON-NLS
return;
}
if (waitPlayers.size() == 2) {
waitPlayers = new HashSet<>();
setState(ServerState.BATTLE);
}
}
private boolean validMap(MapMessage msg) {
List<Battleship> ships = msg.getShips();
return inBounds(ships) && !overlaps(ships);
}
/**
* Validates the placement of battleships on the map.
* Ensures that:
* <ul>
* <li>The number of ships matches the configuration.</li>
* <li>Ships are within the map's boundaries.</li>
* <li>Ships do not overlap.</li>
* </ul>
* Checks if all ships in the given list are within the bounds of the map.
*
* @param ships the list of {@link Battleship} objects to validate
* @return {@code true} if all ships are placed correctly; {@code false} otherwise
* @param ships the list of Battleships to validate
* @return true if all ships are within bounds, false otherwise
*/
private boolean checkMap(List<Battleship> ships) {
int numShips = config.getShipNums().values().stream().mapToInt(Integer::intValue).sum();
if (numShips != ships.size()) return false;
List<IntPoint> occupied = new ArrayList<>();
for (Battleship battleship : ships) {
int x = battleship.getX();
int y = battleship.getY();
for (int i = 0; i < battleship.getLength(); i++) {
if (x >= 0 && x < config.getMapWidth() && y >= 0 && y < config.getMapHeight() && !occupied.contains(new IntPoint(x, y))) {
occupied.add(new IntPoint(x, y));
x += battleship.getRot().dx();
y += battleship.getRot().dy();
}
else return false;
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)) {
return true; // Collision detected
}
}
}
return false;
}
/**
* Handles the reception of a ShootMessage.
*
@@ -223,11 +227,40 @@ private boolean checkMap(List<Battleship> ships) {
public void received(ShootMessage msg, int from) {
if (state != ServerState.BATTLE)
LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS
else{
else {
setState(ServerState.ANIMATION);
shoot(getPlayerById(from), msg.getPosition());
}
}
/**
* Handles the reception of an {@link AnimationMessage}.
* Marks the player's animation as finished and transitions the game state if necessary.
*
* @param msg the received {@code AnimationMessage}
* @param from the ID of the sender client
*/
@Override
public void received(AnimationMessage msg, int from) {
if (state != ServerState.ANIMATION)
LOGGER.log(Level.ERROR, "animation not allowed in {0}", state); //NON-NLS
else
finishedAnimation(getPlayerById(from));
}
/**
* Marks the player's animation as finished and transitions the game state if necessary.
*
* @param player the player whose animation is finished
*/
private void finishedAnimation(Player player) {
if (!finishedAnimation.add(player)) {
LOGGER.log(Level.ERROR, "{0}'s animation was already finished", player);
}
if (finishedAnimation.size() == 2) {
finishedAnimation.clear();
setState(ServerState.BATTLE);
}
}
/**
@@ -265,8 +298,7 @@ void shoot(Player p, IntPoint pos) {
send(activePlayer, EffectMessage.miss(true, pos));
send(otherPlayer, EffectMessage.miss(false, pos));
activePlayer = otherPlayer;
}
else {
} else {
// shot hit a ship
selectedShip.hit(pos);
if (otherPlayer.getMap().getRemainingShips().isEmpty()) {
@@ -274,14 +306,11 @@ void shoot(Player p, IntPoint pos) {
send(activePlayer, EffectMessage.won(pos, selectedShip));
send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips()));
setState(ServerState.GAME_OVER);
return;
}
else if (selectedShip.isDestroyed()) {
} else if (selectedShip.isDestroyed()) {
// ship has been destroyed, but game is not yet over
send(activePlayer, EffectMessage.shipDestroyed(true, pos, selectedShip));
send(otherPlayer, EffectMessage.shipDestroyed(false, pos, selectedShip));
}
else {
} else {
// ship has been hit, but it hasn't been destroyed
send(activePlayer, EffectMessage.hit(true, pos));
send(otherPlayer, EffectMessage.hit(false, pos));

View File

@@ -27,7 +27,7 @@ enum ServerState {
BATTLE,
/**
* The server is waiting for all clients to finish the shoot animation.
* The server is waiting for clients to finish their animations.
*/
ANIMATION,

View File

@@ -7,11 +7,7 @@
package pp.battleship.game.singlemode;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.client.*;
import pp.battleship.model.Battleship;
/**
@@ -64,8 +60,16 @@ public void received(MapMessage msg, int from) {
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(AnimationFinishedMessage msg, int from) {
public void received(AnimationMessage msg, int from) {
// copying is not necessary
copiedMessage = msg;
}

View File

@@ -1,7 +1,7 @@
package pp.battleship.game.singlemode;
import pp.battleship.game.client.BattleshipClient;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.client.AnimationMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
@@ -72,7 +72,6 @@ public void run() {
* Makes the RobotClient take a shot by sending a ShootMessage with the target position.
*/
private void robotShot() {
connection.sendRobotMessage(new ShootMessage(getShotPosition()));
}
@@ -123,7 +122,7 @@ public void received(StartBattleMessage msg) {
@Override
public void received(EffectMessage msg) {
LOGGER.log(Level.INFO, "Received EffectMessage: {0}", msg); //NON-NLS
connection.sendRobotMessage(new AnimationFinishedMessage());
connection.sendRobotMessage(new AnimationMessage());
if (msg.isMyTurn())
shoot();
}

View File

@@ -1,24 +0,0 @@
package pp.battleship.message.client;
import com.jme3.network.serializing.Serializable;
/**
* Represents a message indicating that an animation has finished on the client side.
*/
@Serializable
public class AnimationFinishedMessage extends ClientMessage {
public AnimationFinishedMessage() {
super();
}
/**
* Accepts a visitor to process this message.
*
* @param interpreter the visitor to process this message
* @param from the connection ID from which the message was received
*/
@Override
public void accept(ClientInterpreter interpreter, int from) {
interpreter.received(this, from);
}
}

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

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

View File

@@ -1,116 +1,23 @@
package pp.battleship.model;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
/**
* The {@code Shell} class represents a projectile fired by a ship in the Battleship game.
* It models the position and rotation of the projectile and allows for its movement along
* a Bezier curve.
* Represents a shell in the Battleship game.
*/
public class Shell implements Item {
/**
* Initial position of the shell
*/
private final static Vector3f INIT_POS = new Vector3f(-3, 7, -3);
/**
* The overshot difference vector used to get a shallower flight path
*/
private final static Vector3f OVER_SHOT_DIFF = new Vector3f(-1, -1, -1);
// Target shot position
private final Vector3f shotPosition;
// Position on top of shotPosition used for Bezier curve
private final Vector3f overShotPosition;
// Current position of the shell
private Vector3f position;
// Current rotation of the shell
private final Quaternion rotation;
private final int x;
private final int y;
/**
* Constructs a new {@code Shell} object using the given {@code Shot} target.
* The initial position, target position, and overshot position are calculated.
*
* @param shot The target {@code Shot} object containing the destination coordinates.
*/
public Shell(Shot shot) {
this.shotPosition = new Vector3f(shot.getX(), 0, shot.getY());
this.overShotPosition = new Vector3f(shotPosition.x, INIT_POS.y, shotPosition.z).add(OVER_SHOT_DIFF);
this.position = INIT_POS;
this.rotation = new Quaternion();
this.x = shot.getX();
this.y = shot.getY();
}
/**
* Gets the current position of the shell.
*
* @return The current position as a {@code Vector3f}.
*/
public Vector3f getPosition() {
return this.position;
public int getX() {
return x;
}
/**
* Gets the current rotation of the shell.
*
* @return The current rotation as a {@code Quaternion}.
*/
public Quaternion getRotation() {
return this.rotation;
}
/**
* Moves the shell along a Bezier curve based on the given time factor {@code t}.
* The position and rotation of the shell are updated.
*
* @param t The time factor between 0 and 1, representing the progress of the shell's flight.
*/
public void move(float t) {
if (t > 1f) t = 1f;
Vector3f newPosition = bezInt(INIT_POS, overShotPosition, shotPosition, t);
updateRotation(position, newPosition);
this.position = newPosition;
}
/**
* Performs a quadratic Bezier interpolation between three points based on the time factor {@code t}.
*
* @param p1 The start position.
* @param p2 The overshot position.
* @param p3 The target position.
* @param t The time factor for interpolation.
* @return The interpolated position as a {@code Vector3f}.
*/
private Vector3f bezInt(Vector3f p1, Vector3f p2, Vector3f p3, float t) {
Vector3f inA = linInt(p1, p2, t);
Vector3f inB = linInt(p2, p3, t);
return linInt(inA, inB, t);
}
/**
* Performs linear interpolation between two points {@code p1} and {@code p2} based on the time factor {@code t}.
*
* @param p1 The start position.
* @param p2 The end position.
* @param t The time factor for interpolation.
* @return The interpolated position as a {@code Vector3f}.
*/
private Vector3f linInt(Vector3f p1, Vector3f p2, float t) {
float x = p1.getX() + t * (p2.getX() - p1.getX());
float y = p1.getY() + t * (p2.getY() - p1.getY());
float z = p1.getZ() + t * (p2.getZ() - p1.getZ());
return new Vector3f(x, y, z);
}
/**
* Updates the rotation of the shell to face the new position along its flight path.
*
* @param oldPos The previous position of the shell.
* @param newPos The new position of the shell.
*/
private void updateRotation(Vector3f oldPos, Vector3f newPos) {
Vector3f direction = newPos.subtract(oldPos).normalize();
if (direction.lengthSquared() > 0) {
this.rotation.lookAt(direction, Vector3f.UNIT_Y);
}
public int getY() {
return y;
}
@Override

View File

@@ -92,9 +92,9 @@ public void add(Shot shot) {
}
/**
* Registers a shot on the map and triggers an item addition event.
* Adds a shell to the map and triggers an item addition event.
*
* @param shell the shell to be registered on the map
* @param shell the shell to be added to the map
*/
public void add(Shell shell) {
addItem(shell);
@@ -190,8 +190,8 @@ public int getHeight() {
*/
public boolean isValid(Battleship ship) {
return isValid(ship.getMinX(), ship.getMinY()) &&
isValid(ship.getMaxX(), ship.getMaxY()) &&
getShips().filter(s -> s != ship).noneMatch(ship::collidesWith);
isValid(ship.getMaxX(), ship.getMaxY()) &&
getShips().filter(s -> s != ship).noneMatch(ship::collidesWith);
}
/**
@@ -203,8 +203,8 @@ public boolean isValid(Battleship ship) {
*/
public Battleship findShipAt(int x, int y) {
return getShips().filter(ship -> ship.contains(x, y))
.findAny()
.orElse(null);
.findAny()
.orElse(null);
}
/**
@@ -236,7 +236,7 @@ public boolean isValid(IntPosition pos) {
*/
public boolean isValid(int x, int y) {
return x >= 0 && x < width &&
y >= 0 && y < height;
y >= 0 && y < height;
}
/**

View File

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

View File

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

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