6 Commits

239 changed files with 518056 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# Gradle
.gradle
build
# VSC
bin
# IntelliJ
*.iml
.idea
out
# Eclipse
.classpath
.project
# Libraries
*.so
*.dylib
*.dll
*.jar
*.class
.DS_Store
!Projekte/gradle/wrapper/gradle-wrapper.jar

1
Dokumente/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
#

3
Projekte/.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.bat text eol=crlf
gradlew text eol=lf

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BattleshipServer" type="Application" factoryName="Application"
nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="pp.battleship.server.BattleshipServer"/>
<module name="Projekte.battleship.server.main"/>
<option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/>
<extension name="coverage">
<pattern>
<option name="PATTERN" value="pp.battleship.server.*"/>
<option name="ENABLED" value="true"/>
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true"/>
</method>
</configuration>
</component>

View File

@@ -0,0 +1,24 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Projekte [test]" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--continue" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="test" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
</component>

189
Projekte/README.md Normal file
View File

@@ -0,0 +1,189 @@
# Beispielprogramme des Programmierprojekts
Hier ist der Quellcode für das in der Einarbeitungsphase genutzte Spiel
_Battleships_ zu finden. Die Quellen bestehen aus den folgenden
Gradle-Unterprojekten:
* _:battleship:server_
* _:battleship:client_
* _:battleship:model_
* _:battleship:converter_
* _:common_
* _:jme-common_
_Battleships_ ist ein netzwerkbasiertes Spiel und besteht aus einem Server- und
einem Clientanteil, die in den Unterprojekten _:battleship:server_ und
_:battleship:client_ realisiert sind. Beide nutzen das Unterprojekt
_:battleship:model_, das den gemeinsamen Modellanteil enthält.
Die Unterprojekte _:common_ und _:jme-common_ enthalten Hilfsklassen.
Das Unterprojekt _:battleship:converter_ wird für _Battleships_ selbst nicht
benötigt, sondern enthält lediglich den Code, um ein im Spiel verwendetes
3d-Modell eines Schlachtschiffs in eine _J3O_-Datei umzuwandeln, die von jME
einfacher geladen werden kann.
## 1 Vorbereitung
Für das Programmierprojekt empfehlen wir die Verwendung von Java 20. Unter Linux
sollte [_Eclipse Temurin_](https://adoptium.net/temurin/releases/?version=20)
als JDK verwendet werden, andere JDKs können unter Linux Probleme verursachen.
Auf anderen Betriebssystemen empfehlen wir aber ebenfalls Temurin. Im Folgenden
ist beschrieben, wie Sie Temurin installieren und die Umgebungsvariable
**JAVA_HOME** richtig setzen, damit Sie Gradle (siehe unten) verwenden können.
### 1.1 Installation von Temurin
Laden Sie [_Eclipse Temurin_](https://adoptium.net/temurin/releases/?version=20)
entsprechend Ihrem Betriebssystem und Ihrer Prozessorarchitektur herunter und
entpacken Sie das Archiv in einem Verzeichnis Ihrer Wahl auf Ihrem Rechner.
### 1.2 Setzen von JAVA_HOME
Zur Verwendung mit Gradle muss die Umgebungsvariable **JAVA_HOME** richtig
gesetzt werden. Folgen Sie dazu den nachfolgenden Anweisungen entsprechend Ihrem
Betriebssystem:
* **Windows:**
Öffnen Sie ihre Powershell (Core) bzw. ihr Windows Terminal mit Powershell
(Core). Überprüfen Sie, ob die Umgebungsvariable korrekt gesetzt ist:
`Get-ChildItem -Path Env:JAVA_HOME`
Falls kein oder ein falscher Pfad gesetzt ist, setzen Sie diesen mit dem
folgenden Kommando (in einer Zeile):
`[System.Environment]::SetEnvironmentVariable('JAVA_HOME','<Pfad zum SDK>',[System.EnvironmentVariableTarget]::User)`
Alternativ können Sie die GUI verwenden. Unter Windows 10 klicken Sie die
Windows-Taste und dann das Zahnrad um die Einstellungen zu öffnen. Dort wählen
Sie "System", dann "Info" (links unten) und nun
"Erweiterte Systemeinstellungen" (rechts) um den Dialog "Systemeigenschaften"
zu starten. Im Reiter "Erweitert" klicken Sie
"Umgebungsvariablen..." und klicken dann unter "Benutzervariablen" den Knopf
"Neu..." um JAVA_HOME anzulegen oder "Bearbeiten" um ihn zu ändern. Geben Sie
als Name `JAVA_HOME` und als Wert den Pfad ein. Schließen Sie mit "OK".
> **(!) Beachten Sie, dass Sie die jeweilige Applikation neu starten müssen**,
> um von der gesetzten Umgebungsvariablen Notiz zu nehmen.
> Dies betrifft auch die Shell, die Sie gerade verwenden.
* **UNIX (Linux/MacOS):**
Öffnen oder erstellen Sie die Datei `~/.profile` (wenn Sie die Bash verwenden;
bei anderen Shells sind es andere Dateien) und ergänzen Sie am Ende der Datei
die Zeile:
`export JAVA_HOME="<Pfad zum entpackten Archiv>"`
Ersetzen Sie dabei `<Pfad zum entpackten Archiv>` mit dem entsprechenden Pfad.
Zum Beispiel:
`export JAVA_HOME="/home/user/jdk-20.0.2"`
Fügen Sie dann die folgende Zeile hinzu:
`export PATH="$JAVA_HOME/bin:$PATH"`
## 2 Programmstart
Grundsätzlich kann man das gesamte Projekt einfach in IntelliJ öffnen. Details
dazu sind im Aufgabenblatt zur Einarbeitungsaufgabe zu finden. Im Folgenden ist
beschrieben, wie die _Batttleships_ unmittelbar von der Kommandozeile gestartet
werden können.
Um _Battleships_ spielen zu können, muss man zuerst das Server-Programm auf
einem Rechner und dann zweimal das Client-Programm auf beliebigen Rechnern
starten, die TCP/IP-Verbindungen zum Server erlauben. Natürlich ist es auch
möglich, alle drei Programme auf demselben Rechner zu starten.
Es empfiehlt sich der Start von der Kommandozeile. Will man alle drei Programme
auf demselben Rechner starten, sollte man dazu drei Shell-Instanzen öffnen und
in jeder eines der Programme starten. Auf diese Weise können die
Logging-Ausgaben der drei Programme voneinander unterschieden werden.
Das Server-Programm startet man unmittelbar mit Gradle mit
`./gradlew :battleship:server:run`
Unter Windows kann es je nach Shell (Eingabeaufforderung cmd) erforderlich sein,
`/` jeweils durch `\ ` zu ersetzen.
Im Verzeichnis `battleship/server` befindet sich die Datei `config.propeties`,
worüber sich der Server konfigurieren lässt. Mit der Zeile `port=1234` lässt
sich der verwendete Server-Port (hier 1234) einstellen. Außerdem befindet sich
dort die Datei `logging.properties`, womit das Logging des Servers konfiguriert
wird.
Das Client-Programm startet man unmittelbar mit Gradle mit
`./gradlew :battleship:client:run`
Die Datei `logging.properties` im Verzeichnis `battleship/client` konfiguriert
das Logging des Clients.
Alternativ kann man auch die Start-Skripte
* `./battleship/server/build/install/battleship-server/bin/battleship-server`
* `./battleship/client/build/install/battleship/bin/battleship`
direkt in der Kommandozeile starten. Allerdings müssen sie zuvor mittels
`./gradlew installDist`
erzeugt worden sein. Beachten Sie aber, dass nur im **aktuellen
Arbeitsverzeichnis** nach den Dateien `config.properties` und
`logging.properties` gesucht wird und diese geladen werden. Das heißt, dass die
vordefinierten Dateien in den Verzeichnissen `battleship/server` und
`battleship/client` nur dann gelesen werden, wenn Sie diese Verzeichnisse als
aktuelle Arbeitsverzeichnisse nutzen. Wie üblich müssen Sie dazu in der
Kommandozeile
`cd battleship/server`
bzw.
`cd battleship/client`
eingeben.
## 3 Hinweise zu _Battleships_
Der _Battleships_-Client hat ein Menü, in das man immer mit der
Esc-Taste kommt. Aus dem Menü heraus lässt sich das Programm auch schließen.
Beachte, dass sich beim Laden und Speichern eines Spiels kein Dateidialog
öffnet. Vielmehr muss man den Dateipfad in das Dialogfeld eingeben. Da
JSON-Dateien geschrieben werden, empfiehlt sich das Datei-Suffix _.json_.
## 4 Allgemeine Gradle-Tasks:
- `./gradlew clean`
Entfernt alle `build`-Verzeichnisse und alle erzeugten Dateien.
- `./gradlew classes`
Übersetzt den Quellcode und legt unter build den Bytecode sowie
Ressourcen ab.
- `./gradlew javadoc`
Erzeugt die Dokumentation aus den JavaDoc-Kommentaren im Verzeichnis
`build/docs/javadoc` des jeweiligen Unterprojekts.
- `./gradlew test`
Führt die JUnit-Tests durch. Ergebnisse sind im Verzeichnis
`build/reports/tests` des jeweiligen Unterprojekts zu finden.
- `./gradlew build`
Führt die JUnit-Tests durch und erstellt in `build/distributions`
gepackte Distributionsdateien
- `./gradlew installDist`
Erstellt unter `battleship/client/build/install` und
`battleship/server/build/install` Verzeichnisse, die jeweils eine ausführbare
Distribution samt Start-Skripten enthält (siehe oben).
---
Juli 2024

View File

@@ -0,0 +1,24 @@
plugins {
id 'buildlogic.jme-application-conventions'
}
description = 'Battleship Client'
dependencies {
implementation project(":jme-common")
implementation project(":battleship:model")
implementation libs.jme3.desktop
implementation 'com.simsilica:lemur:1.14.0'
implementation 'com.simsilica:lemur-proto:1.10.0'
runtimeOnly libs.jme3.awt.dialogs
runtimeOnly libs.jme3.plugins
runtimeOnly libs.jme3.jogg
runtimeOnly libs.jme3.testdata
}
application {
mainClass = 'pp.battleship.client.BattleshipApp'
applicationName = 'battleship'
}

View File

@@ -0,0 +1,73 @@
########################################
## Programming project code
## UniBw M, 2022, 2023, 2024
## www.unibw.de/inf2
## (c) Mark Minas (mark.minas@unibw.de)
########################################
#
# Battleship client configuration
#
# Specifies the map used by the opponent in single mode.
# Single mode is activated if this property is set.
#map.opponent=maps/map2.json
#
# Specifies the map used by the player in single mode.
# The player must define their own map if this property is not set.
map.own=maps/map1.json
#
# Coordinates of the shots fired by the RobotClient in the order listed.
# Example:
# 2, 0,\
# 2, 1,\
# 2, 2,\
# 2, 3
# defines four shots, namely at the coordinates
# (x=2, y=0), (x=2, y=1), (x=2, y=2), and (x=2, y=3)
robot.targets=2, 0,\
2, 1,\
2, 2,\
2, 3
#
# Delay in milliseconds between each shot fired by the RobotClient.
robot.delay=500
#
# The dimensions of the game map used in single mode.
# 'map.width' defines the number of columns, and 'map.height' defines the number of rows.
map.width=10
map.height=10
#
# The number of ships of each length available in single mode.
# The value is a comma-separated list where each element corresponds to the number of ships
# with a specific length. For example:
# ship.nums=4, 3, 2, 1
# This configuration means:
# - 4 ships of length 1
# - 3 ships of length 2
# - 2 ships of length 3
# - 1 ship of length 4
ship.nums=4, 3, 2, 1
#
# Screen settings
#
# Color of the text displayed at the top of the overlay.
# The format is (red, green, blue, alpha) where each value ranges from 0 to 1.
overlay.top.color=1, 1, 1, 1
#
# Application settings configuration
# Determines whether the settings window is shown at startup.
settings.show=false
#
# Specifies the width of the application window in pixels.
settings.resolution.width=1200
#
# Specifies the height of the application window in pixels.
settings.resolution.height=800
#
# Determines whether the application runs in full-screen mode.
settings.full-screen=false
#
# Enables or disables gamma correction to improve color accuracy.
settings.use-gamma-correction=true
#
# Indicates whether the statistics window is displayed during gameplay.
statistics.show=false

View File

@@ -0,0 +1,8 @@
handlers=java.util.logging.ConsoleHandler
.level=INFO
pp.level=FINE
com.jme3.network.level=INFO
;com.jme3.util.TangentBinormalGenerator.level=SEVERE
java.util.logging.ConsoleHandler.level=FINER
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
;java.util.logging.SimpleFormatter.format=[%4$s %2$s] %5$s%n

View File

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

View File

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

View File

@@ -0,0 +1,429 @@
////////////////////////////////////////
// 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.DebugKeysAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.system.AppSettings;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.style.BaseStyles;
import pp.battleship.client.gui.BattleAppState;
import pp.battleship.client.gui.EditorAppState;
import pp.battleship.client.gui.SeaAppState;
import pp.battleship.game.client.BattleshipClient;
import pp.battleship.game.client.ClientGameLogic;
import pp.battleship.game.client.ServerConnection;
import pp.battleship.game.singlemode.BattleshipClientConfig;
import pp.battleship.game.singlemode.ServerConnectionMockup;
import pp.battleship.notification.ClientStateEvent;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.dialog.DialogBuilder;
import pp.dialog.DialogManager;
import pp.graphics.Draw;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.LogManager;
import static pp.battleship.Resources.lookup;
/**
* The main class for the Battleship client application.
* It manages the initialization, input setup, GUI setup, and game states for the client.
*/
public class BattleshipApp extends SimpleApplication implements BattleshipClient, GameEventListener {
/**
* Logger for logging messages within the application.
*/
private static final Logger LOGGER = System.getLogger(BattleshipApp.class.getName());
/**
* Path to the styles script for GUI elements.
*/
private static final String STYLES_SCRIPT = "Interface/Lemur/pp-styles.groovy"; //NON-NLS
/**
* Path to the font resource used in the GUI.
*/
private static final String FONT = "Interface/Fonts/Default.fnt"; //NON-NLS
/**
* Path to the client configuration file, if one exists.
*/
private static final File CONFIG_FILE = new File("client.properties");
/**
* Input mapping name for mouse clicks.
*/
public static final String CLICK = "CLICK";
/**
* Input mapping name for the Escape key.
*/
private static final String ESC = "ESC";
/**
* Manager for handling dialogs within the application.
*/
private final DialogManager dialogManager = new DialogManager(this);
/**
* The server connection instance, used for communicating with the game server.
*/
private final ServerConnection serverConnection;
/**
* Instance of the {@link Draw} class for rendering graphics.
*/
private Draw draw;
/**
* Text display at the top of the GUI for showing information to the user.
*/
private BitmapText topText;
/**
* Executor service for handling asynchronous tasks within the application.
*/
private ExecutorService executor;
/**
* Handler for managing the client's game logic.
*/
private final ClientGameLogic logic;
/**
* Configuration settings for the Battleship client application.
*/
private final BattleshipAppConfig config;
/**
* Listener for handling actions triggered by the Escape key.
*/
private final ActionListener escapeListener = (name, isPressed, tpf) -> escape(isPressed);
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) {
LOGGER.log(Level.INFO, e.getMessage());
}
}
/**
* Starts the Battleship application.
*
* @param args Command-line arguments for launching the application.
*/
public static void main(String[] args) {
new BattleshipApp().start();
}
/**
* Constructs a new {@code BattleshipApp} instance.
* Initializes the configuration, server connection, and game logic listeners.
*/
private BattleshipApp() {
config = new BattleshipAppConfig();
config.readFromIfExists(CONFIG_FILE);
serverConnection = makeServerConnection();
logic = new ClientGameLogic(serverConnection);
logic.addListener(this);
setShowSettings(config.getShowSettings());
setSettings(makeSettings());
}
/**
* Creates and configures application settings from the client configuration.
*
* @return A configured {@link AppSettings} object.
*/
private AppSettings makeSettings() {
final AppSettings settings = new AppSettings(true);
settings.setTitle(lookup("battleship.name"));
settings.setResolution(config.getResolutionWidth(), config.getResolutionHeight());
settings.setFullscreen(config.fullScreen());
settings.setUseRetinaFrameBuffer(config.useRetinaFrameBuffer());
settings.setGammaCorrection(config.useGammaCorrection());
return settings;
}
/**
* Factory method for creating a server connection based on the current
* client configuration.
*
* @return A {@link ServerConnection} instance, which could be a real or mock server.
*/
private ServerConnection makeServerConnection() {
if (config.isSingleMode())
return new ServerConnectionMockup(this);
return new NetworkSupport(this);
}
/**
* Returns the dialog manager responsible for managing in-game dialogs.
*
* @return The {@link DialogManager} instance.
*/
DialogManager getDialogManager() {
return dialogManager;
}
/**
* Returns the game logic handler for the client.
*
* @return The {@link ClientGameLogic} instance.
*/
@Override
public ClientGameLogic getGameLogic() {
return logic;
}
/**
* Returns the current configuration settings for the Battleship client.
*
* @return The {@link BattleshipClientConfig} instance.
*/
@Override
public BattleshipAppConfig getConfig() {
return config;
}
/**
* Initializes the application.
* Sets up input mappings, GUI, game states, and connects to the server.
*/
@Override
public void simpleInitApp() {
setPauseOnLostFocus(false);
draw = new Draw(assetManager);
setupInput();
setupStates();
setupGui();
serverConnection.connect();
}
/**
* Sets up the graphical user interface (GUI) for the application.
*/
private void setupGui() {
GuiGlobals.initialize(this);
BaseStyles.loadStyleResources(STYLES_SCRIPT);
GuiGlobals.getInstance().getStyles().setDefaultStyle("pp"); //NON-NLS
final BitmapFont normalFont = assetManager.loadFont(FONT); //NON-NLS
topText = new BitmapText(normalFont);
final int height = context.getSettings().getHeight();
topText.setLocalTranslation(10f, height - 10f, 0f);
topText.setColor(config.getTopColor());
guiNode.attachChild(topText);
}
/**
* Configures input mappings and sets up listeners for user interactions.
*/
private void setupInput() {
inputManager.deleteMapping(INPUT_MAPPING_EXIT);
inputManager.setCursorVisible(false);
inputManager.addMapping(ESC, new KeyTrigger(KeyInput.KEY_ESCAPE));
inputManager.addMapping(CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
inputManager.addListener(escapeListener, ESC);
}
/**
* Initializes and attaches the necessary application states for the game.
*/
private void setupStates() {
if (config.getShowStatistics()) {
final BitmapFont normalFont = assetManager.loadFont(FONT); //NON-NLS
final StatsAppState stats = new StatsAppState(guiNode, normalFont);
stateManager.attach(stats);
}
flyCam.setEnabled(false);
stateManager.detach(stateManager.getState(StatsAppState.class));
stateManager.detach(stateManager.getState(DebugKeysAppState.class));
attachGameSound();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
}
/**
* Attaches the game sound state and sets its initial enabled state.
*/
private void attachGameSound() {
final GameSound gameSound = new GameSound();
logic.addListener(gameSound);
gameSound.setEnabled(GameSound.enabledInPreferences());
stateManager.attach(gameSound);
}
/**
* Updates the application state every frame.
* This method is called once per frame during the game loop.
*
* @param tpf Time per frame in seconds.
*/
@Override
public void simpleUpdate(float tpf) {
super.simpleUpdate(tpf);
dialogManager.update(tpf);
logic.update(tpf);
}
/**
* Handles the Escape key action to either close the top dialog or show the main menu.
*
* @param isPressed Indicates whether the Escape key is pressed.
*/
private void escape(boolean isPressed) {
if (!isPressed) return;
if (dialogManager.showsDialog())
dialogManager.escape();
else
new Menu(this).open();
}
/**
* Returns the {@link Draw} instance used for rendering graphical elements in the game.
*
* @return The {@link Draw} instance.
*/
public Draw getDraw() {
return draw;
}
/**
* Handles a request to close the application.
* If the request is initiated by pressing ESC, this parameter is true.
*
* @param esc If true, the request is due to the ESC key being pressed.
*/
@Override
public void requestClose(boolean esc) { /* do nothing */ }
/**
* Closes the application, displaying a confirmation dialog if the client is connected to a server.
*/
public void closeApp() {
if (serverConnection.isConnected())
confirmDialog(lookup("confirm.leaving"), this::close);
else
close();
}
/**
* Closes the application, disconnecting from the server and stopping the application.
*/
private void close() {
serverConnection.disconnect();
stop();
}
/**
* Updates the informational text displayed in the GUI.
*
* @param text The information text to display.
*/
void setInfoText(String text) {
LOGGER.log(Level.DEBUG, "setInfoText {0}", text); //NON-NLS
topText.setText(text);
}
/**
* Updates the informational text in the GUI based on the key received in an {@link InfoTextEvent}.
*
* @param event The {@link InfoTextEvent} containing the key for the text to display.
*/
@Override
public void receivedEvent(InfoTextEvent event) {
LOGGER.log(Level.DEBUG, "received info text {0}", event.key()); //NON-NLS
setInfoText(lookup(event.key()));
}
/**
* Handles client state events to update the game states accordingly.
*
* @param event The {@link ClientStateEvent} representing the state change.
*/
@Override
public void receivedEvent(ClientStateEvent event) {
stateManager.getState(EditorAppState.class).setEnabled(logic.showEditor());
stateManager.getState(BattleAppState.class).setEnabled(logic.showBattle());
stateManager.getState(SeaAppState.class).setEnabled(logic.showBattle());
}
/**
* Returns the executor service used for handling multithreaded tasks.
*
* @return The {@link ExecutorService} instance.
*/
public ExecutorService getExecutor() {
if (executor == null)
executor = Executors.newCachedThreadPool();
return executor;
}
/**
* Stops the application, shutting down the executor service and halting execution.
*
* @param waitFor If true, waits for the application to stop before returning.
*/
@Override
public void stop(boolean waitFor) {
if (executor != null) executor.shutdownNow();
super.stop(waitFor);
}
/**
* Displays a confirmation dialog with a specified question and action for the "Yes" button.
*
* @param question The question to display in the dialog.
* @param yesAction The action to perform if "Yes" is selected.
*/
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();
}
/**
* Displays an error dialog with the specified error message.
*
* @param errorMessage The error message to display in the dialog.
*/
void errorDialog(String errorMessage) {
DialogBuilder.simple(dialogManager)
.setTitle(lookup("dialog.error"))
.setText(errorMessage)
.setOkButton(lookup("button.ok"))
.build()
.open();
}
}

