Compare commits
1 Commits
client_log
...
revert-71a
Author | SHA1 | Date | |
---|---|---|---|
|
9abf7350e4 |
26
.gitignore
vendored
@@ -1,26 +0,0 @@
|
|||||||
# 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
@@ -1 +0,0 @@
|
|||||||
#
|
|
3
Projekte/.gitattributes
vendored
@@ -1,3 +0,0 @@
|
|||||||
*.bat text eol=crlf
|
|
||||||
gradlew text eol=lf
|
|
||||||
|
|
@@ -1,18 +0,0 @@
|
|||||||
<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>
|
|
@@ -1,18 +0,0 @@
|
|||||||
<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>
|
|
@@ -1,17 +0,0 @@
|
|||||||
<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>
|
|
@@ -1,24 +0,0 @@
|
|||||||
<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>
|
|
@@ -1,189 +0,0 @@
|
|||||||
# 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
|
|
@@ -1,23 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'buildlogic.jme-application-conventions'
|
|
||||||
}
|
|
||||||
|
|
||||||
description = 'Battleship Client'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation project(":jme-common")
|
|
||||||
implementation project(":battleship:model")
|
|
||||||
|
|
||||||
implementation libs.jme3.desktop
|
|
||||||
implementation libs.jme3.effects
|
|
||||||
|
|
||||||
runtimeOnly libs.jme3.awt.dialogs
|
|
||||||
runtimeOnly libs.jme3.plugins
|
|
||||||
runtimeOnly libs.jme3.jogg
|
|
||||||
runtimeOnly libs.jme3.testdata
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
|
||||||
mainClass = 'pp.battleship.client.BattleshipApp'
|
|
||||||
applicationName = 'battleship'
|
|
||||||
}
|
|
@@ -1,73 +0,0 @@
|
|||||||
########################################
|
|
||||||
## 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
|
|
@@ -1,8 +0,0 @@
|
|||||||
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
|
|
@@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
"width": 10,
|
|
||||||
"height": 10,
|
|
||||||
"ships": [
|
|
||||||
{
|
|
||||||
"length": 4,
|
|
||||||
"x": 2,
|
|
||||||
"y": 8,
|
|
||||||
"rot": "RIGHT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"length": 3,
|
|
||||||
"x": 2,
|
|
||||||
"y": 5,
|
|
||||||
"rot": "DOWN"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"length": 3,
|
|
||||||
"x": 5,
|
|
||||||
"y": 6,
|
|
||||||
"rot": "RIGHT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"length": 2,
|
|
||||||
"x": 4,
|
|
||||||
"y": 4,
|
|
||||||
"rot": "RIGHT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"length": 2,
|
|
||||||
"x": 7,
|
|
||||||
"y": 4,
|
|
||||||
"rot": "RIGHT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"length": 2,
|
|
||||||
"x": 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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@@ -1,440 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client;
|
|
||||||
|
|
||||||
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.GameMusic;
|
|
||||||
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();
|
|
||||||
attachGameMusic();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches the background music state and sets its initial enabled state.
|
|
||||||
*/
|
|
||||||
private void attachGameMusic() {
|
|
||||||
final GameMusic gameSound = new GameMusic();
|
|
||||||
gameSound.setEnabled(GameMusic.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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,196 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,102 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
@@ -1,152 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client;
|
|
||||||
|
|
||||||
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;
|
|
||||||
private AudioNode shellFlyingSound;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
shellFlyingSound = loadSound(app, "Sound/Effects/shell_flying.wav");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays the shell flying sound effect.
|
|
||||||
*/
|
|
||||||
public void shellFly() {
|
|
||||||
if (isEnabled() && shellFlyingSound != null) {
|
|
||||||
shellFlyingSound.playInstance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a recieved {@code SoundEvent} and plays the according sound.
|
|
||||||
*
|
|
||||||
* @param event the Sound event to be processed
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void receivedEvent(SoundEvent event) {
|
|
||||||
switch (event.sound()) {
|
|
||||||
case EXPLOSION -> explosion();
|
|
||||||
case SPLASH -> splash();
|
|
||||||
case DESTROYED_SHIP -> shipDestroyed();
|
|
||||||
case SHELL_FLYING -> shellFly();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,154 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client;
|
|
||||||
|
|
||||||
import com.simsilica.lemur.Button;
|
|
||||||
import com.simsilica.lemur.Checkbox;
|
|
||||||
import com.simsilica.lemur.Label;
|
|
||||||
import com.simsilica.lemur.style.ElementId;
|
|
||||||
|
|
||||||
import pp.battleship.client.gui.GameMusic;
|
|
||||||
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 slider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs the Menu dialog for the Battleship application.
|
|
||||||
*
|
|
||||||
* @param app the BattleshipApp instance
|
|
||||||
*/
|
|
||||||
public Menu(BattleshipApp app) {
|
|
||||||
super(app.getDialogManager());
|
|
||||||
this.app = app;
|
|
||||||
slider = new VolumeSlider(app.getStateManager().getState(GameMusic.class));
|
|
||||||
addChild(new Label(lookup("battleship.name"), new ElementId("header"))); //NON-NLS
|
|
||||||
addChild(new Checkbox(lookup("menu.sound-enabled"), new StateCheckboxModel(app, GameSound.class)));
|
|
||||||
|
|
||||||
addChild(new Checkbox(lookup("menu.background-sound-enabled"), new StateCheckboxModel(app, GameMusic.class)));
|
|
||||||
|
|
||||||
addChild(slider);
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void update(float delta) {
|
|
||||||
slider.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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"));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,178 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client;
|
|
||||||
|
|
||||||
import com.simsilica.lemur.Button;
|
|
||||||
import com.simsilica.lemur.Container;
|
|
||||||
import com.simsilica.lemur.Label;
|
|
||||||
import com.simsilica.lemur.TextField;
|
|
||||||
import com.simsilica.lemur.component.SpringGridLayout;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,152 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,122 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client.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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,173 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client.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));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,122 +0,0 @@
|
|||||||
package pp.battleship.client.gui;
|
|
||||||
|
|
||||||
import static pp.util.PreferencesUtils.getPreferences;
|
|
||||||
|
|
||||||
import java.lang.System.Logger;
|
|
||||||
import java.lang.System.Logger.Level;
|
|
||||||
import java.util.prefs.Preferences;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the background music beeing played. Is able to start and stop the music. Set the Volume of the Audio.
|
|
||||||
*/
|
|
||||||
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 music;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if sound is enabled in the preferences.
|
|
||||||
*
|
|
||||||
* @return float to which the volume is set
|
|
||||||
*/
|
|
||||||
public static float volumeInPreferences() {
|
|
||||||
return PREFERENCES.getFloat(VOLUME_PREF, 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
music = loadSound(app, "Sound/background.ogg");
|
|
||||||
setVolume(volumeInPreferences());
|
|
||||||
music.setLooping(true);
|
|
||||||
if (isEnabled() && music != null) {
|
|
||||||
music.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
if (music != null) {
|
|
||||||
if (enabled) {
|
|
||||||
music.play();
|
|
||||||
} else {
|
|
||||||
music.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.setEnabled(enabled);
|
|
||||||
LOGGER.log(Level.INFO, "Sound enabled: {0}", enabled); //NON-NLS
|
|
||||||
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the game sound on or off.
|
|
||||||
*/
|
|
||||||
public void toggleSound() {
|
|
||||||
setEnabled(!isEnabled());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the volume of music
|
|
||||||
* @param vol the volume to which the music should be set
|
|
||||||
*/
|
|
||||||
public void setVolume(float vol){
|
|
||||||
music.setVolume(vol);
|
|
||||||
PREFERENCES.putFloat(VOLUME_PREF, vol);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,191 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client.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 {
|
|
||||||
public 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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,154 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client.gui;
|
|
||||||
|
|
||||||
import com.jme3.material.Material;
|
|
||||||
import com.jme3.material.RenderState;
|
|
||||||
import com.jme3.material.RenderState.BlendMode;
|
|
||||||
import com.jme3.math.ColorRGBA;
|
|
||||||
import com.jme3.scene.Geometry;
|
|
||||||
import com.jme3.scene.Node;
|
|
||||||
import com.jme3.scene.Spatial;
|
|
||||||
import com.jme3.scene.shape.Sphere;
|
|
||||||
|
|
||||||
import pp.battleship.model.Battleship;
|
|
||||||
import pp.battleship.model.Shell;
|
|
||||||
import pp.battleship.model.Shot;
|
|
||||||
import pp.util.Position;
|
|
||||||
|
|
||||||
import static com.jme3.material.Materials.UNSHADED;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and returns a Spatial representation of the given {@code Shell} object
|
|
||||||
* for 2D visualization in the game. The shell is represented as a circle.
|
|
||||||
*
|
|
||||||
* @param shell The {@code Shell} object to be visualized.
|
|
||||||
* @return A {@code Spatial} object representing the shell on the map.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Spatial visit(Shell shell) {
|
|
||||||
final ColorRGBA color = ColorRGBA.Black;
|
|
||||||
Geometry ellipse = new Geometry("ellipse", new Sphere(50, 50, MapView.FIELD_SIZE / 2 * 0.8f));
|
|
||||||
Material mat = new Material(view.getApp().getAssetManager(), UNSHADED); //NON-NLS
|
|
||||||
mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
|
|
||||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
|
||||||
mat.setColor("Color", color);
|
|
||||||
ellipse.setMaterial(mat);
|
|
||||||
ellipse.addControl(new Shell2DControl(view, shell));
|
|
||||||
return ellipse;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,285 +0,0 @@
|
|||||||
package pp.battleship.client.gui;
|
|
||||||
|
|
||||||
import com.jme3.effect.ParticleEmitter;
|
|
||||||
import com.jme3.effect.ParticleMesh.Type;
|
|
||||||
import com.jme3.effect.shapes.EmitterSphereShape;
|
|
||||||
import com.jme3.material.Material;
|
|
||||||
import com.jme3.math.ColorRGBA;
|
|
||||||
import com.jme3.math.FastMath;
|
|
||||||
import com.jme3.math.Vector3f;
|
|
||||||
import pp.battleship.client.BattleshipApp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class responsible for creating particle effects used in the game.
|
|
||||||
* This centralizes the creation of various types of particle emitters.
|
|
||||||
*/
|
|
||||||
public class ParticleEffectFactory {
|
|
||||||
private static final int COUNT_FACTOR = 1;
|
|
||||||
private static final float COUNT_FACTOR_F = 1f;
|
|
||||||
private static final boolean POINT_SPRITE = true;
|
|
||||||
private static final Type EMITTER_TYPE = POINT_SPRITE ? Type.Point : Type.Triangle;
|
|
||||||
|
|
||||||
private final BattleshipApp app;
|
|
||||||
|
|
||||||
ParticleEffectFactory(BattleshipApp app) {
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a flame particle emitter.
|
|
||||||
*
|
|
||||||
* @return a configured flame particle emitter
|
|
||||||
*/
|
|
||||||
ParticleEmitter createFlame() {
|
|
||||||
ParticleEmitter flame = new ParticleEmitter("Flame", EMITTER_TYPE, 32 * COUNT_FACTOR);
|
|
||||||
flame.setSelectRandomImage(true);
|
|
||||||
flame.setStartColor(new ColorRGBA(1f, 0.4f, 0.05f, (1f / COUNT_FACTOR_F)));
|
|
||||||
flame.setEndColor(new ColorRGBA(.4f, .22f, .12f, 0f));
|
|
||||||
flame.setStartSize(0.1f);
|
|
||||||
flame.setEndSize(0.5f);
|
|
||||||
flame.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f));
|
|
||||||
flame.setParticlesPerSec(0);
|
|
||||||
flame.setGravity(0, -5, 0);
|
|
||||||
flame.setLowLife(.4f);
|
|
||||||
flame.setHighLife(.5f);
|
|
||||||
flame.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 7, 0));
|
|
||||||
flame.getParticleInfluencer().setVelocityVariation(1f);
|
|
||||||
flame.setImagesX(2);
|
|
||||||
flame.setImagesY(2);
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/flame.png"));
|
|
||||||
mat.setBoolean("PointSprite", POINT_SPRITE);
|
|
||||||
flame.setMaterial(mat);
|
|
||||||
|
|
||||||
return flame;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a flash particle emitter.
|
|
||||||
*
|
|
||||||
* @return a configured flash particle emitter
|
|
||||||
*/
|
|
||||||
ParticleEmitter createFlash() {
|
|
||||||
ParticleEmitter flash = new ParticleEmitter("Flash", EMITTER_TYPE, 24 * COUNT_FACTOR);
|
|
||||||
flash.setSelectRandomImage(true);
|
|
||||||
flash.setStartColor(new ColorRGBA(1f, 0.8f, 0.36f, 1f / COUNT_FACTOR_F));
|
|
||||||
flash.setEndColor(new ColorRGBA(1f, 0.8f, 0.36f, 0f));
|
|
||||||
flash.setStartSize(.1f);
|
|
||||||
flash.setEndSize(0.5f);
|
|
||||||
flash.setShape(new EmitterSphereShape(Vector3f.ZERO, .05f));
|
|
||||||
flash.setParticlesPerSec(0);
|
|
||||||
flash.setGravity(0, 0, 0);
|
|
||||||
flash.setLowLife(.2f);
|
|
||||||
flash.setHighLife(.2f);
|
|
||||||
flash.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 5f, 0));
|
|
||||||
flash.getParticleInfluencer().setVelocityVariation(1);
|
|
||||||
flash.setImagesX(2);
|
|
||||||
flash.setImagesY(2);
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/flash.png"));
|
|
||||||
mat.setBoolean("PointSprite", POINT_SPRITE);
|
|
||||||
flash.setMaterial(mat);
|
|
||||||
|
|
||||||
return flash;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a round spark particle emitter.
|
|
||||||
*
|
|
||||||
* @return a configured round spark particle emitter
|
|
||||||
*/
|
|
||||||
ParticleEmitter createRoundSpark() {
|
|
||||||
ParticleEmitter roundSpark = new ParticleEmitter("RoundSpark", EMITTER_TYPE, 20 * COUNT_FACTOR);
|
|
||||||
roundSpark.setStartColor(new ColorRGBA(1f, 0.29f, 0.34f, (float) (1.0 / COUNT_FACTOR_F)));
|
|
||||||
roundSpark.setEndColor(new ColorRGBA(0, 0, 0, 0.5f / COUNT_FACTOR_F));
|
|
||||||
roundSpark.setStartSize(0.2f);
|
|
||||||
roundSpark.setEndSize(0.8f);
|
|
||||||
roundSpark.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f));
|
|
||||||
roundSpark.setParticlesPerSec(0);
|
|
||||||
roundSpark.setGravity(0, -.5f, 0);
|
|
||||||
roundSpark.setLowLife(1.8f);
|
|
||||||
roundSpark.setHighLife(2f);
|
|
||||||
roundSpark.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 3, 0));
|
|
||||||
roundSpark.getParticleInfluencer().setVelocityVariation(.5f);
|
|
||||||
roundSpark.setImagesX(1);
|
|
||||||
roundSpark.setImagesY(1);
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/roundspark.png"));
|
|
||||||
mat.setBoolean("PointSprite", POINT_SPRITE);
|
|
||||||
roundSpark.setMaterial(mat);
|
|
||||||
|
|
||||||
return roundSpark;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a spark particle emitter.
|
|
||||||
*
|
|
||||||
* @return a configured spark particle emitter
|
|
||||||
*/
|
|
||||||
ParticleEmitter createSpark() {
|
|
||||||
ParticleEmitter spark = new ParticleEmitter("Spark", Type.Triangle, 30 * COUNT_FACTOR);
|
|
||||||
spark.setStartColor(new ColorRGBA(1f, 0.8f, 0.36f, 1.0f / COUNT_FACTOR_F));
|
|
||||||
spark.setEndColor(new ColorRGBA(1f, 0.8f, 0.36f, 0f));
|
|
||||||
spark.setStartSize(.5f);
|
|
||||||
spark.setEndSize(.5f);
|
|
||||||
spark.setFacingVelocity(true);
|
|
||||||
spark.setParticlesPerSec(0);
|
|
||||||
spark.setGravity(0, 5, 0);
|
|
||||||
spark.setLowLife(1.1f);
|
|
||||||
spark.setHighLife(1.5f);
|
|
||||||
spark.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 20, 0));
|
|
||||||
spark.getParticleInfluencer().setVelocityVariation(1);
|
|
||||||
spark.setImagesX(1);
|
|
||||||
spark.setImagesY(1);
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/spark.png"));
|
|
||||||
spark.setMaterial(mat);
|
|
||||||
|
|
||||||
return spark;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a smoke trail particle emitter.
|
|
||||||
*
|
|
||||||
* @return a configured smoke trail particle emitter
|
|
||||||
*/
|
|
||||||
ParticleEmitter createSmokeTrail() {
|
|
||||||
ParticleEmitter smokeTrail = new ParticleEmitter("SmokeTrail", Type.Triangle, 22 * COUNT_FACTOR);
|
|
||||||
smokeTrail.setStartColor(new ColorRGBA(1f, 0.8f, 0.36f, 1.0f / COUNT_FACTOR_F));
|
|
||||||
smokeTrail.setEndColor(new ColorRGBA(1f, 0.8f, 0.36f, 0f));
|
|
||||||
smokeTrail.setStartSize(.2f);
|
|
||||||
smokeTrail.setEndSize(1f);
|
|
||||||
smokeTrail.setFacingVelocity(true);
|
|
||||||
smokeTrail.setParticlesPerSec(0);
|
|
||||||
smokeTrail.setGravity(0, 1, 0);
|
|
||||||
smokeTrail.setLowLife(.4f);
|
|
||||||
smokeTrail.setHighLife(.5f);
|
|
||||||
smokeTrail.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 12, 0));
|
|
||||||
smokeTrail.getParticleInfluencer().setVelocityVariation(1);
|
|
||||||
smokeTrail.setImagesX(1);
|
|
||||||
smokeTrail.setImagesY(3);
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/smoketrail.png"));
|
|
||||||
smokeTrail.setMaterial(mat);
|
|
||||||
|
|
||||||
return smokeTrail;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a debris particle emitter.
|
|
||||||
*
|
|
||||||
* @return a configured debris particle emitter
|
|
||||||
*/
|
|
||||||
ParticleEmitter createDebris() {
|
|
||||||
ParticleEmitter debris = new ParticleEmitter("Debris", Type.Triangle, 15 * COUNT_FACTOR);
|
|
||||||
debris.setSelectRandomImage(true);
|
|
||||||
debris.setRandomAngle(true);
|
|
||||||
debris.setRotateSpeed(FastMath.TWO_PI * 4);
|
|
||||||
debris.setStartColor(new ColorRGBA(1f, 0.59f, 0.28f, 1.0f / COUNT_FACTOR_F));
|
|
||||||
debris.setEndColor(new ColorRGBA(.5f, 0.5f, 0.5f, 0f));
|
|
||||||
debris.setStartSize(.10f);
|
|
||||||
debris.setEndSize(.15f);
|
|
||||||
debris.setParticlesPerSec(0);
|
|
||||||
debris.setGravity(0, 12f, 0);
|
|
||||||
debris.setLowLife(1.4f);
|
|
||||||
debris.setHighLife(1.5f);
|
|
||||||
debris.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 15, 0));
|
|
||||||
debris.getParticleInfluencer().setVelocityVariation(.60f);
|
|
||||||
debris.setImagesX(3);
|
|
||||||
debris.setImagesY(3);
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/Debris.png"));
|
|
||||||
debris.setMaterial(mat);
|
|
||||||
|
|
||||||
return debris;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a shockwave particle emitter.
|
|
||||||
*
|
|
||||||
* @return a configured shockwave particle emitter
|
|
||||||
*/
|
|
||||||
ParticleEmitter createShockwave() {
|
|
||||||
ParticleEmitter shockwave = new ParticleEmitter("Shockwave", Type.Triangle, 1 * COUNT_FACTOR);
|
|
||||||
shockwave.setFaceNormal(Vector3f.UNIT_Y);
|
|
||||||
shockwave.setStartColor(new ColorRGBA(.48f, 0.17f, 0.01f, .8f / COUNT_FACTOR_F));
|
|
||||||
shockwave.setEndColor(new ColorRGBA(.48f, 0.17f, 0.01f, 0f));
|
|
||||||
shockwave.setStartSize(0f);
|
|
||||||
shockwave.setEndSize(3f);
|
|
||||||
shockwave.setParticlesPerSec(0);
|
|
||||||
shockwave.setGravity(0, 0, 0);
|
|
||||||
shockwave.setLowLife(0.5f);
|
|
||||||
shockwave.setHighLife(0.5f);
|
|
||||||
shockwave.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 0, 0));
|
|
||||||
shockwave.getParticleInfluencer().setVelocityVariation(0f);
|
|
||||||
shockwave.setImagesX(1);
|
|
||||||
shockwave.setImagesY(1);
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Explosion/shockwave.png"));
|
|
||||||
shockwave.setMaterial(mat);
|
|
||||||
|
|
||||||
return shockwave;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a moving smoke emitter.
|
|
||||||
*
|
|
||||||
* @return a configured smoke emitter
|
|
||||||
*/
|
|
||||||
ParticleEmitter createMovingSmokeEmitter() {
|
|
||||||
ParticleEmitter smokeEmitter = new ParticleEmitter("SmokeEmitter", Type.Triangle, 300);
|
|
||||||
smokeEmitter.setGravity(0, 0, 0);
|
|
||||||
smokeEmitter.getParticleInfluencer().setVelocityVariation(1);
|
|
||||||
smokeEmitter.setLowLife(1);
|
|
||||||
smokeEmitter.setHighLife(1);
|
|
||||||
smokeEmitter.getParticleInfluencer().setInitialVelocity(new Vector3f(0, .5f, 0));
|
|
||||||
smokeEmitter.setImagesX(15); // Assuming the smoke texture is a sprite sheet with 15 frames
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Smoke/Smoke.png"));
|
|
||||||
smokeEmitter.setMaterial(mat);
|
|
||||||
|
|
||||||
return smokeEmitter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a one-time water splash particle emitter.
|
|
||||||
*
|
|
||||||
* @return a configured one-time water splash particle emitter
|
|
||||||
*/
|
|
||||||
public ParticleEmitter createWaterSplash() {
|
|
||||||
// Create a new particle emitter for the splash effect
|
|
||||||
ParticleEmitter waterSplash = new ParticleEmitter("WaterSplash", Type.Triangle, 30);
|
|
||||||
|
|
||||||
// Set the shape of the emitter, making particles emit from a point or small area
|
|
||||||
waterSplash.setShape(new EmitterSphereShape(Vector3f.ZERO, 0.2f));
|
|
||||||
|
|
||||||
// Start and end colors for water (blue, fading out)
|
|
||||||
waterSplash.setStartColor(new ColorRGBA(0.4f, 0.4f, 1f, 1f)); // Light blue at start
|
|
||||||
waterSplash.setEndColor(new ColorRGBA(0.4f, 0.4f, 1f, 0f)); // Transparent at the end
|
|
||||||
|
|
||||||
// Particle size: small at start, larger before fading out
|
|
||||||
waterSplash.setStartSize(0.1f);
|
|
||||||
waterSplash.setEndSize(0.3f);
|
|
||||||
|
|
||||||
// Particle lifespan (how long particles live)
|
|
||||||
waterSplash.setLowLife(0.5f);
|
|
||||||
waterSplash.setHighLife(1f);
|
|
||||||
|
|
||||||
// Gravity: Pull the water particles downwards
|
|
||||||
waterSplash.setGravity(0, -9.81f, 0); // Earth's gravity simulation
|
|
||||||
|
|
||||||
// Velocity: Give particles an initial burst upward (simulates splash)
|
|
||||||
waterSplash.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 3, 0));
|
|
||||||
waterSplash.getParticleInfluencer().setVelocityVariation(0.6f); // Add randomness to splash
|
|
||||||
|
|
||||||
// Set how many particles are emitted per second (0 to emit all particles at once)
|
|
||||||
waterSplash.setParticlesPerSec(0);
|
|
||||||
|
|
||||||
// Load a texture for the water splash (assuming a texture exists at this path)
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
|
|
||||||
mat.setTexture("Texture", app.getAssetManager().loadTexture("Effects/Splash/splash.png"));
|
|
||||||
waterSplash.setMaterial(mat);
|
|
||||||
|
|
||||||
return waterSplash;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,203 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client.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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,378 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client.gui;
|
|
||||||
|
|
||||||
import com.jme3.effect.ParticleEmitter;
|
|
||||||
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.Shell;
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
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 BOAT_SMALL_MODEL = "Models/BoatSmall/12219_boat_v2_L2.j3o"; //NON-NLS
|
|
||||||
private static final String CV_MODEL = "Models/CV/CV.j3o"; //NON-NLS
|
|
||||||
private static final String BATTLE_MODEL = "Models/Battle/Battle.j3o"; //NON-NLS
|
|
||||||
private static final String LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
|
|
||||||
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) : handleMiss(shot);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles a miss by representing it with a blue cylinder
|
|
||||||
* and attaching a water splash effect to it.
|
|
||||||
* @param shot the shot to be processed
|
|
||||||
* @return a Spatial simulating a miss with water splash effect
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the sinking animation and removal of ship if destroyed
|
|
||||||
* @param ship the ship to be sunk
|
|
||||||
*/
|
|
||||||
private void sinkAndRemoveShip(Battleship ship) {
|
|
||||||
Battleship wilkeningklaunichtmeinencode = ship;
|
|
||||||
final Node shipNode = (Node) getSpatial(wilkeningklaunichtmeinencode);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
private Spatial createShip(Battleship ship) {
|
|
||||||
switch (ship.getLength()) {
|
|
||||||
case 4: return createBattleship(ship);
|
|
||||||
case 3: return createCV(ship);
|
|
||||||
case 2: return createBattle(ship);
|
|
||||||
case 1: return createSmallship(ship);
|
|
||||||
default: return createBox(ship);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
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.scale(0.0007f);
|
|
||||||
model.setShadowMode(ShadowMode.CastAndReceive);
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a detailed 3D model to represent a small tug boat.
|
|
||||||
*
|
|
||||||
* @param ship the battleship to be represented
|
|
||||||
* @return the spatial representing a small tug boat
|
|
||||||
*/
|
|
||||||
private Spatial createSmallship(Battleship ship) {
|
|
||||||
final Spatial model = app.getAssetManager().loadModel(BOAT_SMALL_MODEL);
|
|
||||||
|
|
||||||
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
|
|
||||||
model.scale(0.0005f);
|
|
||||||
model.setShadowMode(ShadowMode.CastAndReceive);
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a detailed 3D model to represent a "German WWII UBoat".
|
|
||||||
*
|
|
||||||
* @param ship the battleship to be represented
|
|
||||||
* @return the spatial representing the "German WWII UBoat"
|
|
||||||
*/
|
|
||||||
private Spatial createCV(Battleship ship) {
|
|
||||||
final Spatial model = app.getAssetManager().loadModel(CV_MODEL);
|
|
||||||
|
|
||||||
model.rotate(0f, calculateRotationAngle(ship.getRot()), 0f);
|
|
||||||
model.move(0f, 0.25f, 0f);
|
|
||||||
model.scale(0.85f);
|
|
||||||
model.setShadowMode(ShadowMode.CastAndReceive);
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a detailed 3D model to represent a battleship.
|
|
||||||
*
|
|
||||||
* @param ship the battleship to be represented
|
|
||||||
* @return the spatial representing a battleship
|
|
||||||
*/
|
|
||||||
private Spatial createBattle(Battleship ship) {
|
|
||||||
final Spatial model = app.getAssetManager().loadModel(BATTLE_MODEL);
|
|
||||||
|
|
||||||
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
|
|
||||||
model.move(0f, -0.06f, 0f);
|
|
||||||
model.scale(0.27f);
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and returns a 3D model representation of the given {@code Shell} object
|
|
||||||
* for visualization in the game.
|
|
||||||
*
|
|
||||||
* @param shell The {@code Shell} object to be visualized.
|
|
||||||
* @return A {@code Spatial} object representing the 3D model of the shell.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Spatial visit(Shell shell) {
|
|
||||||
final Spatial model = app.getAssetManager().loadModel("Models/Shell/shell.j3o");
|
|
||||||
model.setLocalScale(.05f);
|
|
||||||
model.setShadowMode(ShadowMode.CastAndReceive);
|
|
||||||
Material mat = new Material(app.getAssetManager(), LIGHTING);
|
|
||||||
mat.setTexture("DiffuseMap", app.getAssetManager().loadTexture("Models/Shell/shell_color.png"));
|
|
||||||
mat.setReceivesShadows(true);
|
|
||||||
model.setMaterial(mat);
|
|
||||||
|
|
||||||
model.addControl(new ShellControl(shell));
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,57 +0,0 @@
|
|||||||
package pp.battleship.client.gui;
|
|
||||||
|
|
||||||
import com.jme3.math.Vector3f;
|
|
||||||
import com.jme3.renderer.RenderManager;
|
|
||||||
import com.jme3.renderer.ViewPort;
|
|
||||||
import com.jme3.scene.control.AbstractControl;
|
|
||||||
import pp.battleship.model.Shell;
|
|
||||||
import pp.util.Position;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls the 2D representation of a {@code Shell} in the game, updating its position
|
|
||||||
* based on the shell's current state in the game model. The {@code Shell2DControl} class
|
|
||||||
* is responsible for translating the shell's 3D position to a 2D view position within
|
|
||||||
* the game's map view.
|
|
||||||
*/
|
|
||||||
public class Shell2DControl extends AbstractControl {
|
|
||||||
private final Shell shell;
|
|
||||||
private final MapView view;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code Shell2DControl} to manage the 2D visualization of the given {@code Shell}.
|
|
||||||
*
|
|
||||||
* @param view The {@code MapView} used to get information about the map to display.
|
|
||||||
* @param shell The {@code Shell} being visualized.
|
|
||||||
*/
|
|
||||||
public Shell2DControl(MapView view, Shell shell){
|
|
||||||
this.shell = shell;
|
|
||||||
this.view = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the position of the shell's 2D representation based on the shell's current
|
|
||||||
* 3D position in the game model. The position is mapped from model space to view space
|
|
||||||
* coordinates and translated to the appropriate location within the {@code MapView}.
|
|
||||||
*
|
|
||||||
* @param tpf Time per frame, representing the time elapsed since the last frame.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void controlUpdate(float tpf) {
|
|
||||||
Vector3f shellPos = shell.getPosition();
|
|
||||||
Position viewPos = view.modelToView(shellPos.x, shellPos.z);
|
|
||||||
spatial.setLocalTranslation(viewPos.getX() + MapView.FIELD_SIZE / 2, viewPos.getY() + MapView.FIELD_SIZE / 2, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called during the rendering phase, but it does not perform any
|
|
||||||
* operations in this implementation as the control only influences the spatial's
|
|
||||||
* transformation, not its rendering process.
|
|
||||||
*
|
|
||||||
* @param rm the RenderManager rendering the controlled Spatial (not null)
|
|
||||||
* @param vp the ViewPort being rendered (not null)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void controlRender(RenderManager rm, ViewPort vp) {
|
|
||||||
// No rendering-specific behavior required for this control
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,58 +0,0 @@
|
|||||||
package pp.battleship.client.gui;
|
|
||||||
|
|
||||||
import com.jme3.math.Vector3f;
|
|
||||||
import com.jme3.renderer.RenderManager;
|
|
||||||
import com.jme3.renderer.ViewPort;
|
|
||||||
import com.jme3.scene.control.AbstractControl;
|
|
||||||
import pp.battleship.model.Shell;
|
|
||||||
|
|
||||||
import static pp.util.FloatMath.PI;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls the 3D representation of a {@code Shell} in the game, updating its position
|
|
||||||
* and rotation based on the shell's current state in the game model. The {@code ShellControl}
|
|
||||||
* class ensures that the spatial associated with the shell is positioned and oriented correctly
|
|
||||||
* within the world.
|
|
||||||
*/
|
|
||||||
public class ShellControl extends AbstractControl {
|
|
||||||
private final Shell shell;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new {@code ShellControl} to manage the 3D visualization of the given {@code Shell}.
|
|
||||||
*
|
|
||||||
* @param shell The {@code Shell} being visualized and controlled.
|
|
||||||
*/
|
|
||||||
public ShellControl(Shell shell){
|
|
||||||
super();
|
|
||||||
this.shell = shell;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the 3D position and rotation of the shell based on its current state.
|
|
||||||
* Converts map coordinates to world coordinates and applies the shell's orientation.
|
|
||||||
*
|
|
||||||
* @param tpf Time per frame, representing the elapsed time since the last update.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void controlUpdate(float tpf) {
|
|
||||||
Vector3f pos = shell.getPosition();
|
|
||||||
Vector3f fixed = new Vector3f(pos.z + 0.5f, pos.y, pos.x + 0.5f);
|
|
||||||
fixed.setY(pos.y);
|
|
||||||
spatial.setLocalTranslation(fixed);
|
|
||||||
spatial.setLocalRotation(shell.getRotation());
|
|
||||||
spatial.rotate(PI/2,0,0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called during the rendering phase, but it does not perform any
|
|
||||||
* operations in this implementation as the control only influences the spatial's
|
|
||||||
* transformation, not its rendering process.
|
|
||||||
*
|
|
||||||
* @param rm the RenderManager rendering the controlled Spatial (not null)
|
|
||||||
* @param vp the ViewPort being rendered (not null)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void controlRender(RenderManager rm, ViewPort vp) {
|
|
||||||
// No rendering-specific behavior required for this control
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,106 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client.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
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,89 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.client.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());
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,61 +0,0 @@
|
|||||||
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} 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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-related code needed
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
package pp.battleship.client.gui;
|
|
||||||
|
|
||||||
import com.simsilica.lemur.Slider;
|
|
||||||
/**
|
|
||||||
* The VolumeSlider class represents the Volume Slider in the Menu.
|
|
||||||
* It extends the Slider class and provides functionalities for setting the music volume,
|
|
||||||
* with the help of the Slider in the GUI
|
|
||||||
*/
|
|
||||||
public class VolumeSlider extends Slider {
|
|
||||||
|
|
||||||
private final GameMusic music;
|
|
||||||
private double vol;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs the Volume Slider for the Menu dialog
|
|
||||||
* @param music the music instance
|
|
||||||
*/
|
|
||||||
public VolumeSlider(GameMusic music) {
|
|
||||||
super();
|
|
||||||
this.music = music;
|
|
||||||
vol = GameMusic.volumeInPreferences();
|
|
||||||
getModel().setPercent(vol);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* when triggered it updates the volume to the value set with the slider
|
|
||||||
*/
|
|
||||||
public void update() {
|
|
||||||
if (vol != getModel().getPercent()) {
|
|
||||||
vol = getModel().getPercent();
|
|
||||||
music.setVolume( (float) vol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,182 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.server;
|
|
||||||
|
|
||||||
import com.jme3.network.ConnectionListener;
|
|
||||||
import com.jme3.network.HostedConnection;
|
|
||||||
import com.jme3.network.Message;
|
|
||||||
import com.jme3.network.MessageListener;
|
|
||||||
import com.jme3.network.Network;
|
|
||||||
import com.jme3.network.Server;
|
|
||||||
import com.jme3.network.serializing.Serializer;
|
|
||||||
import pp.battleship.BattleshipConfig;
|
|
||||||
import pp.battleship.game.server.Player;
|
|
||||||
import pp.battleship.game.server.ServerGameLogic;
|
|
||||||
import pp.battleship.game.server.ServerSender;
|
|
||||||
import pp.battleship.message.client.AnimationFinishedMessage;
|
|
||||||
import pp.battleship.message.client.ClientMessage;
|
|
||||||
import pp.battleship.message.client.MapMessage;
|
|
||||||
import pp.battleship.message.client.ShootMessage;
|
|
||||||
import pp.battleship.message.server.EffectMessage;
|
|
||||||
import pp.battleship.message.server.GameDetails;
|
|
||||||
import pp.battleship.message.server.ServerMessage;
|
|
||||||
import pp.battleship.message.server.StartBattleMessage;
|
|
||||||
import pp.battleship.model.Battleship;
|
|
||||||
import pp.battleship.model.IntPoint;
|
|
||||||
import pp.battleship.model.Shot;
|
|
||||||
|
|
||||||
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.BlockingQueue;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.logging.LogManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server implementing the visitor pattern as MessageReceiver for ClientMessages
|
|
||||||
*/
|
|
||||||
public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
|
|
||||||
private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName());
|
|
||||||
private static final File CONFIG_FILE = new File("server.properties");
|
|
||||||
|
|
||||||
private final BattleshipConfig config = new BattleshipConfig();
|
|
||||||
private Server myServer;
|
|
||||||
private final ServerGameLogic logic;
|
|
||||||
private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();
|
|
||||||
|
|
||||||
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 Battleships server.
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
|
||||||
new BattleshipServer().run();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the server.
|
|
||||||
*/
|
|
||||||
BattleshipServer() {
|
|
||||||
config.readFromIfExists(CONFIG_FILE);
|
|
||||||
LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
|
|
||||||
logic = new ServerGameLogic(this, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run() {
|
|
||||||
startServer();
|
|
||||||
while (true)
|
|
||||||
processNextMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startServer() {
|
|
||||||
try {
|
|
||||||
LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
|
|
||||||
myServer = Network.createServer(config.getPort());
|
|
||||||
initializeSerializables();
|
|
||||||
myServer.start();
|
|
||||||
registerListeners();
|
|
||||||
LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processNextMessage() {
|
|
||||||
try {
|
|
||||||
pendingMessages.take().process(logic);
|
|
||||||
}
|
|
||||||
catch (InterruptedException ex) {
|
|
||||||
LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeSerializables() {
|
|
||||||
Serializer.registerClass(GameDetails.class);
|
|
||||||
Serializer.registerClass(StartBattleMessage.class);
|
|
||||||
Serializer.registerClass(MapMessage.class);
|
|
||||||
Serializer.registerClass(ShootMessage.class);
|
|
||||||
Serializer.registerClass(EffectMessage.class);
|
|
||||||
Serializer.registerClass(AnimationFinishedMessage.class);
|
|
||||||
Serializer.registerClass(Battleship.class);
|
|
||||||
Serializer.registerClass(IntPoint.class);
|
|
||||||
Serializer.registerClass(Shot.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerListeners() {
|
|
||||||
myServer.addMessageListener(this, MapMessage.class);
|
|
||||||
myServer.addMessageListener(this, ShootMessage.class);
|
|
||||||
myServer.addMessageListener(this, AnimationFinishedMessage.class);
|
|
||||||
myServer.addConnectionListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void messageReceived(HostedConnection source, Message message) {
|
|
||||||
LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS
|
|
||||||
if (message instanceof ClientMessage clientMessage)
|
|
||||||
pendingMessages.add(new ReceivedMessage(clientMessage, source.getId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connectionAdded(Server server, HostedConnection hostedConnection) {
|
|
||||||
LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS
|
|
||||||
logic.addPlayer(hostedConnection.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void connectionRemoved(Server server, HostedConnection hostedConnection) {
|
|
||||||
LOGGER.log(Level.INFO, "connection closed: {0}", hostedConnection); //NON-NLS
|
|
||||||
final Player player = logic.getPlayerById(hostedConnection.getId());
|
|
||||||
if (player == null)
|
|
||||||
LOGGER.log(Level.INFO, "closed connection does not belong to an active player"); //NON-NLS
|
|
||||||
else { //NON-NLS
|
|
||||||
LOGGER.log(Level.INFO, "closed connection belongs to {0}", player); //NON-NLS
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void exit(int exitValue) { //NON-NLS
|
|
||||||
LOGGER.log(Level.INFO, "close request"); //NON-NLS
|
|
||||||
if (myServer != null)
|
|
||||||
for (HostedConnection client : myServer.getConnections()) //NON-NLS
|
|
||||||
if (client != null) client.close("Game over"); //NON-NLS
|
|
||||||
System.exit(exitValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the specified message to the specified connection.
|
|
||||||
*
|
|
||||||
* @param id the connection id
|
|
||||||
* @param message the message
|
|
||||||
*/
|
|
||||||
public void send(int id, ServerMessage message) {
|
|
||||||
if (myServer == null || !myServer.isRunning()) {
|
|
||||||
LOGGER.log(Level.ERROR, "no server running when trying to send {0}", message); //NON-NLS
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final HostedConnection connection = myServer.getConnection(id);
|
|
||||||
if (connection != null)
|
|
||||||
connection.send(message);
|
|
||||||
else
|
|
||||||
LOGGER.log(Level.ERROR, "there is no connection with id={0}", id); //NON-NLS
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
////////////////////////////////////////
|
|
||||||
// Programming project code
|
|
||||||
// UniBw M, 2022, 2023, 2024
|
|
||||||
// www.unibw.de/inf2
|
|
||||||
// (c) Mark Minas (mark.minas@unibw.de)
|
|
||||||
////////////////////////////////////////
|
|
||||||
|
|
||||||
package pp.battleship.server;
|
|
||||||
|
|
||||||
import pp.battleship.message.client.ClientInterpreter;
|
|
||||||
import pp.battleship.message.client.ClientMessage;
|
|
||||||
|
|
||||||
record ReceivedMessage(ClientMessage message, int from) {
|
|
||||||
void process(ClientInterpreter interpreter) {
|
|
||||||
message.accept(interpreter, from);
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 168 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 166 KiB |
Before Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 264 KiB |
Before Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 98 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 78 B |
Before Width: | Height: | Size: 79 B |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 670 KiB |