+ * Note: Attributes of this class should not be marked as {@code final} + * to ensure proper functionality when reading from a properties file. + *
+ */ +public class BattleshipAppConfig extends BattleshipClientConfig { + + /** + * Converts a string value found in the properties file into an object of the specified type. + * Extends the superclass method to support conversion to {@link ColorRGBA}. + * + * @param value the string value to be converted + * @param targetType the target type into which the value string is converted + * @return the converted object of the specified type + */ + @Override + protected Object convertToType(String value, Class> targetType) { + if (targetType == ColorRGBA.class) + return makeColorRGBA(value); + return super.convertToType(value, targetType); + } + + /** + * Converts the specified string value to a corresponding {@link ColorRGBA} object. + * + * @param value the color in the format "red, green, blue, alpha" with all values in the range [0..1] + * @return a {@link ColorRGBA} object representing the color + * @throws IllegalArgumentException if the input string is not in the expected format + */ + private static ColorRGBA makeColorRGBA(String value) { + String[] split = value.split(",", -1); + try { + if (split.length == 4) + return new ColorRGBA(Float.parseFloat(split[0]), + Float.parseFloat(split[1]), + Float.parseFloat(split[2]), + Float.parseFloat(split[3])); + } + catch (NumberFormatException e) { + // deliberately left empty + } + throw new IllegalArgumentException(value + " should consist of exactly 4 numbers"); + } + + /** + * The width of the game view resolution in pixels. + */ + @Property("settings.resolution.width") //NON-NLS + private int resolutionWidth = 1200; + + /** + * The height of the game view resolution in pixels. + */ + @Property("settings.resolution.height") //NON-NLS + private int resolutionHeight = 800; + + /** + * Specifies whether the game should start in full-screen mode. + */ + @Property("settings.full-screen") //NON-NLS + private boolean fullScreen = false; + + /** + * Specifies whether gamma correction should be enabled. + * If enabled, the main framebuffer is configured for sRGB colors, + * and sRGB images are linearized. + *+ * Requires a GPU that supports GL_ARB_framebuffer_sRGB; otherwise, this setting will be ignored. + *
+ */ + @Property("settings.use-gamma-correction") //NON-NLS + private boolean useGammaCorrection = true; + + /** + * Specifies whether full resolution framebuffers should be used on Retina displays. + * This setting is ignored on non-Retina platforms. + */ + @Property("settings.use-retina-framebuffer") //NON-NLS + private boolean useRetinaFrameBuffer = false; + + /** + * Specifies whether the settings window should be shown for configuring the game. + */ + @Property("settings.show") //NON-NLS + private boolean showSettings = false; + + /** + * Specifies whether the JME statistics window should be shown in the lower left corner of the screen. + */ + @Property("statistics.show") //NON-NLS + private boolean showStatistics = false; + + /** + * The color of the top text during gameplay, represented as a {@link ColorRGBA} object. + */ + @Property("overlay.top.color") //NON-NLS + private ColorRGBA topColor = ColorRGBA.White; + + /** + * Creates a default {@code BattleshipAppConfig} with predefined values. + */ + public BattleshipAppConfig() { + // Default constructor + } + + /** + * Returns the width of the game view resolution in pixels. + * + * @return the width of the game view resolution in pixels + */ + public int getResolutionWidth() { + return resolutionWidth; + } + + /** + * Returns the height of the game view resolution in pixels. + * + * @return the height of the game view resolution in pixels + */ + public int getResolutionHeight() { + return resolutionHeight; + } + + /** + * Returns whether the game should start in full-screen mode. + * + * @return {@code true} if the game should start in full-screen mode; {@code false} otherwise + */ + public boolean fullScreen() { + return fullScreen; + } + + /** + * Returns whether gamma correction is enabled. + * If enabled, the main framebuffer is configured for sRGB colors, + * and sRGB images are linearized. + * + * @return {@code true} if gamma correction is enabled; {@code false} otherwise + */ + public boolean useGammaCorrection() { + return useGammaCorrection; + } + + /** + * Returns whether full resolution framebuffers should be used on Retina displays. + * This setting is ignored on non-Retina platforms. + * + * @return {@code true} if full resolution framebuffers should be used on Retina displays; {@code false} otherwise + */ + public boolean useRetinaFrameBuffer() { + return useRetinaFrameBuffer; + } + + /** + * Returns whether the settings window should be shown for configuring the game. + * + * @return {@code true} if the settings window should be shown; {@code false} otherwise + */ + public boolean getShowSettings() { + return showSettings; + } + + /** + * Returns whether the JME statistics window should be shown in the lower left corner of the screen. + * + * @return {@code true} if the statistics window should be shown; {@code false} otherwise + */ + public boolean getShowStatistics() { + return showStatistics; + } + + /** + * Returns the color of the top text during gameplay as a {@link ColorRGBA} object. + * + * @return the color of the top text during gameplay + */ + public ColorRGBA getTopColor() { + return topColor; + } +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipAppState.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipAppState.java new file mode 100644 index 0000000..0094516 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/BattleshipAppState.java @@ -0,0 +1,102 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.battleship.client; + +import com.jme3.app.Application; +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; +import pp.battleship.game.client.ClientGameLogic; + +/** + * Abstract class representing a state in the Battleship game. + * Extends the AbstractAppState from jMonkeyEngine to manage state behavior. + */ +public abstract class BattleshipAppState extends AbstractAppState { + private BattleshipApp app; + + /** + * Creates a new BattleshipAppState that is initially disabled. + * + * @see #setEnabled(boolean) + */ + protected BattleshipAppState() { + setEnabled(false); + } + + /** + * Initializes the state manager and application. + * + * @param stateManager The state manager + * @param application The application instance + */ + @Override + public void initialize(AppStateManager stateManager, Application application) { + super.initialize(stateManager, application); + this.app = (BattleshipApp) application; + if (isEnabled()) enableState(); + } + + /** + * Returns the BattleshipApp instance associated with this BattleshipAppState. + * + * @return The BattleshipApp instance. + */ + public BattleshipApp getApp() { + return app; + } + + /** + * Returns the client game logic handler. + * + * @return the client game logic handler + */ + public ClientGameLogic getGameLogic() { + return app.getGameLogic(); + } + + /** + * Checks if any dialog is currently displayed. + * + * @return true if any dialog is currently shown, false otherwise + */ + public boolean showsDialog() { + return app.getDialogManager().showsDialog(); + } + + /** + * Sets the enabled state of the BattleshipAppState. + * If the new state is the same as the current state, the method returns. + * + * @param enabled The new enabled state. + */ + @Override + public void setEnabled(boolean enabled) { + if (isEnabled() == enabled) return; + super.setEnabled(enabled); + if (app != null) { + if (enabled) + enableState(); + else + disableState(); + } + } + + /** + * This method is called when the state is enabled. + * It is meant to be overridden by subclasses to perform + * specific actions when the state is enabled. + */ + protected abstract void enableState(); + + /** + * This method is called when the state is disabled. + * It is meant to be overridden by subclasses to perform + * specific actions when the state is disabled. + */ + protected abstract void disableState(); +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java new file mode 100644 index 0000000..0fdcef6 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/GameSound.java @@ -0,0 +1,135 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +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; +import com.jme3.audio.AudioNode; +import pp.battleship.notification.GameEventListener; +import pp.battleship.notification.SoundEvent; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.util.prefs.Preferences; + +import static pp.util.PreferencesUtils.getPreferences; + +/** + * An application state that plays sounds. + */ +public class GameSound extends AbstractAppState implements GameEventListener { + private static final Logger LOGGER = System.getLogger(GameSound.class.getName()); + private static final Preferences PREFERENCES = getPreferences(GameSound.class); + private static final String ENABLED_PREF = "enabled"; //NON-NLS + + private AudioNode splashSound; + private AudioNode shipDestroyedSound; + private AudioNode explosionSound; + + /** + * Checks if sound is enabled in the preferences. + * + * @return {@code true} if sound is enabled, {@code false} otherwise. + */ + public static boolean enabledInPreferences() { + return PREFERENCES.getBoolean(ENABLED_PREF, true); + } + + /** + * Toggles the game sound on or off. + */ + public void toggleSound() { + setEnabled(!isEnabled()); + } + + /** + * Sets the enabled state of this AppState. + * 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, "Sound enabled: {0}", enabled); //NON-NLS + PREFERENCES.putBoolean(ENABLED_PREF, enabled); + } + + /** + * Initializes the sound effects 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) { + super.initialize(stateManager, 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 + } + + /** + * Loads a sound from the specified file. + * + * @param app The application + * @param name The name of the sound file. + * @return The loaded AudioNode. + */ + 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; + } + + /** + * Plays the splash sound effect. + */ + public void splash() { + if (isEnabled() && splashSound != null) + splashSound.playInstance(); + } + + /** + * Plays the explosion sound effect. + */ + public void explosion() { + if (isEnabled() && explosionSound != null) + explosionSound.playInstance(); + } + + /** + * Plays sound effect when a ship has been destroyed. + */ + public void shipDestroyed() { + if (isEnabled() && shipDestroyedSound != null) + shipDestroyedSound.playInstance(); + } + + @Override + public void receivedEvent(SoundEvent event) { + switch (event.sound()) { + case EXPLOSION -> explosion(); + case SPLASH -> splash(); + case DESTROYED_SHIP -> shipDestroyed(); + } + } +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/Menu.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/Menu.java new file mode 100644 index 0000000..0e100e8 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/Menu.java @@ -0,0 +1,143 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.battleship.client; + +import com.simsilica.lemur.Button; +import com.simsilica.lemur.Checkbox; +import com.simsilica.lemur.Label; +import com.simsilica.lemur.style.ElementId; +import pp.dialog.Dialog; +import pp.dialog.StateCheckboxModel; +import pp.dialog.TextInputDialog; + +import java.io.File; +import java.io.IOException; +import java.util.prefs.Preferences; + +import static pp.battleship.Resources.lookup; +import static pp.util.PreferencesUtils.getPreferences; + +/** + * The Menu class represents the main menu in the Battleship game application. + * It extends the Dialog class and provides functionalities for loading, saving, + * returning to the game, and quitting the application. + */ +class Menu extends Dialog { + 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")); + + /** + * Constructs the Menu dialog for the Battleship application. + * + * @param app the BattleshipApp instance + */ + public Menu(BattleshipApp app) { + super(app.getDialogManager()); + this.app = app; + addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS + addChild(new Checkbox(lookup("menu.sound-enabled"), + new StateCheckboxModel(app, GameSound.class))); + addChild(loadButton) + .addClickCommands(s -> ifTopDialog(this::loadDialog)); + addChild(saveButton) + .addClickCommands(s -> ifTopDialog(this::saveDialog)); + addChild(new Button(lookup("menu.return-to-game"))) + .addClickCommands(s -> ifTopDialog(this::close)); + addChild(new Button(lookup("menu.quit"))) + .addClickCommands(s -> ifTopDialog(app::closeApp)); + update(); + } + + /** + * Updates the state of the load and save buttons based on the game logic. + */ + @Override + public void update() { + loadButton.setEnabled(app.getGameLogic().mayLoadMap()); + saveButton.setEnabled(app.getGameLogic().maySaveMap()); + } + + /** + * As an escape action, this method closes the menu if it is the top dialog. + */ + @Override + public void escape() { + close(); + } + + /** + * Functional interface for file actions. + */ + @FunctionalInterface + private interface FileAction { + /** + * Executes a file action. + * + * @param file the file to be processed + * @throws IOException if an I/O error occurs + */ + void run(File file) throws IOException; + } + + /** + * Handles the file action for the provided dialog. + * + * @param fileAction the file action to be executed + * @param dialog the dialog providing the file input + */ + private void handle(FileAction fileAction, TextInputDialog dialog) { + try { + final String path = dialog.getInput().getText(); + PREFERENCES.put(LAST_PATH, path); + fileAction.run(new File(path)); + dialog.close(); + } + catch (IOException e) { + app.errorDialog(e.getLocalizedMessage()); + } + } + + /** + * Shows a file dialog for loading or saving files. + * + * @param fileAction the action to perform with the selected file + * @param label the label for the 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(); + final String path = PREFERENCES.get(LAST_PATH, null); + if (path != null) + dialog.getInput().setText(path.trim()); + dialog.open(); + } + + /** + * Shows the load dialog for loading maps. + */ + private void loadDialog() { + fileDialog(app.getGameLogic()::loadMap, lookup("menu.map.load")); + } + + /** + * Shows the save dialog for saving maps. + */ + private void saveDialog() { + fileDialog(app.getGameLogic()::saveMap, lookup("menu.map.save")); + } +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java new file mode 100644 index 0000000..3f2f5a6 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java @@ -0,0 +1,153 @@ +//////////////////////////////////////// +// Programming project code +// UniBw M, 2022, 2023, 2024 +// www.unibw.de/inf2 +// (c) Mark Minas (mark.minas@unibw.de) +//////////////////////////////////////// + +package pp.battleship.client; + +import com.simsilica.lemur.Container; +import com.simsilica.lemur.Label; +import com.simsilica.lemur.TextField; +import com.simsilica.lemur.component.SpringGridLayout; +import pp.dialog.Dialog; +import pp.dialog.DialogBuilder; +import pp.dialog.SimpleDialog; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static pp.battleship.Resources.lookup; + +/** + * Represents a dialog for setting up a network connection in the Battleship game. + * Allows users to specify the host and port for connecting to a game server. + */ +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 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