View File

@@ -0,0 +1,196 @@
////////////////////////////////////////
// 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.math.ColorRGBA;
import pp.battleship.game.singlemode.BattleshipClientConfig;
/**
* Provides access to the Battleship application configuration.
* Extends {@link BattleshipClientConfig} to include additional properties specific to the client,
* particularly those related to screen settings and visual customization.
* <p>
* <b>Note:</b> Attributes of this class should not be marked as {@code final}
* to ensure proper functionality when reading from a properties file.
* </p>
*/
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.
* <p>
* Requires a GPU that supports GL_ARB_framebuffer_sRGB; otherwise, this setting will be ignored.
* </p>
*/
@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;
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,127 @@
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 music.
*/
public class GameMusic extends AbstractAppState {
private static final Logger LOGGER = System.getLogger(GameMusic.class.getName());
private static final Preferences PREFERENCES = getPreferences(GameMusic.class);
private static final String ENABLED_PREF = "enabled"; //NON-NLS
private static final String VOLUME_PREF = "volume"; //NON-NLS
private AudioNode piratenMusic;
/**
* Checks if sound is enabled in the preferences.
*
* @return {@code true} if sound is enabled, {@code false} otherwise.
*/
//Prüft, ob die Musik eingeschalten ist.
public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true);
}
/**
* Checks enabled volume in preferences.
*
* @return {@code float} if a volumePreference is set, the volume is set to the Value in PREFERENCES,
* {@code 0.25f} if no volumePreference is set in PREFERENCES, the Volume is set to a default of 0.25f
*
*/
//Standardeinstellung für Musik
public static float volumePreference() {
return PREFERENCES.getFloat(VOLUME_PREF, 0.33f);
}
/**
* 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;
else if (!isEnabled() && enabled) {
if (piratenMusic != null) piratenMusic.play();
} else if (isEnabled() && !enabled) {
if (piratenMusic != null) piratenMusic.stop();
}
super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Music enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
}
/**
* Initializes the music.
* 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);
piratenMusic =loadSound(app, "Sound/BackgroundMusic/Piratenmusik.ogg");
setVolume(volumePreference());
piratenMusic.setLooping(true);
if (isEnabled() && piratenMusic != null) {
piratenMusic.play();
}
}
/**
* Loads the music 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;
}
//Einstellung der Lautstärke
public void setVolume(float vol){
piratenMusic.setVolume(vol);
PREFERENCES.putFloat(VOLUME_PREF, vol);
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,170 @@
////////////////////////////////////////
// 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.math.Vector3f;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.RangedValueModel;
import com.simsilica.lemur.style.ElementId;
import com.simsilica.lemur.Slider;
import pp.battleship.client.gui.VolumeSlider;
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"));
private final VolumeSlider volumeSlider;
/**
* 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(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.background-sound-enabled"), new StateCheckboxModel(app, GameMusic.class)));
volumeSlider = new VolumeSlider(app.getStateManager().getState(GameMusic.class));
addChild(volumeSlider);
addChild(loadButton)
.addClickCommands(s -> ifTopDialog(this::loadDialog));
addChild(saveButton);
saveButton.setEnabled(app.getGameLogic().maySaveMap());
// Füge den Slider zum GUI hinzu
addChild(volumeSlider);
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"));
}
}

View File

@@ -0,0 +1,178 @@
////////////////////////////////////////
// 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 com.simsilica.lemur.Button;
import pp.battleship.server.BattleshipServer;
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 final Button serverButton = new Button(lookup("client.server-star"));
private final Button serverButton = new Button(lookup("client.server-start"));
private String hostname;
private int portNumber;
private Future<Object> connectionFuture;
private Dialog progressDialog;
/**
* Constructs a new NetworkDialog.
*
* @param network The NetworkSupport instance to be used for network operations.
*/
NetworkDialog(NetworkSupport network) {
super(network.getApp().getDialogManager());
this.network = network;
host.setSingleLine(true);
host.setPreferredWidth(400f);
port.setSingleLine(true);
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);
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);
//Add the button to start the sever
addChild(serverButton).addClickCommands(s -> ifTopDialog(this::startServerInThread));
}
/**
* Handles the action for the connect button in the connection dialog.
* Tries to parse the port number and initiate connection to the server.
*/
private void connect() {
LOGGER.log(Level.INFO, "connect to host={0}, port={1}", host, port); //NON-NLS
try {
hostname = host.getText().trim().isEmpty() ? LOCALHOST : host.getText();
portNumber = Integer.parseInt(port.getText());
openProgressDialog();
connectionFuture = network.getApp().getExecutor().submit(this::initNetwork);
}
catch (NumberFormatException e) {
network.getApp().errorDialog(lookup("port.must.be.integer"));
}
}
/**
* Creates a dialog indicating that the connection is in progress.
*/
private void openProgressDialog() {
progressDialog = DialogBuilder.simple(network.getApp().getDialogManager())
.setText(lookup("label.connecting"))
.build();
progressDialog.open();
}
/**
* Tries to initialize the network connection.
*
* @throws RuntimeException If an error occurs when creating the client.
*/
private Object initNetwork() {
try {
network.initNetwork(hostname, portNumber);
return null;
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* This method is called by {@linkplain pp.dialog.DialogManager#update(float)} for periodically
* updating this dialog. T
*/
@Override
public void update(float delta) {
if (connectionFuture != null && connectionFuture.isDone())
try {
connectionFuture.get();
success();
}
catch (ExecutionException e) {
failure(e.getCause());
}
catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Interrupted!", e); //NON-NLS
Thread.currentThread().interrupt();
}
}
/**
* Handles a successful connection to the game server.
*/
private void success() {
connectionFuture = null;
progressDialog.close();
this.close();
network.getApp().setInfoText(lookup("wait.for.an.opponent"));
}
/**
* Handles a failed connection attempt.
*
* @param e The cause of the failure.
*/
private void failure(Throwable e) {
connectionFuture = null;
progressDialog.close();
network.getApp().errorDialog(lookup("server.connection.failed"));
network.getApp().setInfoText(e.getLocalizedMessage());
}
/**
* Starts the server in a separate thread.
*/
private void startServerInThread() {
serverButton.setEnabled(false);
Thread serverThread = new Thread(() -> {
try {
BattleshipServer.main(null);
} catch (Exception e) {
serverButton.setEnabled(true);
LOGGER.log(Level.ERROR, "Server could not be started", e);
network.getApp().errorDialog("Could not start server: " + e.getMessage());
}
});
serverThread.start();
}
}

View File

@@ -0,0 +1,152 @@
////////////////////////////////////////
// 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.network.Client;
import com.jme3.network.ClientStateListener;
import com.jme3.network.Message;
import com.jme3.network.MessageListener;
import com.jme3.network.Network;
import pp.battleship.game.client.ServerConnection;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.server.ServerMessage;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import static pp.battleship.Resources.lookup;
/**
* Manages the network connection for the Battleship application.
* Handles connecting to and disconnecting from the server, and sending messages.
*/
class NetworkSupport implements MessageListener<Client>, ClientStateListener, ServerConnection {
private static final Logger LOGGER = System.getLogger(NetworkSupport.class.getName());
private final BattleshipApp app;
private Client client;
/**
* Constructs a NetworkSupport instance for the given Battleship application.
*
* @param app The Battleship application instance.
*/
public NetworkSupport(BattleshipApp app) {
this.app = app;
}
/**
* Returns the Battleship application instance.
*
* @return Battleship application instance
*/
BattleshipApp getApp() {
return app;
}
/**
* Checks if there is a connection to the game server.
*
* @return true if there is a connection to the game server, false otherwise.
*/
@Override
public boolean isConnected() {
return client != null && client.isConnected();
}
/**
* Attempts to join the game if there is no connection yet.
* Opens a dialog for the user to enter the host and port information.
*/
@Override
public void connect() {
if (client == null)
new NetworkDialog(this).open();
}
/**
* Closes the client connection.
*/
@Override
public void disconnect() {
if (client == null) return;
client.close();
client = null;
LOGGER.log(Level.INFO, "client closed"); //NON-NLS
}
/**
* Initializes the network connection.
*
* @param host The server's address.
* @param port The server's port.
* @throws IOException If an I/O error occurs when creating the client.
*/
void initNetwork(String host, int port) throws IOException {
if (client != null)
throw new IllegalStateException("trying to join a game again");
client = Network.connectToServer(host, port);
client.start();
client.addMessageListener(this);
client.addClientStateListener(this);
}
/**
* Called when a message is received from the server.
*
* @param client The client instance that received the message.
* @param message The message received from the server.
*/
@Override
public void messageReceived(Client client, Message message) {
LOGGER.log(Level.INFO, "message received from server: {0}", message); //NON-NLS
if (message instanceof ServerMessage serverMessage)
app.enqueue(() -> serverMessage.accept(app.getGameLogic()));
}
/**
* Called when the client has successfully connected to the server.
*
* @param client The client that connected to the server.
*/
@Override
public void clientConnected(Client client) {
LOGGER.log(Level.INFO, "Client connected: {0}", client); //NON-NLS
}
/**
* Called when the client is disconnected from the server.
*
* @param client The client that was disconnected.
* @param disconnectInfo Information about the disconnection.
*/
@Override
public void clientDisconnected(Client client, DisconnectInfo disconnectInfo) {
LOGGER.log(Level.INFO, "Client {0} disconnected: {1}", client, disconnectInfo); //NON-NLS
if (this.client != client)
throw new IllegalArgumentException("parameter value must be client");
LOGGER.log(Level.INFO, "client still connected: {0}", client.isConnected()); //NON-NLS
this.client = null;
disconnect();
app.enqueue(() -> app.setInfoText(lookup("lost.connection.to.server")));
}
/**
* Sends the specified message to the server.
*
* @param message The message to be sent to the server.
*/
@Override
public void send(ClientMessage message) {
LOGGER.log(Level.INFO, "sending {0}", message); //NON-NLS
if (client == null)
app.errorDialog(lookup("lost.connection.to.server"));
else
client.send(message);
}
}

View File

@@ -0,0 +1,122 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.input.controls.ActionListener;
import com.jme3.scene.Node;
import com.jme3.system.AppSettings;
import pp.battleship.client.BattleshipAppState;
import pp.battleship.model.IntPoint;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import static pp.battleship.client.BattleshipApp.CLICK;
/**
* Represents the state responsible for managing the battle interface within the Battleship game.
* This state handles the display and interaction of the battle map, including the opponent's map.
* It manages GUI components, input events, and the layout of the interface when this state is enabled.
*/
public class BattleAppState extends BattleshipAppState {
private static final Logger LOGGER = System.getLogger(BattleAppState.class.getName());
private static final float DEPTH = 0f;
private static final float GAP = 20f;
/**
* A listener for handling click events in the battle interface.
* When a click is detected, it triggers the corresponding actions on the opponent's map.
*/
private final ActionListener clickListener = (name, isPressed, tpf) -> click(isPressed);
/**
* The root node for all GUI components in the battle state.
*/
private final Node battleNode = new Node("Battle"); //NON-NLS
/**
* A view representing the opponent's map in the GUI.
*/
private MapView opponentMapView;
/**
* Enables the battle state by initializing, laying out, and adding GUI components.
* Attaches the components to the GUI node and registers input listeners.
*/
@Override
protected void enableState() {
battleNode.detachAllChildren();
initializeGuiComponents();
layoutGuiComponents();
addGuiComponents();
getApp().getGuiNode().attachChild(battleNode);
getApp().getInputManager().addListener(clickListener, CLICK);
}
/**
* Disables the battle state by removing GUI components and unregistering input listeners.
* Also handles cleanup of resources, such as the opponent's map view.
*/
@Override
protected void disableState() {
getApp().getGuiNode().detachChild(battleNode);
getApp().getInputManager().removeListener(clickListener);
if (opponentMapView != null) {
opponentMapView.unregister();
opponentMapView = null;
}
}
/**
* Initializes the GUI components used in the battle state.
* Creates the opponent's map view and adds a grid overlay to it.
*/
private void initializeGuiComponents() {
opponentMapView = new MapView(getGameLogic().getOpponentMap(), getApp());
opponentMapView.addGrid();
}
/**
* Adds the initialized GUI components to the battle node.
* Currently, it attaches the opponent's map view to the node.
*/
private void addGuiComponents() {
battleNode.attachChild(opponentMapView.getNode());
}
/**
* Lays out the GUI components within the window, positioning them appropriately.
* The opponent's map view is positioned based on the window's dimensions and a specified gap.
*/
private void layoutGuiComponents() {
final AppSettings s = getApp().getContext().getSettings();
final float mapWidth = opponentMapView.getWidth();
final float mapHeight = opponentMapView.getHeight();
final float windowWidth = s.getWidth();
final float windowHeight = s.getHeight();
opponentMapView.getNode().setLocalTranslation(windowWidth - mapWidth - GAP,
windowHeight - mapHeight - GAP,
DEPTH);
}
/**
* Handles click events in the battle interface. If the event indicates a click (not a release),
* it translates the cursor position to the model's coordinate system and triggers the game logic
* for interacting with the opponent's map.
*
* @param isPressed whether the mouse button is currently pressed (true) or released (false)
*/
private void click(boolean isPressed) {
if (!isPressed || showsDialog())
return;
final IntPoint cursorPos = opponentMapView.mouseToModel(getApp().getInputManager().getCursorPosition());
LOGGER.log(Level.DEBUG, "click: {0}", cursorPos); //NON-NLS
getGameLogic().clickOpponentMap(cursorPos);
}
}

View File

@@ -0,0 +1,173 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.input.controls.ActionListener;
import com.jme3.math.Vector2f;
import com.jme3.scene.Node;
import com.jme3.system.AppSettings;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Container;
import pp.battleship.client.BattleshipAppState;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import static pp.battleship.Resources.lookup;
import static pp.battleship.client.BattleshipApp.CLICK;
/**
* EditorState manages the editor mode in the Battleship game,
* allowing players to place and rotate ships.
*/
public class EditorAppState extends BattleshipAppState {
private static final Logger LOGGER = System.getLogger(EditorAppState.class.getName());
private static final float DEPTH = 0f;
private static final float GAP = 20f;
private final ActionListener clickListener = (name, isPressed, tpf) -> click(isPressed);
private final Node editorNode = new Node("Editor"); //NON-NLS
private Container buttonContainer;
private Button rotateButton;
private Button readyButton;
private MapView ownMapView;
private MapView harborView;
private Vector2f oldCursorPosition;
/**
* Enables the editor state by attaching necessary nodes and listeners.
*/
@Override
protected void enableState() {
editorNode.detachAllChildren();
initializeGuiComponents();
addGuiComponents();
layoutGuiComponents();
getApp().getGuiNode().attachChild(editorNode);
getApp().getInputManager().addListener(clickListener, CLICK);
}
/**
* Disables the editor state by detaching nodes and removing listeners.
*/
@Override
protected void disableState() {
getApp().getGuiNode().detachChild(editorNode);
getApp().getInputManager().removeListener(clickListener);
if (ownMapView != null) {
ownMapView.unregister();
ownMapView = null;
}
if (harborView != null) {
harborView.unregister();
harborView = null;
}
}
/**
* Updates the editor state, handling cursor movement and enabling buttons.
*
* @param tpf Time per frame
*/
@Override
public void update(float tpf) {
super.update(tpf);
cursorMovement();
enableButtons();
}
/**
* Enables or disables buttons based on the logic state.
*/
private void enableButtons() {
readyButton.setEnabled(getGameLogic().isMapComplete());
rotateButton.setEnabled(getGameLogic().movingShip());
}
/**
* Handles cursor movement for previewing ship placement.
*/
private void cursorMovement() {
if (!getGameLogic().movingShip() || ownMapView == null || showsDialog())
return;
final Vector2f cursorPosition = getApp().getInputManager().getCursorPosition();
if (!cursorPosition.equals(oldCursorPosition)) {
oldCursorPosition = new Vector2f(cursorPosition);
getGameLogic().movePreview(ownMapView.mouseToModel(cursorPosition));
}
}
/**
* Initializes the GUI components for the editor.
*/
private void initializeGuiComponents() {
ownMapView = new MapView(getGameLogic().getOwnMap(), getApp());
harborView = new MapView(getGameLogic().getHarbor(), getApp());
ownMapView.addGrid();
rotateButton = new Button(lookup("button.rotate"));
readyButton = new Button(lookup("button.ready"));
rotateButton.addClickCommands(e -> {
if (!showsDialog())
getGameLogic().rotateShip();
});
readyButton.addClickCommands(e -> {
if (!showsDialog())
getGameLogic().mapFinished();
});
buttonContainer = new Container();
}
/**
* Adds the GUI components to the editor node.
*/
private void addGuiComponents() {
buttonContainer.addChild(rotateButton, 0, 0);
buttonContainer.addChild(readyButton, 0, 1);
editorNode.attachChild(ownMapView.getNode());
editorNode.attachChild(harborView.getNode());
editorNode.attachChild(buttonContainer);
}
/**
* Lays out the GUI components on the screen.
*/
private void layoutGuiComponents() {
final AppSettings s = getApp().getContext().getSettings();
final float harborWidth = harborView.getWidth();
final float harborHeight = harborView.getHeight();
final float ownMapWidth = ownMapView.getWidth();
final float ownMapHeight = ownMapView.getHeight();
final float windowWidth = s.getWidth();
final float windowHeight = s.getHeight();
ownMapView.getNode()
.setLocalTranslation(0.5f * (windowWidth - harborWidth - ownMapWidth - GAP),
0.5f * (windowHeight - ownMapHeight),
DEPTH);
harborView.getNode()
.setLocalTranslation(0.5f * (windowWidth - harborWidth + ownMapWidth + GAP),
0.5f * (windowHeight - harborHeight),
DEPTH);
buttonContainer.setLocalTranslation(0.5f * (windowWidth - harborWidth - ownMapWidth - GAP),
0.5f * (windowHeight - ownMapHeight - GAP),
DEPTH);
}
/**
* Handles click events to place or rotate ships on the maps.
*/
private void click(boolean isPressed) {
if (!isPressed || showsDialog()) return;
final Vector2f cursorPos = getApp().getInputManager().getCursorPosition();
LOGGER.log(Level.DEBUG, "click: {0}", cursorPos); //NON-NLS
getGameLogic().clickHarbor(harborView.mouseToModel(cursorPos));
getGameLogic().clickOwnMap(ownMapView.mouseToModel(cursorPos));
}
}

View File

@@ -0,0 +1,191 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.shape.Quad;
import pp.battleship.client.BattleshipApp;
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.util.FloatPoint;
import pp.util.Position;
/**
* Represents the visual view of a {@link ShipMap}, used to display the map structure such as the player's map, harbor,
* and opponent's map. This class handles the graphical representation of the map, including background setup, grid lines,
* and interaction between the model and the view.
*/
class MapView {
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;
private static final ColorRGBA BACKGROUND_COLOR = new ColorRGBA(0, 0.05f, 0.05f, 0.5f);
private static final ColorRGBA GRID_COLOR = ColorRGBA.Green;
// Reference to the main application and the ship map being visualized
private final BattleshipApp app;
private final Node mapNode = new Node("map"); // NON-NLS
private final ShipMap map;
private final MapViewSynchronizer synchronizer;
/**
* Constructs a new MapView for a given {@link ShipMap} and {@link BattleshipApp}.
* Initializes the view by setting up the background and registering a synchronizer to listen to changes in the map.
*
* @param map the ship map to visualize
* @param app the main application instance
*/
MapView(ShipMap map, BattleshipApp app) {
this.map = map;
this.app = app;
this.synchronizer = new MapViewSynchronizer(this);
setupBackground();
app.getGameLogic().addListener(synchronizer);
}
/**
* Unregisters the {@link MapViewSynchronizer} from the listener list of the ClientGameLogic,
* stopping the view from receiving updates when the underlying {@link ShipMap} changes.
* After calling this method, this MapView instance should no longer be used.
*/
void unregister() {
app.getGameLogic().removeListener(synchronizer);
}
/**
* Gets the {@link ShipMap} associated with this view.
*
* @return the ship map
*/
public ShipMap getMap() {
return map;
}
/**
* Gets the {@link BattleshipApp} instance associated with this view.
*
* @return the main application instance
*/
public BattleshipApp getApp() {
return app;
}
/**
* Sets up the background of the map view using a quad geometry.
* The background is configured with a semi-transparent color and placed at a specific depth.
*/
private void setupBackground() {
final Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); // NON-NLS
mat.setColor("Color", BACKGROUND_COLOR); // NON-NLS
mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
final Position corner = modelToView(map.getWidth(), map.getHeight());
final Geometry background = new Geometry("MapBackground", new Quad(corner.getX(), corner.getY()));
background.setMaterial(mat);
background.setLocalTranslation(0f, 0f, BACKGROUND_DEPTH);
background.setCullHint(CullHint.Never);
mapNode.attachChild(background);
}
/**
* Adds grid lines to the map view to visually separate the fields within the map.
* The grid lines are drawn based on the dimensions of the ship map.
*/
public void addGrid() {
for (int x = 0; x <= map.getWidth(); x++) {
final Position f = modelToView(x, 0);
final Position t = modelToView(x, map.getHeight());
mapNode.attachChild(gridLine(f, t));
}
for (int y = 0; y <= map.getHeight(); y++) {
final Position f = modelToView(0, y);
final Position t = modelToView(map.getWidth(), y);
mapNode.attachChild(gridLine(f, t));
}
}
/**
* Gets the root node containing all visual elements in this map view.
*
* @return the root node for the map view
*/
public Node getNode() {
return mapNode;
}
/**
* Gets the total width of the map in view coordinates.
*
* @return the width of the map in view coordinates
*/
public float getWidth() {
return FIELD_SIZE * map.getWidth();
}
/**
* Gets the total height of the map in view coordinates.
*
* @return the height of the map in view coordinates
*/
public float getHeight() {
return FIELD_SIZE * map.getHeight();
}
/**
* Converts coordinates from view coordinates to model coordinates.
*
* @param x the x-coordinate in view space
* @param y the y-coordinate in view space
* @return the corresponding model coordinates as an {@link IntPoint}
*/
public IntPoint viewToModel(float x, float y) {
return new IntPoint((int) Math.floor(x / FIELD_SIZE), (int) Math.floor(y / FIELD_SIZE));
}
/**
* Converts coordinates from model coordinates to view coordinates.
*
* @param x the x-coordinate in model space
* @param y the y-coordinate in model space
* @return the corresponding view coordinates as a {@link Position}
*/
public Position modelToView(float x, float y) {
return new FloatPoint(x * FIELD_SIZE, y * FIELD_SIZE);
}
/**
* Converts the mouse position to model coordinates.
* This method takes into account the map's transformation in the 3D scene.
*
* @param pos the 2D vector representing the mouse position in the view
* @return the corresponding model coordinates as an {@link IntPoint}
*/
public IntPoint mouseToModel(Vector2f pos) {
final Vector3f world = new Vector3f(pos.getX(), pos.getY(), 0f);
final Vector3f view = mapNode.getWorldTransform().transformInverseVector(world, null);
return viewToModel(view.getX(), view.getY());
}
/**
* Creates a visual representation of a grid line between two positions.
*
* @param p1 the start position of the grid line
* @param p2 the end position of the grid line
* @return a {@link Geometry} representing the grid line
*/
private Geometry gridLine(Position p1, Position p2) {
return app.getDraw().makeFatLine(p1, p2, GRID_DEPTH, GRID_COLOR, GRID_LINE_WIDTH);
}
}

View File

@@ -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.gui;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import pp.battleship.model.Battleship;
import pp.battleship.model.Shell;
import pp.battleship.model.Shot;
import pp.util.Position;
/**
* Synchronizes the visual representation of the ship map with the game model.
* It handles the rendering of ships and shots on the map view, updating the view
* whenever changes occur in the model.
*/
class MapViewSynchronizer extends ShipMapSynchronizer {
// Constants for rendering properties
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 INDENT = 4f;
// Colors used for different visual elements
private static final ColorRGBA HIT_COLOR = ColorRGBA.Red;
private static final ColorRGBA MISS_COLOR = ColorRGBA.Blue;
private static final ColorRGBA SHIP_BORDER_COLOR = ColorRGBA.White;
private static final ColorRGBA PREVIEW_COLOR = ColorRGBA.Gray;
private static final ColorRGBA ERROR_COLOR = ColorRGBA.Red;
// The MapView associated with this synchronizer
private final MapView view;
private Shell shell;
/**
* Constructs a new MapViewSynchronizer for the given MapView.
* Initializes the synchronizer and adds existing elements from the model to the view.
*
* @param view the MapView to synchronize with the game model
*/
public MapViewSynchronizer(MapView view) {
super(view.getMap(), view.getNode());
this.view = view;
addExisting();
}
/**
* Creates a visual representation of a shot on the map.
* A hit shot is represented in red, while a miss is represented in blue.
*
* @param shot the Shot object representing the shot in the model
* @return a Spatial representing the shot on the map
*/
@Override
public Spatial visit(Shot shot) {
// Convert the shot's model coordinates to view coordinates
final Position p1 = view.modelToView(shot.getX(), shot.getY());
final Position p2 = view.modelToView(shot.getX() + 1, shot.getY() + 1);
final ColorRGBA color = shot.isHit() ? HIT_COLOR : MISS_COLOR;
// 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);
}
/**
* Creates a visual representation of a battleship on the map.
* The ship's border color depends on its status: normal, valid preview, or invalid preview.
*
* @param ship the Battleship object representing the ship in the model
* @return a Spatial representing the ship on the map
*/
@Override
public Spatial visit(Battleship ship) {
// Create a node to represent the ship
final Node shipNode = new Node("ship"); //NON-NLS
// Convert the ship's model coordinates to view coordinates
final Position p1 = view.modelToView(ship.getMinX(), ship.getMinY());
final Position p2 = view.modelToView(ship.getMaxX() + 1, ship.getMaxY() + 1);
// Calculate the coordinates for the ship's bounding box
final float x1 = p1.getX() + INDENT;
final float y1 = p1.getY() + INDENT;
final float x2 = p2.getX() - INDENT;
final float y2 = p2.getY() - INDENT;
// Determine the color based on the ship's status
final ColorRGBA color = switch (ship.getStatus()) {
case NORMAL -> SHIP_BORDER_COLOR;
case VALID_PREVIEW -> PREVIEW_COLOR;
case INVALID_PREVIEW -> ERROR_COLOR;
};
// Add the ship's borders to the node
shipNode.attachChild(shipLine(x1, y1, x2, y1, color));
shipNode.attachChild(shipLine(x1, y2, x2, y2, color));
shipNode.attachChild(shipLine(x1, y1, x1, y2, color));
shipNode.attachChild(shipLine(x2, y1, x2, y2, color));
// Return the complete ship representation
return shipNode;
}
/**
* Creates a line geometry representing part of the ship's border.
*
* @param x1 the starting x-coordinate of the line
* @param y1 the starting y-coordinate of the line
* @param x2 the ending x-coordinate of the line
* @param y2 the ending y-coordinate of the line
* @param color the color of the line
* @return a Geometry representing the line
*/
private Geometry shipLine(float x1, float y1, float x2, float y2, ColorRGBA color) {
return view.getApp().getDraw().makeFatLine(x1, y1, x2, y2, SHIP_DEPTH, color, SHIP_LINE_WIDTH);
}
/**
*
* @param deltaTime
*/
public void update(float deltaTime) {
if (shell != null) {
shell.updatePosition(deltaTime);
drawShell(shell.getCurrentPosition());
}
}
private void drawShell(Vector3f position) {
// Methode zum Zeichnen des Geschosses auf der 2D-Karte TODO
}
}

View File

@@ -0,0 +1,203 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.app.Application;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetManager;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
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.shadow.DirectionalLightShadowRenderer;
import com.jme3.shadow.EdgeFilteringMode;
import com.jme3.texture.Texture;
import com.jme3.util.SkyFactory;
import com.jme3.util.TangentBinormalGenerator;
import pp.battleship.client.BattleshipAppState;
import pp.battleship.model.ShipMap;
import pp.util.FloatMath;
import static pp.util.FloatMath.TWO_PI;
import static pp.util.FloatMath.cos;
import static pp.util.FloatMath.sin;
import static pp.util.FloatMath.sqrt;
/**
* Manages the rendering and visual aspects of the sea and sky in the Battleship game.
* This state is responsible for setting up and updating the sea, sky, and lighting
* conditions, and controls the camera to create a dynamic view of the game environment.
*/
public class SeaAppState extends BattleshipAppState {
/**
* The path to the sea texture material.
*/
private static final String SEA_TEXTURE = "Textures/Terrain/Water/Water.j3m"; //NON-NLS
private static final float ABOVE_SEA_LEVEL = 4f;
private static final float INCLINATION = 2.5f;
/**
* The root node for all visual elements in this state.
*/
private final Node viewNode = new Node("view"); //NON-NLS
/**
* The node containing the scene elements, such as the sea surface.
*/
private final Node sceneNode = new Node("scene"); //NON-NLS
/**
* Synchronizes the sea's visual representation with the game logic.
*/
private SeaSynchronizer synchronizer;
/**
* The current angle of the camera around the center of the map.
*/
private float cameraAngle;
/**
* Initializes the state by setting up the sky, lights, and other visual components.
* This method is called when the state is first attached to the state manager.
*
* @param stateManager the state manager
* @param application the application
*/
@Override
public void initialize(AppStateManager stateManager, Application application) {
super.initialize(stateManager, application);
viewNode.attachChild(sceneNode);
setupLights();
setupSky();
}
/**
* Enables the sea and sky state, setting up the scene and registering any necessary listeners.
* This method is called when the state is set to active.
*/
@Override
protected void enableState() {
sceneNode.detachAllChildren();
setupScene();
if (synchronizer == null) {
synchronizer = new SeaSynchronizer(getApp(), sceneNode, getGameLogic().getOwnMap());
getGameLogic().addListener(synchronizer);
}
getApp().getRootNode().attachChild(viewNode);
}
/**
* Disables the sea and sky state, removing visual elements from the scene and unregistering listeners.
* This method is called when the state is set to inactive.
*/
@Override
protected void disableState() {
getApp().getRootNode().detachChild(viewNode);
if (synchronizer != null) {
getGameLogic().removeListener(synchronizer);
synchronizer = null;
}
}
/**
* Updates the state each frame, moving the camera to simulate it circling around the map.
*
* @param tpf the time per frame (seconds)
*/
@Override
public void update(float tpf) {
super.update(tpf);
cameraAngle += TWO_PI * 0.05f * tpf;
adjustCamera();
}
/**
* Sets up the lighting for the scene, including directional and ambient lights.
* Also configures shadows to enhance the visual depth of the scene.
*/
private void setupLights() {
final AssetManager assetManager = getApp().getAssetManager();
final DirectionalLightShadowRenderer shRend = new DirectionalLightShadowRenderer(assetManager, 2048, 3);
shRend.setLambda(0.55f);
shRend.setShadowIntensity(0.6f);
shRend.setEdgeFilteringMode(EdgeFilteringMode.Bilinear);
getApp().getViewPort().addProcessor(shRend);
final DirectionalLight sun = new DirectionalLight();
sun.setDirection(new Vector3f(-1f, -0.7f, -1f).normalizeLocal());
viewNode.addLight(sun);
shRend.setLight(sun);
final AmbientLight ambientLight = new AmbientLight(new ColorRGBA(0.3f, 0.3f, 0.3f, 0f));
viewNode.addLight(ambientLight);
}
/**
* Sets up the sky in the scene using a skybox with textures for all six directions.
* This creates a realistic and immersive environment for the sea.
*/
private void setupSky() {
final AssetManager assetManager = getApp().getAssetManager();
final Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg"); //NON-NLS
final Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg"); //NON-NLS
final Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg"); //NON-NLS
final Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg"); //NON-NLS
final Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg"); //NON-NLS
final Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg"); //NON-NLS
final Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down);
sky.rotate(0f, FloatMath.PI, 0f);
viewNode.attachChild(sky);
}
/**
* Sets up the sea surface in the scene. This includes creating the sea mesh,
* applying textures, and enabling shadows.
*/
private void setupScene() {
final ShipMap ownMap = getGameLogic().getOwnMap();
final float x = 0.5f * ownMap.getWidth();
final float y = 0.5f * ownMap.getHeight();
final Box seaMesh = new Box(y + 0.5f, 0.1f, x + 0.5f);
final Geometry seaGeo = new Geometry("sea", seaMesh); //NON-NLS
seaGeo.setLocalTranslation(new Vector3f(y, -0.1f, x));
seaMesh.scaleTextureCoordinates(new Vector2f(4f, 4f));
final Material seaMat = getApp().getAssetManager().loadMaterial(SEA_TEXTURE);
seaGeo.setMaterial(seaMat);
seaGeo.setShadowMode(ShadowMode.CastAndReceive);
TangentBinormalGenerator.generate(seaGeo);
sceneNode.attachChild(seaGeo);
}
/**
* Adjusts the camera position and orientation to create a circular motion around
* the center of the map. This provides a dynamic view of the sea and surrounding environment.
*/
private void adjustCamera() {
final ShipMap ownMap = getGameLogic().getOwnMap();
final float mx = 0.5f * ownMap.getWidth();
final float my = 0.5f * ownMap.getHeight();
final float radius = 2f * sqrt(mx * mx + my + my);
final float cos = radius * cos(cameraAngle);
final float sin = radius * sin(cameraAngle);
final float x = mx - cos;
final float y = my - sin;
final Camera camera = getApp().getCamera();
camera.setLocation(new Vector3f(y, ABOVE_SEA_LEVEL, x));
camera.getRotation().lookAt(new Vector3f(sin, -INCLINATION, cos),
Vector3f.UNIT_Y);
camera.update();
}
}

View File

@@ -0,0 +1,334 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
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.ShipMap;
import pp.battleship.model.Shot;
import static java.util.Objects.requireNonNull;
import static pp.util.FloatMath.HALF_PI;
import static pp.util.FloatMath.PI;
/**
* The {@code SeaSynchronizer} class is responsible for synchronizing the graphical
* representation of the ships and shots on the sea map with the underlying data model.
* It extends the {@link ShipMapSynchronizer} to provide specific synchronization
* logic for the sea map.
*/
//ANPASSEN!!!!
class SeaSynchronizer extends ShipMapSynchronizer {
private static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md"; //NON-NLS
private static final String KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o"; //NON-NLS
private static final String FLUGZEUGTRAEGER = "Models/Flugzeugträger_5er/Flugzeugträger.j3o";
private static final String KORVETTE = "Models/Korvette_3er/Korvette.j3o";
private static final String ZERSTOERER = "Models/Zerstörer_2er/Zerstörer.j3o";
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 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;
private final ParticleEffectFactory particleFactory;
/**
* Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map.
*
* @param app the Battleship application
* @param root the root node to which graphical elements will be attached
* @param map the ship map containing the ships and shots
*/
public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) {
super(app.getGameLogic().getOwnMap(), root);
this.app = app;
this.map = map;
this.particleFactory = new ParticleEffectFactory(app);
addExisting();
}
/**
* Visits a {@link Shot} and creates a graphical representation of it.
* If the shot is a hit, it attaches the representation to the ship node.
*
* @param shot the shot to be represented
* @return the graphical representation of the shot, or null if the shot is a hit
* and the representation has been attached to the ship node
*/
@Override
public Spatial visit(Shot shot) {
return shot.isHit() ? handleHit(shot) : createCylinder(shot);
}
/**
* 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.
*
* @param shot a hit
* @return always null to prevent the representation from being attached
* to the items node as well
*/
private Spatial handleMiss(Shot shot) {
Node shotNode = new Node("ShotNode");
Geometry shotCylinder = createCylinder(shot);
shotNode.attachChild(shotCylinder);
ParticleEmitter waterSplash = particleFactory.createWaterSplash();
waterSplash.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
shotNode.attachChild(waterSplash);
waterSplash.emitAllParticles();
return shotNode;
}
* @param shot a hit
* @return always null to prevent the representation from being attached to the items node as well
*/
private Spatial handleHit(Shot shot) {
final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship");
final Node shipNode = requireNonNull((Node) getSpatial(ship), "Missing ship node");
// Create a new node specifically for the hit effects
Node hitEffectNode = new Node("HitEffectNode");
// Create particle effects
ParticleEmitter flame = particleFactory.createFlame();
ParticleEmitter flash = particleFactory.createFlash();
ParticleEmitter spark = particleFactory.createSpark();
ParticleEmitter roundSpark = particleFactory.createRoundSpark();
ParticleEmitter smokeTrail = particleFactory.createSmokeTrail();
ParticleEmitter debris = particleFactory.createDebris();
ParticleEmitter shockwave = particleFactory.createShockwave();
ParticleEmitter movingSmoke = particleFactory.createMovingSmokeEmitter();
// Attach all effects to the hitEffectNode
hitEffectNode.attachChild(flame);
hitEffectNode.attachChild(flash);
hitEffectNode.attachChild(spark);
hitEffectNode.attachChild(roundSpark);
hitEffectNode.attachChild(smokeTrail);
hitEffectNode.attachChild(debris);
hitEffectNode.attachChild(shockwave);
hitEffectNode.attachChild(movingSmoke);
// Set the local translation for the hit effect to the point of impact
hitEffectNode.setLocalTranslation(shot.getY() + 0.5f - shipNode.getLocalTranslation().x,
0.5f, // Adjust as needed for height above the ship
shot.getX() + 0.5f - shipNode.getLocalTranslation().z);
// Attach the hitEffectNode to the shipNode so it moves with the ship
shipNode.attachChild(hitEffectNode);
// Emit particles when the hit happens
flash.emitAllParticles();
spark.emitAllParticles();
smokeTrail.emitAllParticles();
debris.emitAllParticles();
shockwave.emitAllParticles();
flame.emitAllParticles();
roundSpark.emitAllParticles();
//Checks if ship is destroyed and triggers animation accordingly
if (ship.isDestroyed()) {
sinkAndRemoveShip(ship);
}
return null;
}
/**
* Handles the sinking animation and removal of ship if destroyed
* @param ship the ship to be sunk
*/
private void sinkAndRemoveShip(Battleship ship) {
final Node shipNode = (Node) getSpatial(ship);
if (shipNode == null) return;
// Add sinking control to animate the sinking
shipNode.addControl(new SinkingControl(shipNode));
// Add particle effects
ParticleEmitter bubbles = particleFactory.createWaterSplash();
bubbles.setLocalTranslation(shipNode.getLocalTranslation());
shipNode.attachChild(bubbles);
bubbles.emitAllParticles();
}
/**
* Creates a cylinder geometry representing the specified shot.
* The appearance of the cylinder depends on whether the shot is a hit or a miss.
*
* @param shot the shot to be represented
* @return the geometry representing the shot
*/
private Geometry createCylinder(Shot shot) {
final ColorRGBA color = shot.isHit() ? HIT_COLOR : SPLASH_COLOR;
final float height = shot.isHit() ? 1.2f : 0.1f;
final Cylinder cylinder = new Cylinder(2, 20, 0.45f, height, true);
final Geometry geometry = new Geometry(SHOT, cylinder);
geometry.setMaterial(createColoredMaterial(color));
geometry.rotate(HALF_PI, 0f, 0f);
// compute the center of the shot in world coordinates
geometry.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
return geometry;
}
/**
* Visits a {@link Battleship} and creates a graphical representation of it.
* The representation is either a 3D model or a simple box depending on the
* type of battleship.
*
* @param ship the battleship to be represented
* @return the node containing the graphical representation of the battleship
*/
@Override
public Spatial visit(Battleship ship) {
final Node node = new Node(SHIP);
node.attachChild(createShip(ship));
// compute the center of the ship in world coordinates
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));
return node;
}
/**
* 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.
*
* @param ship the battleship to be represented
* @return the spatial representing the battleship
*/
//ANPASSEN!!!!!!
private Spatial createShip(Battleship ship) {
if( ship.getLength() == 4){ createBox(ship); return createBattleship(ship); }
else if(ship.getLength()==5){createBox(ship);return createFlugzeugtraeger(ship); }
else if(ship.getLength()==3){createBox(ship);return createKorvette(ship); }
else if(ship.getLength()==2){createBox(ship);return createZerstoerer(ship); }
else if(ship.getLength()==1){createBox(ship);return createZerstoerer(ship); }
else throw new IllegalArgumentException("Deine Länge ist nicht definiert!");
}
/**
* Creates a simple box to represent a battleship that is not of the "King George V" type.
*
* @param ship the battleship to be represented
* @return the geometry representing the battleship as a box
*/
private Spatial createBox(Battleship ship) {
final Box box = new Box(0.5f * (ship.getMaxY() - ship.getMinY()) + 0.3f,
0.3f,
0.5f * (ship.getMaxX() - ship.getMinX()) + 0.3f);
final Geometry geometry = new Geometry(SHIP, box);
geometry.setMaterial(createColoredMaterial(BOX_COLOR));
geometry.setShadowMode(ShadowMode.CastAndReceive);
return geometry;
}
/**
* Creates a new {@link Material} with the specified color.
* If the color includes transparency (i.e., alpha value less than 1),
* the material's render state is set to use alpha blending, allowing for
* semi-transparent rendering.
*
* @param color the {@link ColorRGBA} to be applied to the material. If the alpha value
* of the color is less than 1, the material will support transparency.
* @return a {@link Material} instance configured with the specified color and,
* if necessary, alpha blending enabled.
*/
private Material createColoredMaterial(ColorRGBA color) {
final Material material = new Material(app.getAssetManager(), UNSHADED);
if (color.getAlpha() < 1f)
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
material.setColor(COLOR, color);
return material;
}
/**
* Creates a detailed 3D model to represent a "King George V" battleship.
*
* @param ship the battleship to be represented
* @return the spatial representing the "King George V" battleship
*/
//ANPASSEN!!!!
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(1.48f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
private Spatial createFlugzeugtraeger(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(FLUGZEUGTRAEGER);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(1.48f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
private Spatial createKorvette(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(KORVETTE);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(1.48f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
private Spatial createZerstoerer(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(ZERSTOERER);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(1.48f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/**
* Calculates the rotation angle for the specified rotation.
*
* @param rot the rotation of the battleship
* @return the rotation angle in radians
*/
private static float calculateRotationAngle(Rotation rot) {
return switch (rot) {
case RIGHT -> HALF_PI;
case DOWN -> 0f;
case LEFT -> -HALF_PI;
case UP -> PI;
};
}
}

View File

@@ -0,0 +1,106 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.math.Quaternion;
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.Battleship;
import static pp.util.FloatMath.DEG_TO_RAD;
import static pp.util.FloatMath.TWO_PI;
import static pp.util.FloatMath.sin;
/**
* 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 {
/**
* The axis of rotation for the ship's pitch (tilting forward and backward).
*/
private final Vector3f axis;
/**
* The duration of one complete oscillation cycle in seconds.
*/
private final float cycle;
/**
* The amplitude of the pitch oscillation in radians, determining how much the ship tilts.
*/
private final float amplitude;
/**
* A quaternion representing the ship's current pitch rotation.
*/
private final Quaternion pitch = new Quaternion();
/**
* The current time within the oscillation cycle, used to calculate the ship's pitch angle.
*/
private float time;
/**
* 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 ship the Battleship object to control
*/
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 = ship.getLength() * 2f;
amplitude = 5f * DEG_TO_RAD / ship.getLength();
}
/**
* Updates the ship's pitch oscillation each frame. The ship's pitch is adjusted
* to create a continuous tilting motion, simulating the effect of waves.
*
* @param tpf time per frame (in seconds), used to calculate the new pitch angle
*/
@Override
protected void controlUpdate(float tpf) {
// If spatial is null, do nothing
if (spatial == null) return;
// Update the time within the oscillation cycle
time = (time + tpf) % cycle;
// Calculate the current angle of the oscillation
final float angle = amplitude * sin(time * TWO_PI / cycle);
// Update the pitch Quaternion with the new angle
pitch.fromAngleAxis(angle, axis);
// Apply the pitch rotation to the spatial
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 logic is needed for this control
}
}

View File

@@ -0,0 +1,89 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.client.gui;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import pp.battleship.model.Item;
import pp.battleship.model.ShipMap;
import pp.battleship.model.Visitor;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.ItemAddedEvent;
import pp.battleship.notification.ItemRemovedEvent;
import pp.view.ModelViewSynchronizer;
/**
* Abstract base class for synchronizing the visual representation of a {@link ShipMap} with its model state.
* This class handles the addition and removal of items from the ship map, ensuring that changes in the model
* are accurately reflected in the view.
* <p>
* Subclasses are responsible for providing the specific implementation of how each item in the map
* is represented visually by implementing the {@link Visitor} interface.
* </p>
*/
abstract class ShipMapSynchronizer extends ModelViewSynchronizer<Item> implements Visitor<Spatial>, GameEventListener {
// The ship map that this synchronizer is responsible for
private final ShipMap shipMap;
/**
* Constructs a new ShipMapSynchronizer.
* Initializes the synchronizer with the provided ship map and the root node for attaching view representations.
*
* @param map the ship map to be synchronized
* @param root the root node to which the view representations of the ship map items are attached
*/
protected ShipMapSynchronizer(ShipMap map, Node root) {
super(root);
this.shipMap = map;
}
/**
* Translates a model item into its corresponding visual representation.
* The specific visual representation is determined by the concrete implementation of the {@link Visitor} interface.
*
* @param item the item from the model to be translated
* @return the visual representation of the item as a {@link Spatial}
*/
@Override
protected Spatial translate(Item item) {
return item.accept(this);
}
/**
* Adds the existing items from the ship map to the view.
* This method should be called during initialization to ensure that all current items in the ship map
* are visually represented.
*/
protected void addExisting() {
shipMap.getItems().forEach(this::add);
}
/**
* Handles the event when an item is removed from the ship map.
* Removes the visual representation of the item from the view if it belongs to the synchronized ship map.
*
* @param event the event indicating that an item has been removed from the ship map
*/
@Override
public void receivedEvent(ItemRemovedEvent event) {
if (shipMap == event.map())
delete(event.item());
}
/**
* Handles the event when an item is added to the ship map.
* Adds the visual representation of the new item to the view if it belongs to the synchronized ship map.
*
* @param event the event indicating that an item has been added to the ship map
*/
@Override
public void receivedEvent(ItemAddedEvent event) {
if (shipMap == event.map())
add(event.item());
}
}

View File

@@ -0,0 +1,53 @@
package pp.battleship.client.gui;
import com.jme3.scene.control.AbstractControl;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
/**
* Control that handles the sinking effect for destroyed ships.
* It will gradually move the ship downwards and then remove it from the scene.
*/
class SinkingControl extends AbstractControl {
private static final float SINK_DURATION = 5f; // Duration of the sinking animation
private static final float SINK_SPEED = 0.1f; // Speed at which the ship sinks
private float elapsedTime = 0;
private final Node shipNode;
/**
* Constructs a {@code SinkingControl.java} object with the shipNode to be to be sunk
* @param shipNode the node to handeld
*/
public SinkingControl(Node shipNode) {
this.shipNode = shipNode;
}
/**
* Updated the Map to sink the ship
*
* @param tpf time per frame
*/
@Override
protected void controlUpdate(float tpf) {
// Update the sinking effect
elapsedTime += tpf;
// Move the ship down over time
Vector3f currentPos = shipNode.getLocalTranslation();
shipNode.setLocalTranslation(currentPos.x, currentPos.y - SINK_SPEED * tpf, currentPos.z);
// Check if sinking duration has passed
if (elapsedTime >= SINK_DURATION) {
// Remove the ship from the scene
shipNode.removeFromParent();
}
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering-related code needed
}
}

View File

@@ -0,0 +1,29 @@
package pp.battleship.client.gui;
import com.simsilica.lemur.Slider;
import pp.battleship.client.GameMusic;
public class VolumeSlider extends Slider {
private final GameMusic piratenMusic;
private double vol;
public VolumeSlider(GameMusic music) {
super();
this.piratenMusic = music;
vol = GameMusic.volumePreference();
getModel().setPercent(vol);
}
public void update() {
if (vol != getModel().getPercent()) {
vol = getModel().getPercent();
piratenMusic.setVolume( (float) vol);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

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

@@ -0,0 +1,4 @@
based on:
water 002
by Katsukagi on 29/11/2018
https://3dtextures.me/2018/11/29/water-002/

View File

@@ -0,0 +1,14 @@
Material Water : Common/MatDefs/Light/Lighting.j3md {
MaterialParameters {
Shininess : 64
DiffuseMap : Repeat Textures/Terrain/Water/Water_002_COLOR.jpg
NormalMap : Repeat Textures/Terrain/Water/Water_002_NORM.jpg
SpecularMap : Repeat Textures/Terrain/Water/Water_002_ROUGH.jpg
ParallaxMap : Repeat Textures/Terrain/Water/Water_002_DISP.png
// PackedNormalParallax : true
// UseMaterialColors : true
Ambient : 0.5 0.5 0.5 1.0
Diffuse : 1.0 1.0 1.0 1.0
Specular : 1.0 1.0 1.0 1.0
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,16 @@
plugins {
id 'buildlogic.jme-application-conventions'
}
description = 'Battleship converter for resources'
dependencies {
implementation libs.jme3.core
runtimeOnly libs.jme3.desktop
runtimeOnly libs.jme3.plugins
}
application {
mainClass = 'pp.battleship.exporter.ModelExporter'
}

View File

@@ -0,0 +1,72 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.battleship.exporter;
import com.jme3.app.SimpleApplication;
import com.jme3.export.JmeExporter;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.scene.Spatial;
import com.jme3.system.JmeContext;
import com.jme3.util.TangentBinormalGenerator;
import java.io.File;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
/**
* This class transforms models into j3o format.
*/
public class ModelExporter extends SimpleApplication {
private static final Logger LOGGER = System.getLogger(ModelExporter.class.getName());
/**
* The main method of the converter
*
* @param args input args
*/
public static void main(String[] args) {
ModelExporter application = new ModelExporter();
application.start(JmeContext.Type.Headless);
}
/**
* Overrides {@link com.jme3.app.SimpleApplication#simpleInitApp()}.
* It initializes a simple app by exporting robots and rocks.
*/
@Override
public void simpleInitApp() {
export("Models/KingGeorgeV/King_George_V.obj", "KingGeorgeV.j3o"); //NON-NLS
export("Models/Flugzeugträger_5er/Flugzeugträger.obj", "Flugzeugträger.j3o");
export("Models/Korvette_3er/caravel.obj", "Korvette.j3o");
export("Models/Zerstörer_2er/ShipV_LOW.obj", "Zerstörer.j3o");
stop();
}
/**
* Exports spatial into a file
*
* @param fileName the file name of the model
* @param exportFileName the name of the file where the .j3o file is going to be stored
*/
private void export(String fileName, String exportFileName) {
final File file = new File(exportFileName);
JmeExporter exporter = BinaryExporter.getInstance();
try {
final Spatial model = getAssetManager().loadModel(fileName);
TangentBinormalGenerator.generate(model);
exporter.save(model, file);
}
catch (IOException exception) {
LOGGER.log(Level.ERROR, "write to {0} failed", file); //NON-NLS
throw new RuntimeException(exception);
}
LOGGER.log(Level.INFO, "wrote file {0}", file); //NON-NLS
}
}

View File

@@ -0,0 +1,112 @@
# Blender 4.2.2 LTS MTL File: 'None'
# www.blender.org
newmtl Main.002
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.490000 0.257495 0.132300
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.016
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.000000 0.000000 0.000000
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.017
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.190000 0.077615 0.017100
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.018
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.266600 0.466860 0.620000
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.019
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.990000 0.990000 0.990000
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.020
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.070000 0.062265 0.058100
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.021
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.160000 0.151680 0.147200
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.022
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.340000 0.164333 0.000000
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.023
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.940000 0.561180 0.206800
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Mat.1_1.002
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.320000 0.151520 0.060800
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2
newmtl Sail.002
Ns 319.999939
Ka 1.000000 1.000000 1.000000
Kd 0.070000 0.070000 0.070000
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 2

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

@@ -0,0 +1,52 @@
# Blender 4.2.2 LTS MTL File: 'None'
# www.blender.org
newmtl DeckStuff.003
Ns 20.000006
Ka 1.000000 1.000000 1.000000
Kd 0.184314 0.600000 0.792157
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 1
newmtl Hull.003
Ns 20.000006
Ka 1.000000 1.000000 1.000000
Kd 0.701961 0.678431 0.019608
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 1
newmtl MastsRigging.003
Ns 20.000006
Ka 1.000000 1.000000 1.000000
Kd 0.086275 0.768628 0.117647
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 1
newmtl Poopdeck.003
Ns 20.000006
Ka 1.000000 1.000000 1.000000
Kd 0.976471 0.211765 0.211765
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 1
newmtl Sail.007
Ns 20.000006
Ka 1.000000 1.000000 1.000000
Kd 0.588000 0.588000 0.588000
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.500000
d 1.000000
illum 1

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

@@ -0,0 +1,32 @@
# Blender 4.2.2 LTS MTL File: 'None'
# www.blender.org
newmtl map_ShipV_001
Ns 224.999985
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.144874 0.000708
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl map_ShipV_002
Ns 224.999985
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.035888 0.004678
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl map_ShipV_003
Ns 224.999985
Ka 1.000000 1.000000 1.000000
Kd 0.001405 0.006690 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

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