Compare commits
	
		
			202 Commits
		
	
	
		
			b_schmelz_
			...
			acc797f2ff
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | acc797f2ff | ||
|  | 853c32a5b8 | ||
|  | 72bef7143a | ||
|  | b1ed571950 | ||
|  | 1a75d6d6a8 | ||
|  | c990e7b562 | ||
|  | c87406f60e | ||
|  | 21914a1294 | ||
|  | a344145732 | ||
|  | a27ac31086 | ||
|  | 9a7f75b76b | ||
|  | 3e487c00d5 | ||
|  | 4820b8cf46 | ||
|  | caa4cb5262 | ||
|  | 1b2a7d73b5 | ||
|  | 55778dbcea | ||
|  | 422faec281 | ||
|  | dcf10e0819 | ||
|  | 6b78733a5d | ||
|  | c124a99901 | ||
|  | d7df4002da | ||
|  | 8b0ef97a82 | ||
|  | 75d3bef5c8 | ||
|  | c2d5611ab9 | ||
|  | fb28f3fefc | ||
|  | 6a34dab00c | ||
|  | c7bd7d18b7 | ||
|  | 437114704a | ||
|  | 9b4cac4e56 | ||
|  | e780513b35 | ||
|  | d582c59a7d | ||
|  | 7d2c85f617 | ||
|  | 4a882bc4ac | ||
|  | 3c4eac1fcd | ||
|  | 75d5a15bdb | ||
|  | 17f121f7d1 | ||
|  | a66c570b51 | ||
|  | e1e7f2eaf6 | ||
|  | 46d2dce372 | ||
|  | c39f1f6d4c | ||
|  | 738b04f2d9 | ||
|  | ecba1c3135 | ||
|  | 748226f4ed | ||
|  | f658f53ba9 | ||
|  | 4a8304ee4f | ||
|  | 7fe9604338 | ||
|  | 7f09a6dae8 | ||
|  | 270129bc97 | ||
|  | 62ec6f6bef | ||
|  | b24c14900c | ||
|  | 44c55ce3a6 | ||
|  | 895bea048c | ||
|  | 885ab5bdc9 | ||
|  | c6355b2c09 | ||
|  | ada6bf3f24 | ||
|  | db471ac679 | ||
|  | 84059c4085 | ||
|  | 09b363f943 | ||
|  | fd347c7780 | ||
|  | b38a34efa5 | ||
|  | 10e24cf30e | ||
|  | 3369057afb | ||
|  | 7ada56e215 | ||
|  | 373b9e6d53 | ||
|  | 3de31de9f7 | ||
|  | 7252259ab3 | ||
|  | f33c673a05 | ||
|  | 65a7958b66 | ||
|  | 91e4df7478 | ||
|  | 399606d9a0 | ||
|  | 559afcaffd | ||
|  | 15f5f91ddb | ||
|  | 067a8063a5 | ||
|  | 35e367ba24 | ||
|  | fe68d991fc | ||
|  | 88dc633e8e | ||
|  | 0b9cbb0f5f | ||
|  | c3a33b4402 | ||
|  | e12f00aad6 | ||
|  | 47bac0185f | ||
|  | dafe1dab2a | ||
|  | 8ba4ea2819 | ||
|  | f6f708fd5b | ||
|  | 1203077aed | ||
|  | ddbf79c928 | ||
|  | aa47dd579f | ||
|  | cdce478d77 | ||
|  | f11f4a59f1 | ||
|  | d631e8df1e | ||
|  | af80d40700 | ||
|  | 3a6f8a945a | ||
|  | bb9727d54a | ||
|  | 7da8b3e545 | ||
|  | b6968df451 | ||
|  | b85bbdd0ad | ||
|  | aa986fb948 | ||
|  | 6e63de6e99 | ||
|  | 962ecd08fa | ||
|  | 580a26b3ea | ||
|  | 0cda6b5fa0 | ||
|  | cc8c3795b8 | ||
|  | 3c21165efb | ||
|  | 818fdf4670 | ||
|  | b7d679b492 | ||
|  | a42093df23 | ||
|  | 6157db51da | ||
|  | 843052989b | ||
|  | 8e01fa3192 | ||
|  | e2126a7ea7 | ||
|  | 0b46d14650 | ||
|  | 9e28deedf7 | ||
|  | 9c0172b413 | ||
|  | 2933d022aa | ||
|  | bdefb7193e | ||
|  | 7246133363 | ||
|  | 69b68a6975 | ||
|  | e38a7e8cdd | ||
|  | dbd9dd0001 | ||
|  | ccb1743a01 | ||
|  | 4ac897693c | ||
|  | dfb962e9fe | ||
|  | f97cbd778d | ||
|  | e889a61d43 | ||
|  | a33d422547 | ||
|  | c4b4001b96 | ||
|  | 20949b641e | ||
|  | 56e2174b6d | ||
|  | acfeebca14 | ||
|  | cc47acbd3f | ||
|  | 869e16406d | ||
|  | 1c99da4fc5 | ||
|  | b74ca4e702 | ||
|  | 5fde7451c8 | ||
|  | 6fd9209336 | ||
|  | 0213dc3560 | ||
|  | e73c434c1b | ||
|  | b135f3fa50 | ||
|  | 0e580a3180 | ||
|  | 8e3cb43244 | ||
|  | 6dcfb92dba | ||
|  | a7ea5773da | ||
|  | 5b66131d30 | ||
|  | cac06dda7d | ||
|  | 12c1c97a99 | ||
|  | af4b4243ea | ||
|  | 5189c74058 | ||
|  | fffd32e13c | ||
|  | b37a5095f0 | ||
|  | 951b4c198c | ||
|  | 8698eed273 | ||
|  | 1af8614b35 | ||
|  | 11faeeaf45 | ||
|  | 9d0fcc3370 | ||
|  | 2f6d6037de | ||
|  | 1d69bcc814 | ||
|  | 62c363068b | ||
|  | d2e0b8187b | ||
|  | 628b98af9b | ||
|  | 627e3dbd7f | ||
|  | 61b88f6bf8 | ||
|  | 8ab37e73c1 | ||
|  | afdf43ebf1 | ||
|  | 2e0d1c059d | ||
|  | 2b772199b0 | ||
|  | c37b850798 | ||
|  | c31f924d77 | ||
|  | ed87a6167d | ||
|  | 0d86ba0ca9 | ||
|  | f2fd283d06 | ||
|  | 10b978debf | ||
|  | 85756713df | ||
|  | 3bdfd6a78a | ||
|  | e59ab4a320 | ||
|  | 15b3902bd3 | ||
|  | b622d66942 | ||
|  | 12ef219064 | ||
|  | 853b52b52d | ||
|  | 7a2ad1d31a | ||
|  | c5ad476eaf | ||
|  | 12978ff410 | ||
|  | caa45097c3 | ||
|  | 232e3a117c | ||
|  | 3668382911 | ||
|  | fafa53ffb7 | ||
|  | 44673fd57e | ||
|  | f7149f225c | ||
|  | 6773e18d34 | ||
|  | 19a9b06f3c | ||
|  | 7ee2273761 | ||
|  | 86b8297c9d | ||
|  | 17533112a1 | ||
|  | 2cc1a338ec | ||
|  | 81731247c7 | ||
|  | 65c85aacf0 | ||
|  | dca23151a8 | ||
|  | 25305760c5 | ||
|  | 3ef2459ad0 | ||
|  | 29a56f42a8 | ||
|  | 3784348f91 | ||
|  | 188ec03abd | ||
|  | f47cda7dc4 | ||
|  | 71a4ac8d12 | 
							
								
								
									
										27
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| # Gradle | ||||
| .gradle | ||||
| build | ||||
|  | ||||
| # VSC | ||||
| bin | ||||
| .vscode | ||||
|  | ||||
| # IntelliJ | ||||
| *.iml | ||||
| .idea | ||||
| out | ||||
|  | ||||
| # Eclipse | ||||
| .classpath | ||||
| .project | ||||
|  | ||||
| # Libraries | ||||
| *.so | ||||
| *.dylib | ||||
| *.dll | ||||
| *.jar | ||||
| *.class | ||||
|  | ||||
| .DS_Store | ||||
| !Projekte/gradle/wrapper/gradle-wrapper.jar | ||||
|  | ||||
							
								
								
									
										1
									
								
								Dokumente/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| # | ||||
							
								
								
									
										3
									
								
								Projekte/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| *.bat           text eol=crlf | ||||
| gradlew         text eol=lf | ||||
|  | ||||
							
								
								
									
										18
									
								
								Projekte/.run/BattleshipApp (Mac).run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | ||||
| <component name="ProjectRunConfigurationManager"> | ||||
|     <configuration default="false" name="BattleshipApp (Mac)" type="Application" factoryName="Application" | ||||
|                    singleton="false"> | ||||
|         <option name="MAIN_CLASS_NAME" value="pp.battleship.client.BattleshipApp"/> | ||||
|         <module name="Projekte.battleship.client.main"/> | ||||
|         <option name="VM_PARAMETERS" value="-XstartOnFirstThread"/> | ||||
|         <option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/> | ||||
|         <extension name="coverage"> | ||||
|             <pattern> | ||||
|                 <option name="PATTERN" value="pp.battleship.client.*"/> | ||||
|                 <option name="ENABLED" value="true"/> | ||||
|             </pattern> | ||||
|         </extension> | ||||
|         <method v="2"> | ||||
|             <option name="Make" enabled="true"/> | ||||
|         </method> | ||||
|     </configuration> | ||||
| </component> | ||||
							
								
								
									
										18
									
								
								Projekte/.run/BattleshipApp.run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | ||||
| <component name="ProjectRunConfigurationManager"> | ||||
|   <configuration default="false" name="BattleshipApp" type="Application" factoryName="Application" singleton="false" | ||||
|                  nameIsGenerated="true"> | ||||
|     <option name="MAIN_CLASS_NAME" value="pp.battleship.client.BattleshipApp"/> | ||||
|     <module name="Projekte.battleship.client.main"/> | ||||
|     <option name="VM_PARAMETERS" value="-Djava.util.logging.config.file=logging.properties"/> | ||||
|     <option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/> | ||||
|     <extension name="coverage"> | ||||
|       <pattern> | ||||
|         <option name="PATTERN" value="pp.battleship.client.*"/> | ||||
|         <option name="ENABLED" value="true"/> | ||||
|       </pattern> | ||||
|     </extension> | ||||
|     <method v="2"> | ||||
|       <option name="Make" enabled="true"/> | ||||
|     </method> | ||||
|   </configuration> | ||||
| </component> | ||||
							
								
								
									
										17
									
								
								Projekte/.run/BattleshipServer.run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| <component name="ProjectRunConfigurationManager"> | ||||
|   <configuration default="false" name="BattleshipServer" type="Application" factoryName="Application" | ||||
|                  nameIsGenerated="true"> | ||||
|     <option name="MAIN_CLASS_NAME" value="pp.battleship.server.BattleshipServer"/> | ||||
|     <module name="Projekte.battleship.server.main"/> | ||||
|     <option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/> | ||||
|     <extension name="coverage"> | ||||
|       <pattern> | ||||
|         <option name="PATTERN" value="pp.battleship.server.*"/> | ||||
|         <option name="ENABLED" value="true"/> | ||||
|       </pattern> | ||||
|     </extension> | ||||
|     <method v="2"> | ||||
|       <option name="Make" enabled="true"/> | ||||
|     </method> | ||||
|   </configuration> | ||||
| </component> | ||||
							
								
								
									
										18
									
								
								Projekte/.run/MonopolyApp.run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | ||||
| <component name="ProjectRunConfigurationManager"> | ||||
|   <configuration default="false" name="MonopolyApp" type="Application" factoryName="Application" singleton="false" | ||||
|                  nameIsGenerated="true"> | ||||
|     <option name="MAIN_CLASS_NAME" value="pp.monopoly.client.MonopolyApp"/> | ||||
|     <module name="Projekte.monopoly.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.monopoly.client.*"/> | ||||
|         <option name="ENABLED" value="true"/> | ||||
|       </pattern> | ||||
|     </extension> | ||||
|     <method v="2"> | ||||
|       <option name="Make" enabled="true"/> | ||||
|     </method> | ||||
|   </configuration> | ||||
| </component> | ||||
							
								
								
									
										17
									
								
								Projekte/.run/MonopolyServer.run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| <component name="ProjectRunConfigurationManager"> | ||||
|   <configuration default="false" name="MonopolyServer" type="Application" factoryName="Application" | ||||
|                  nameIsGenerated="true"> | ||||
|     <option name="MAIN_CLASS_NAME" value="pp.monopoly.server.MonopolyServer"/> | ||||
|     <module name="Projekte.monopoly.server.main"/> | ||||
|     <option name="WORKING_DIRECTORY" value="$MODULE_WORKING_DIR$"/> | ||||
|     <extension name="coverage"> | ||||
|       <pattern> | ||||
|         <option name="PATTERN" value="pp.monopoly.server.*"/> | ||||
|         <option name="ENABLED" value="true"/> | ||||
|       </pattern> | ||||
|     </extension> | ||||
|     <method v="2"> | ||||
|       <option name="Make" enabled="true"/> | ||||
|     </method> | ||||
|   </configuration> | ||||
| </component> | ||||
							
								
								
									
										24
									
								
								Projekte/.run/Projekte [test].run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| <component name="ProjectRunConfigurationManager"> | ||||
|   <configuration default="false" name="Projekte [test]" type="GradleRunConfiguration" factoryName="Gradle"> | ||||
|     <ExternalSystemSettings> | ||||
|       <option name="executionName" /> | ||||
|       <option name="externalProjectPath" value="$PROJECT_DIR$" /> | ||||
|       <option name="externalSystemIdString" value="GRADLE" /> | ||||
|       <option name="scriptParameters" value="--continue" /> | ||||
|       <option name="taskDescriptions"> | ||||
|         <list /> | ||||
|       </option> | ||||
|       <option name="taskNames"> | ||||
|         <list> | ||||
|           <option value="test" /> | ||||
|         </list> | ||||
|       </option> | ||||
|       <option name="vmOptions" /> | ||||
|     </ExternalSystemSettings> | ||||
|     <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> | ||||
|     <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> | ||||
|     <DebugAllEnabled>false</DebugAllEnabled> | ||||
|     <RunAsTest>false</RunAsTest> | ||||
|     <method v="2" /> | ||||
|   </configuration> | ||||
| </component> | ||||
							
								
								
									
										189
									
								
								Projekte/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,189 @@ | ||||
| # Beispielprogramme des Programmierprojekts | ||||
|  | ||||
| Hier ist der Quellcode für das in der Einarbeitungsphase genutzte Spiel | ||||
| _Battleships_ zu finden. Die Quellen bestehen aus den folgenden | ||||
| Gradle-Unterprojekten: | ||||
|  | ||||
| * _:battleship:server_ | ||||
| * _:battleship:client_ | ||||
| * _:battleship:model_ | ||||
| * _:battleship:converter_ | ||||
| * _:common_ | ||||
| * _:jme-common_ | ||||
|  | ||||
| _Battleships_ ist ein netzwerkbasiertes Spiel und besteht aus einem Server- und | ||||
| einem Clientanteil, die in den Unterprojekten _:battleship:server_ und | ||||
| _:battleship:client_ realisiert sind. Beide nutzen das Unterprojekt | ||||
| _:battleship:model_, das den gemeinsamen Modellanteil enthält. | ||||
|  | ||||
| Die Unterprojekte _:common_ und _:jme-common_ enthalten Hilfsklassen. | ||||
|  | ||||
| Das Unterprojekt _:battleship:converter_ wird für _Battleships_ selbst nicht | ||||
| benötigt, sondern enthält lediglich den Code, um ein im Spiel verwendetes | ||||
| 3d-Modell eines Schlachtschiffs in eine _J3O_-Datei umzuwandeln, die von jME | ||||
| einfacher geladen werden kann. | ||||
|  | ||||
| ## 1 Vorbereitung | ||||
|  | ||||
| Für das Programmierprojekt empfehlen wir die Verwendung von Java 20. Unter Linux | ||||
| sollte [_Eclipse Temurin_](https://adoptium.net/temurin/releases/?version=20) | ||||
| als JDK verwendet werden, andere JDKs können unter Linux Probleme verursachen. | ||||
| Auf anderen Betriebssystemen empfehlen wir aber ebenfalls Temurin. Im Folgenden | ||||
| ist beschrieben, wie Sie Temurin installieren und die Umgebungsvariable | ||||
| **JAVA_HOME** richtig setzen, damit Sie Gradle (siehe unten) verwenden können. | ||||
|  | ||||
| ### 1.1 Installation von Temurin | ||||
|  | ||||
| Laden Sie [_Eclipse Temurin_](https://adoptium.net/temurin/releases/?version=20) | ||||
| entsprechend Ihrem Betriebssystem und Ihrer Prozessorarchitektur herunter und | ||||
| entpacken Sie das Archiv in einem Verzeichnis Ihrer Wahl auf Ihrem Rechner. | ||||
|  | ||||
| ### 1.2 Setzen von JAVA_HOME | ||||
|  | ||||
| Zur Verwendung mit Gradle muss die Umgebungsvariable **JAVA_HOME** richtig | ||||
| gesetzt werden. Folgen Sie dazu den nachfolgenden Anweisungen entsprechend Ihrem | ||||
| Betriebssystem: | ||||
|  | ||||
| * **Windows:** | ||||
|  | ||||
|   Öffnen Sie ihre Powershell (Core) bzw. ihr Windows Terminal mit Powershell | ||||
|   (Core). Überprüfen Sie, ob die Umgebungsvariable korrekt gesetzt ist:   | ||||
|   `Get-ChildItem -Path Env:JAVA_HOME`   | ||||
|   Falls kein oder ein falscher Pfad gesetzt ist, setzen Sie diesen mit dem | ||||
|   folgenden Kommando (in einer Zeile):   | ||||
|   `[System.Environment]::SetEnvironmentVariable('JAVA_HOME','<Pfad zum SDK>',[System.EnvironmentVariableTarget]::User)` | ||||
|  | ||||
|   Alternativ können Sie die GUI verwenden. Unter Windows 10 klicken Sie die | ||||
|   Windows-Taste und dann das Zahnrad um die Einstellungen zu öffnen. Dort wählen | ||||
|   Sie "System", dann "Info" (links unten) und nun | ||||
|   "Erweiterte Systemeinstellungen" (rechts) um den Dialog "Systemeigenschaften" | ||||
|   zu starten. Im Reiter "Erweitert" klicken Sie | ||||
|   "Umgebungsvariablen..." und klicken dann unter "Benutzervariablen" den Knopf | ||||
|   "Neu..." um JAVA_HOME anzulegen oder "Bearbeiten" um ihn zu ändern. Geben Sie | ||||
|   als Name `JAVA_HOME` und als Wert den Pfad ein. Schließen Sie mit "OK". | ||||
|  | ||||
|   > **(!) Beachten Sie, dass Sie die jeweilige Applikation neu starten müssen**, | ||||
|   > um von der gesetzten Umgebungsvariablen Notiz zu nehmen. | ||||
|   > Dies betrifft auch die Shell, die Sie gerade verwenden. | ||||
|  | ||||
| * **UNIX (Linux/MacOS):** | ||||
|  | ||||
|   Öffnen oder erstellen Sie die Datei `~/.profile` (wenn Sie die Bash verwenden; | ||||
|   bei anderen Shells sind es andere Dateien) und ergänzen Sie am Ende der Datei | ||||
|   die Zeile: | ||||
|  | ||||
|   `export JAVA_HOME="<Pfad zum entpackten Archiv>"` | ||||
|  | ||||
|   Ersetzen Sie dabei `<Pfad zum entpackten Archiv>` mit dem entsprechenden Pfad. | ||||
|   Zum Beispiel: | ||||
|  | ||||
|   `export JAVA_HOME="/home/user/jdk-20.0.2"` | ||||
|  | ||||
|   Fügen Sie dann die folgende Zeile hinzu: | ||||
|  | ||||
|   `export PATH="$JAVA_HOME/bin:$PATH"` | ||||
|  | ||||
| ## 2 Programmstart | ||||
|  | ||||
| Grundsätzlich kann man das gesamte Projekt einfach in IntelliJ öffnen. Details | ||||
| dazu sind im Aufgabenblatt zur Einarbeitungsaufgabe zu finden. Im Folgenden ist | ||||
| beschrieben, wie die _Batttleships_ unmittelbar von der Kommandozeile gestartet | ||||
| werden können. | ||||
|  | ||||
| Um _Battleships_ spielen zu können, muss man zuerst das Server-Programm auf | ||||
| einem Rechner und dann zweimal das Client-Programm auf beliebigen Rechnern | ||||
| starten, die TCP/IP-Verbindungen zum Server erlauben. Natürlich ist es auch | ||||
| möglich, alle drei Programme auf demselben Rechner zu starten. | ||||
|  | ||||
| Es empfiehlt sich der Start von der Kommandozeile. Will man alle drei Programme | ||||
| auf demselben Rechner starten, sollte man dazu drei Shell-Instanzen öffnen und | ||||
| in jeder eines der Programme starten. Auf diese Weise können die | ||||
| Logging-Ausgaben der drei Programme voneinander unterschieden werden. | ||||
|  | ||||
| Das Server-Programm startet man unmittelbar mit Gradle mit | ||||
|  | ||||
| `./gradlew :battleship:server:run` | ||||
|  | ||||
| Unter Windows kann es je nach Shell (Eingabeaufforderung cmd) erforderlich sein, | ||||
| `/` jeweils durch `\ ` zu ersetzen. | ||||
|  | ||||
| Im Verzeichnis `battleship/server` befindet sich die Datei `config.propeties`, | ||||
| worüber sich der Server konfigurieren lässt. Mit der Zeile `port=1234` lässt | ||||
| sich der verwendete Server-Port (hier 1234) einstellen. Außerdem befindet sich | ||||
| dort die Datei `logging.properties`, womit das Logging des Servers konfiguriert | ||||
| wird. | ||||
|  | ||||
| Das Client-Programm startet man unmittelbar mit Gradle mit | ||||
|  | ||||
| `./gradlew :battleship:client:run` | ||||
|  | ||||
| Die Datei `logging.properties` im Verzeichnis `battleship/client` konfiguriert | ||||
| das Logging des Clients. | ||||
|  | ||||
| Alternativ kann man auch die Start-Skripte | ||||
|  | ||||
| * `./battleship/server/build/install/battleship-server/bin/battleship-server` | ||||
| * `./battleship/client/build/install/battleship/bin/battleship` | ||||
|  | ||||
| direkt in der Kommandozeile starten. Allerdings müssen sie zuvor mittels | ||||
|  | ||||
| `./gradlew installDist` | ||||
|  | ||||
| erzeugt worden sein. Beachten Sie aber, dass nur im **aktuellen | ||||
| Arbeitsverzeichnis** nach den Dateien `config.properties` und | ||||
| `logging.properties` gesucht wird und diese geladen werden. Das heißt, dass die | ||||
| vordefinierten Dateien in den Verzeichnissen `battleship/server` und | ||||
| `battleship/client` nur dann gelesen werden, wenn Sie diese Verzeichnisse als | ||||
| aktuelle Arbeitsverzeichnisse nutzen. Wie üblich müssen Sie dazu in der | ||||
| Kommandozeile | ||||
|  | ||||
| `cd battleship/server` | ||||
|  | ||||
| bzw. | ||||
|  | ||||
| `cd battleship/client` | ||||
|  | ||||
| eingeben. | ||||
|  | ||||
| ## 3 Hinweise zu _Battleships_ | ||||
|  | ||||
| Der _Battleships_-Client hat ein Menü, in das man immer mit der | ||||
| Esc-Taste kommt. Aus dem Menü heraus lässt sich das Programm auch schließen. | ||||
| Beachte, dass sich beim Laden und Speichern eines Spiels kein Dateidialog | ||||
| öffnet. Vielmehr muss man den Dateipfad in das Dialogfeld eingeben. Da | ||||
| JSON-Dateien geschrieben werden, empfiehlt sich das Datei-Suffix _.json_. | ||||
|  | ||||
| ## 4 Allgemeine Gradle-Tasks: | ||||
|  | ||||
| - `./gradlew clean` | ||||
|  | ||||
|   Entfernt alle `build`-Verzeichnisse und alle erzeugten Dateien. | ||||
|  | ||||
| - `./gradlew classes` | ||||
|  | ||||
|   Übersetzt den Quellcode und legt unter build den Bytecode sowie | ||||
|   Ressourcen ab. | ||||
|  | ||||
| - `./gradlew javadoc` | ||||
|  | ||||
|   Erzeugt die Dokumentation aus den JavaDoc-Kommentaren im Verzeichnis | ||||
|   `build/docs/javadoc` des jeweiligen Unterprojekts. | ||||
|  | ||||
| - `./gradlew test` | ||||
|  | ||||
|   Führt die JUnit-Tests durch. Ergebnisse sind im Verzeichnis | ||||
|   `build/reports/tests` des jeweiligen Unterprojekts zu finden. | ||||
|  | ||||
| - `./gradlew build` | ||||
|  | ||||
|   Führt die JUnit-Tests durch und erstellt in `build/distributions` | ||||
|   gepackte Distributionsdateien | ||||
|  | ||||
| - `./gradlew installDist` | ||||
|  | ||||
|   Erstellt unter `battleship/client/build/install` und | ||||
|   `battleship/server/build/install` Verzeichnisse, die jeweils eine ausführbare | ||||
|   Distribution samt Start-Skripten enthält (siehe oben). | ||||
|  | ||||
| --- | ||||
| Juli 2024 | ||||
							
								
								
									
										23
									
								
								Projekte/battleship/client/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| 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' | ||||
| } | ||||
							
								
								
									
										73
									
								
								Projekte/battleship/client/client.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | ||||
| ######################################## | ||||
| ## Programming project code | ||||
| ## UniBw M, 2022, 2023, 2024 | ||||
| ## www.unibw.de/inf2 | ||||
| ## (c) Mark Minas (mark.minas@unibw.de) | ||||
| ######################################## | ||||
| # | ||||
| # Battleship client configuration | ||||
| # | ||||
| # Specifies the map used by the opponent in single mode. | ||||
| # Single mode is activated if this property is set. | ||||
| #map.opponent=maps/map2.json | ||||
| # | ||||
| # Specifies the map used by the player in single mode. | ||||
| # The player must define their own map if this property is not set. | ||||
| map.own=maps/map1.json | ||||
| # | ||||
| # Coordinates of the shots fired by the RobotClient in the order listed. | ||||
| # Example: | ||||
| #   2, 0,\ | ||||
| #   2, 1,\ | ||||
| #   2, 2,\ | ||||
| #   2, 3 | ||||
| #  defines four shots, namely at the coordinates | ||||
| #  (x=2, y=0), (x=2, y=1), (x=2, y=2), and (x=2, y=3) | ||||
| robot.targets=2, 0,\ | ||||
|               2, 1,\ | ||||
|               2, 2,\ | ||||
|               2, 3 | ||||
| # | ||||
| # Delay in milliseconds between each shot fired by the RobotClient. | ||||
| robot.delay=500 | ||||
| # | ||||
| # The dimensions of the game map used in single mode. | ||||
| # 'map.width' defines the number of columns, and 'map.height' defines the number of rows. | ||||
| map.width=10 | ||||
| map.height=10 | ||||
| # | ||||
| # The number of ships of each length available in single mode. | ||||
| # The value is a comma-separated list where each element corresponds to the number of ships | ||||
| # with a specific length. For example: | ||||
| # ship.nums=4, 3, 2, 1 | ||||
| # This configuration means: | ||||
| #   - 4 ships of length 1 | ||||
| #   - 3 ships of length 2 | ||||
| #   - 2 ships of length 3 | ||||
| #   - 1 ship of length 4 | ||||
| ship.nums=4, 3, 2, 1 | ||||
| # | ||||
| # Screen settings | ||||
| # | ||||
| # Color of the text displayed at the top of the overlay. | ||||
| # The format is (red, green, blue, alpha) where each value ranges from 0 to 1. | ||||
| overlay.top.color=1, 1, 1, 1 | ||||
| # | ||||
| # Application settings configuration | ||||
| # Determines whether the settings window is shown at startup. | ||||
| settings.show=false | ||||
| # | ||||
| # Specifies the width of the application window in pixels. | ||||
| settings.resolution.width=1200 | ||||
| # | ||||
| # Specifies the height of the application window in pixels. | ||||
| settings.resolution.height=800 | ||||
| # | ||||
| # Determines whether the application runs in full-screen mode. | ||||
| settings.full-screen=false | ||||
| # | ||||
| # Enables or disables gamma correction to improve color accuracy. | ||||
| settings.use-gamma-correction=true | ||||
| # | ||||
| # Indicates whether the statistics window is displayed during gameplay. | ||||
| statistics.show=false | ||||
							
								
								
									
										8
									
								
								Projekte/battleship/client/logging.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| handlers=java.util.logging.ConsoleHandler | ||||
| .level=INFO | ||||
| pp.level=FINE | ||||
| com.jme3.network.level=INFO | ||||
| ;com.jme3.util.TangentBinormalGenerator.level=SEVERE | ||||
| java.util.logging.ConsoleHandler.level=FINER | ||||
| java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter | ||||
| ;java.util.logging.SimpleFormatter.format=[%4$s %2$s] %5$s%n | ||||
							
								
								
									
										66
									
								
								Projekte/battleship/client/maps/map1.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,66 @@ | ||||
| { | ||||
|   "width": 10, | ||||
|   "height": 10, | ||||
|   "ships": [ | ||||
|     { | ||||
|       "length": 4, | ||||
|       "x": 2, | ||||
|       "y": 8, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 3, | ||||
|       "x": 2, | ||||
|       "y": 5, | ||||
|       "rot": "DOWN" | ||||
|     }, | ||||
|     { | ||||
|       "length": 3, | ||||
|       "x": 5, | ||||
|       "y": 6, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 2, | ||||
|       "x": 4, | ||||
|       "y": 4, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 2, | ||||
|       "x": 7, | ||||
|       "y": 4, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 2, | ||||
|       "x": 4, | ||||
|       "y": 2, | ||||
|       "rot": "DOWN" | ||||
|     }, | ||||
|     { | ||||
|       "length": 1, | ||||
|       "x": 6, | ||||
|       "y": 2, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 1, | ||||
|       "x": 8, | ||||
|       "y": 2, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 1, | ||||
|       "x": 6, | ||||
|       "y": 0, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 1, | ||||
|       "x": 8, | ||||
|       "y": 0, | ||||
|       "rot": "RIGHT" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										66
									
								
								Projekte/battleship/client/maps/map2.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,66 @@ | ||||
| { | ||||
|   "width": 10, | ||||
|   "height": 10, | ||||
|   "ships": [ | ||||
|     { | ||||
|       "length": 4, | ||||
|       "x": 0, | ||||
|       "y": 5, | ||||
|       "rot": "DOWN" | ||||
|     }, | ||||
|     { | ||||
|       "length": 3, | ||||
|       "x": 0, | ||||
|       "y": 9, | ||||
|       "rot": "DOWN" | ||||
|     }, | ||||
|     { | ||||
|       "length": 3, | ||||
|       "x": 2, | ||||
|       "y": 6, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 2, | ||||
|       "x": 4, | ||||
|       "y": 8, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 2, | ||||
|       "x": 2, | ||||
|       "y": 4, | ||||
|       "rot": "DOWN" | ||||
|     }, | ||||
|     { | ||||
|       "length": 2, | ||||
|       "x": 2, | ||||
|       "y": 1, | ||||
|       "rot": "DOWN" | ||||
|     }, | ||||
|     { | ||||
|       "length": 1, | ||||
|       "x": 6, | ||||
|       "y": 2, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 1, | ||||
|       "x": 8, | ||||
|       "y": 2, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 1, | ||||
|       "x": 6, | ||||
|       "y": 0, | ||||
|       "rot": "RIGHT" | ||||
|     }, | ||||
|     { | ||||
|       "length": 1, | ||||
|       "x": 8, | ||||
|       "y": 0, | ||||
|       "rot": "RIGHT" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -0,0 +1,440 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,196 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client; | ||||
|  | ||||
| import com.jme3.math.ColorRGBA; | ||||
| import pp.battleship.game.singlemode.BattleshipClientConfig; | ||||
|  | ||||
| /** | ||||
|  * Provides access to the Battleship application configuration. | ||||
|  * Extends {@link BattleshipClientConfig} to include additional properties specific to the client, | ||||
|  * particularly those related to screen settings and visual customization. | ||||
|  * <p> | ||||
|  * <b>Note:</b> Attributes of this class should not be marked as {@code final} | ||||
|  * to ensure proper functionality when reading from a properties file. | ||||
|  * </p> | ||||
|  */ | ||||
| public class BattleshipAppConfig extends BattleshipClientConfig { | ||||
|  | ||||
|     /** | ||||
|      * Converts a string value found in the properties file into an object of the specified type. | ||||
|      * Extends the superclass method to support conversion to {@link ColorRGBA}. | ||||
|      * | ||||
|      * @param value      the string value to be converted | ||||
|      * @param targetType the target type into which the value string is converted | ||||
|      * @return the converted object of the specified type | ||||
|      */ | ||||
|     @Override | ||||
|     protected Object convertToType(String value, Class<?> targetType) { | ||||
|         if (targetType == ColorRGBA.class) | ||||
|             return makeColorRGBA(value); | ||||
|         return super.convertToType(value, targetType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Converts the specified string value to a corresponding {@link ColorRGBA} object. | ||||
|      * | ||||
|      * @param value the color in the format "red, green, blue, alpha" with all values in the range [0..1] | ||||
|      * @return a {@link ColorRGBA} object representing the color | ||||
|      * @throws IllegalArgumentException if the input string is not in the expected format | ||||
|      */ | ||||
|     private static ColorRGBA makeColorRGBA(String value) { | ||||
|         String[] split = value.split(",", -1); | ||||
|         try { | ||||
|             if (split.length == 4) | ||||
|                 return new ColorRGBA(Float.parseFloat(split[0]), | ||||
|                                      Float.parseFloat(split[1]), | ||||
|                                      Float.parseFloat(split[2]), | ||||
|                                      Float.parseFloat(split[3])); | ||||
|         } | ||||
|         catch (NumberFormatException e) { | ||||
|             // deliberately left empty | ||||
|         } | ||||
|         throw new IllegalArgumentException(value + " should consist of exactly 4 numbers"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * The width of the game view resolution in pixels. | ||||
|      */ | ||||
|     @Property("settings.resolution.width") //NON-NLS | ||||
|     private int resolutionWidth = 1200; | ||||
|  | ||||
|     /** | ||||
|      * The height of the game view resolution in pixels. | ||||
|      */ | ||||
|     @Property("settings.resolution.height") //NON-NLS | ||||
|     private int resolutionHeight = 800; | ||||
|  | ||||
|     /** | ||||
|      * Specifies whether the game should start in full-screen mode. | ||||
|      */ | ||||
|     @Property("settings.full-screen") //NON-NLS | ||||
|     private boolean fullScreen = false; | ||||
|  | ||||
|     /** | ||||
|      * Specifies whether gamma correction should be enabled. | ||||
|      * If enabled, the main framebuffer is configured for sRGB colors, | ||||
|      * and sRGB images are linearized. | ||||
|      * <p> | ||||
|      * Requires a GPU that supports GL_ARB_framebuffer_sRGB; otherwise, this setting will be ignored. | ||||
|      * </p> | ||||
|      */ | ||||
|     @Property("settings.use-gamma-correction") //NON-NLS | ||||
|     private boolean useGammaCorrection = true; | ||||
|  | ||||
|     /** | ||||
|      * Specifies whether full resolution framebuffers should be used on Retina displays. | ||||
|      * This setting is ignored on non-Retina platforms. | ||||
|      */ | ||||
|     @Property("settings.use-retina-framebuffer") //NON-NLS | ||||
|     private boolean useRetinaFrameBuffer = false; | ||||
|  | ||||
|     /** | ||||
|      * Specifies whether the settings window should be shown for configuring the game. | ||||
|      */ | ||||
|     @Property("settings.show") //NON-NLS | ||||
|     private boolean showSettings = false; | ||||
|  | ||||
|     /** | ||||
|      * Specifies whether the JME statistics window should be shown in the lower left corner of the screen. | ||||
|      */ | ||||
|     @Property("statistics.show") //NON-NLS | ||||
|     private boolean showStatistics = false; | ||||
|  | ||||
|     /** | ||||
|      * The color of the top text during gameplay, represented as a {@link ColorRGBA} object. | ||||
|      */ | ||||
|     @Property("overlay.top.color") //NON-NLS | ||||
|     private ColorRGBA topColor = ColorRGBA.White; | ||||
|  | ||||
|     /** | ||||
|      * Creates a default {@code BattleshipAppConfig} with predefined values. | ||||
|      */ | ||||
|     public BattleshipAppConfig() { | ||||
|         // Default constructor | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the width of the game view resolution in pixels. | ||||
|      * | ||||
|      * @return the width of the game view resolution in pixels | ||||
|      */ | ||||
|     public int getResolutionWidth() { | ||||
|         return resolutionWidth; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the height of the game view resolution in pixels. | ||||
|      * | ||||
|      * @return the height of the game view resolution in pixels | ||||
|      */ | ||||
|     public int getResolutionHeight() { | ||||
|         return resolutionHeight; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns whether the game should start in full-screen mode. | ||||
|      * | ||||
|      * @return {@code true} if the game should start in full-screen mode; {@code false} otherwise | ||||
|      */ | ||||
|     public boolean fullScreen() { | ||||
|         return fullScreen; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns whether gamma correction is enabled. | ||||
|      * If enabled, the main framebuffer is configured for sRGB colors, | ||||
|      * and sRGB images are linearized. | ||||
|      * | ||||
|      * @return {@code true} if gamma correction is enabled; {@code false} otherwise | ||||
|      */ | ||||
|     public boolean useGammaCorrection() { | ||||
|         return useGammaCorrection; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns whether full resolution framebuffers should be used on Retina displays. | ||||
|      * This setting is ignored on non-Retina platforms. | ||||
|      * | ||||
|      * @return {@code true} if full resolution framebuffers should be used on Retina displays; {@code false} otherwise | ||||
|      */ | ||||
|     public boolean useRetinaFrameBuffer() { | ||||
|         return useRetinaFrameBuffer; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns whether the settings window should be shown for configuring the game. | ||||
|      * | ||||
|      * @return {@code true} if the settings window should be shown; {@code false} otherwise | ||||
|      */ | ||||
|     public boolean getShowSettings() { | ||||
|         return showSettings; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns whether the JME statistics window should be shown in the lower left corner of the screen. | ||||
|      * | ||||
|      * @return {@code true} if the statistics window should be shown; {@code false} otherwise | ||||
|      */ | ||||
|     public boolean getShowStatistics() { | ||||
|         return showStatistics; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the color of the top text during gameplay as a {@link ColorRGBA} object. | ||||
|      * | ||||
|      * @return the color of the top text during gameplay | ||||
|      */ | ||||
|     public ColorRGBA getTopColor() { | ||||
|         return topColor; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,102 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client; | ||||
|  | ||||
| import com.jme3.app.Application; | ||||
| import com.jme3.app.state.AbstractAppState; | ||||
| import com.jme3.app.state.AppStateManager; | ||||
| import pp.battleship.game.client.ClientGameLogic; | ||||
|  | ||||
| /** | ||||
|  * Abstract class representing a state in the Battleship game. | ||||
|  * Extends the AbstractAppState from jMonkeyEngine to manage state behavior. | ||||
|  */ | ||||
| public abstract class BattleshipAppState extends AbstractAppState { | ||||
|     private BattleshipApp app; | ||||
|  | ||||
|     /** | ||||
|      * Creates a new BattleshipAppState that is initially disabled. | ||||
|      * | ||||
|      * @see #setEnabled(boolean) | ||||
|      */ | ||||
|     protected BattleshipAppState() { | ||||
|         setEnabled(false); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the state manager and application. | ||||
|      * | ||||
|      * @param stateManager The state manager | ||||
|      * @param application  The application instance | ||||
|      */ | ||||
|     @Override | ||||
|     public void initialize(AppStateManager stateManager, Application application) { | ||||
|         super.initialize(stateManager, application); | ||||
|         this.app = (BattleshipApp) application; | ||||
|         if (isEnabled()) enableState(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the BattleshipApp instance associated with this BattleshipAppState. | ||||
|      * | ||||
|      * @return The BattleshipApp instance. | ||||
|      */ | ||||
|     public BattleshipApp getApp() { | ||||
|         return app; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the client game logic handler. | ||||
|      * | ||||
|      * @return the client game logic handler | ||||
|      */ | ||||
|     public ClientGameLogic getGameLogic() { | ||||
|         return app.getGameLogic(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks if any dialog is currently displayed. | ||||
|      * | ||||
|      * @return true if any dialog is currently shown, false otherwise | ||||
|      */ | ||||
|     public boolean showsDialog() { | ||||
|         return app.getDialogManager().showsDialog(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets the enabled state of the BattleshipAppState. | ||||
|      * If the new state is the same as the current state, the method returns. | ||||
|      * | ||||
|      * @param enabled The new enabled state. | ||||
|      */ | ||||
|     @Override | ||||
|     public void setEnabled(boolean enabled) { | ||||
|         if (isEnabled() == enabled) return; | ||||
|         super.setEnabled(enabled); | ||||
|         if (app != null) { | ||||
|             if (enabled) | ||||
|                 enableState(); | ||||
|             else | ||||
|                 disableState(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This method is called when the state is enabled. | ||||
|      * It is meant to be overridden by subclasses to perform | ||||
|      * specific actions when the state is enabled. | ||||
|      */ | ||||
|     protected abstract void enableState(); | ||||
|  | ||||
|     /** | ||||
|      * This method is called when the state is disabled. | ||||
|      * It is meant to be overridden by subclasses to perform | ||||
|      * specific actions when the state is disabled. | ||||
|      */ | ||||
|     protected abstract void disableState(); | ||||
| } | ||||
| @@ -0,0 +1,152 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client; | ||||
|  | ||||
| import com.jme3.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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,154 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.util.prefs.Preferences; | ||||
|  | ||||
| import com.simsilica.lemur.Button; | ||||
| import com.simsilica.lemur.Checkbox; | ||||
| import com.simsilica.lemur.Label; | ||||
| import com.simsilica.lemur.style.ElementId; | ||||
|  | ||||
| import static pp.battleship.Resources.lookup; | ||||
| import pp.battleship.client.gui.GameMusic; | ||||
| import pp.dialog.Dialog; | ||||
| import pp.dialog.StateCheckboxModel; | ||||
| import pp.dialog.TextInputDialog; | ||||
| import pp.battleship.client.gui.VolumeSlider; | ||||
|  | ||||
| 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")); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,177 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client; | ||||
|  | ||||
| import java.lang.System.Logger; | ||||
| import java.lang.System.Logger.Level; | ||||
| import java.util.concurrent.ExecutionException; | ||||
| import java.util.concurrent.Future; | ||||
|  | ||||
| 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 static pp.battleship.Resources.lookup; | ||||
| import pp.battleship.server.BattleshipServer; | ||||
| import pp.dialog.Dialog; | ||||
| import pp.dialog.DialogBuilder; | ||||
| import pp.dialog.SimpleDialog; | ||||
|  | ||||
| /** | ||||
|  * 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 = "42069"; //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(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,152 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client; | ||||
|  | ||||
| import com.jme3.network.Client; | ||||
| import com.jme3.network.ClientStateListener; | ||||
| import com.jme3.network.Message; | ||||
| import com.jme3.network.MessageListener; | ||||
| import com.jme3.network.Network; | ||||
| import pp.battleship.game.client.ServerConnection; | ||||
| import pp.battleship.message.client.ClientMessage; | ||||
| import pp.battleship.message.server.ServerMessage; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.lang.System.Logger; | ||||
| import java.lang.System.Logger.Level; | ||||
|  | ||||
| import static pp.battleship.Resources.lookup; | ||||
|  | ||||
| /** | ||||
|  * Manages the network connection for the Battleship application. | ||||
|  * Handles connecting to and disconnecting from the server, and sending messages. | ||||
|  */ | ||||
| class NetworkSupport implements MessageListener<Client>, ClientStateListener, ServerConnection { | ||||
|     private static final Logger LOGGER = System.getLogger(NetworkSupport.class.getName()); | ||||
|     private final BattleshipApp app; | ||||
|     private Client client; | ||||
|  | ||||
|     /** | ||||
|      * Constructs a NetworkSupport instance for the given Battleship application. | ||||
|      * | ||||
|      * @param app The Battleship application instance. | ||||
|      */ | ||||
|     public NetworkSupport(BattleshipApp app) { | ||||
|         this.app = app; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the Battleship application instance. | ||||
|      * | ||||
|      * @return Battleship application instance | ||||
|      */ | ||||
|     BattleshipApp getApp() { | ||||
|         return app; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks if there is a connection to the game server. | ||||
|      * | ||||
|      * @return true if there is a connection to the game server, false otherwise. | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean isConnected() { | ||||
|         return client != null && client.isConnected(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Attempts to join the game if there is no connection yet. | ||||
|      * Opens a dialog for the user to enter the host and port information. | ||||
|      */ | ||||
|     @Override | ||||
|     public void connect() { | ||||
|         if (client == null) | ||||
|             new NetworkDialog(this).open(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Closes the client connection. | ||||
|      */ | ||||
|     @Override | ||||
|     public void disconnect() { | ||||
|         if (client == null) return; | ||||
|         client.close(); | ||||
|         client = null; | ||||
|         LOGGER.log(Level.INFO, "client closed"); //NON-NLS | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the network connection. | ||||
|      * | ||||
|      * @param host The server's address. | ||||
|      * @param port The server's port. | ||||
|      * @throws IOException If an I/O error occurs when creating the client. | ||||
|      */ | ||||
|     void initNetwork(String host, int port) throws IOException { | ||||
|         if (client != null) | ||||
|             throw new IllegalStateException("trying to join a game again"); | ||||
|         client = Network.connectToServer(host, port); | ||||
|         client.start(); | ||||
|         client.addMessageListener(this); | ||||
|         client.addClientStateListener(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when a message is received from the server. | ||||
|      * | ||||
|      * @param client  The client instance that received the message. | ||||
|      * @param message The message received from the server. | ||||
|      */ | ||||
|     @Override | ||||
|     public void messageReceived(Client client, Message message) { | ||||
|         LOGGER.log(Level.INFO, "message received from server: {0}", message); //NON-NLS | ||||
|         if (message instanceof ServerMessage serverMessage) | ||||
|             app.enqueue(() -> serverMessage.accept(app.getGameLogic())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when the client has successfully connected to the server. | ||||
|      * | ||||
|      * @param client The client that connected to the server. | ||||
|      */ | ||||
|     @Override | ||||
|     public void clientConnected(Client client) { | ||||
|         LOGGER.log(Level.INFO, "Client connected: {0}", client); //NON-NLS | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when the client is disconnected from the server. | ||||
|      * | ||||
|      * @param client         The client that was disconnected. | ||||
|      * @param disconnectInfo Information about the disconnection. | ||||
|      */ | ||||
|     @Override | ||||
|     public void clientDisconnected(Client client, DisconnectInfo disconnectInfo) { | ||||
|         LOGGER.log(Level.INFO, "Client {0} disconnected: {1}", client, disconnectInfo); //NON-NLS | ||||
|         if (this.client != client) | ||||
|             throw new IllegalArgumentException("parameter value must be client"); | ||||
|         LOGGER.log(Level.INFO, "client still connected: {0}", client.isConnected()); //NON-NLS | ||||
|         this.client = null; | ||||
|         disconnect(); | ||||
|         app.enqueue(() -> app.setInfoText(lookup("lost.connection.to.server"))); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sends the specified message to the server. | ||||
|      * | ||||
|      * @param message The message to be sent to the server. | ||||
|      */ | ||||
|     @Override | ||||
|     public void send(ClientMessage message) { | ||||
|         LOGGER.log(Level.INFO, "sending {0}", message); //NON-NLS | ||||
|         if (client == null) | ||||
|             app.errorDialog(lookup("lost.connection.to.server")); | ||||
|         else | ||||
|             client.send(message); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,122 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client.gui; | ||||
|  | ||||
| import com.jme3.input.controls.ActionListener; | ||||
| import com.jme3.scene.Node; | ||||
| import com.jme3.system.AppSettings; | ||||
| import pp.battleship.client.BattleshipAppState; | ||||
| import pp.battleship.model.IntPoint; | ||||
|  | ||||
| import java.lang.System.Logger; | ||||
| import java.lang.System.Logger.Level; | ||||
|  | ||||
| import static pp.battleship.client.BattleshipApp.CLICK; | ||||
|  | ||||
| /** | ||||
|  * Represents the state responsible for managing the battle interface within the Battleship game. | ||||
|  * This state handles the display and interaction of the battle map, including the opponent's map. | ||||
|  * It manages GUI components, input events, and the layout of the interface when this state is enabled. | ||||
|  */ | ||||
| public class BattleAppState extends BattleshipAppState { | ||||
|     private static final Logger LOGGER = System.getLogger(BattleAppState.class.getName()); | ||||
|     private static final float DEPTH = 0f; | ||||
|     private static final float GAP = 20f; | ||||
|  | ||||
|     /** | ||||
|      * A listener for handling click events in the battle interface. | ||||
|      * When a click is detected, it triggers the corresponding actions on the opponent's map. | ||||
|      */ | ||||
|     private final ActionListener clickListener = (name, isPressed, tpf) -> click(isPressed); | ||||
|  | ||||
|     /** | ||||
|      * The root node for all GUI components in the battle state. | ||||
|      */ | ||||
|     private final Node battleNode = new Node("Battle"); //NON-NLS | ||||
|  | ||||
|     /** | ||||
|      * A view representing the opponent's map in the GUI. | ||||
|      */ | ||||
|     private MapView opponentMapView; | ||||
|  | ||||
|     /** | ||||
|      * Enables the battle state by initializing, laying out, and adding GUI components. | ||||
|      * Attaches the components to the GUI node and registers input listeners. | ||||
|      */ | ||||
|     @Override | ||||
|     protected void enableState() { | ||||
|         battleNode.detachAllChildren(); | ||||
|         initializeGuiComponents(); | ||||
|         layoutGuiComponents(); | ||||
|         addGuiComponents(); | ||||
|         getApp().getGuiNode().attachChild(battleNode); | ||||
|         getApp().getInputManager().addListener(clickListener, CLICK); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Disables the battle state by removing GUI components and unregistering input listeners. | ||||
|      * Also handles cleanup of resources, such as the opponent's map view. | ||||
|      */ | ||||
|     @Override | ||||
|     protected void disableState() { | ||||
|         getApp().getGuiNode().detachChild(battleNode); | ||||
|         getApp().getInputManager().removeListener(clickListener); | ||||
|         if (opponentMapView != null) { | ||||
|             opponentMapView.unregister(); | ||||
|             opponentMapView = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the GUI components used in the battle state. | ||||
|      * Creates the opponent's map view and adds a grid overlay to it. | ||||
|      */ | ||||
|     private void initializeGuiComponents() { | ||||
|         opponentMapView = new MapView(getGameLogic().getOpponentMap(), getApp()); | ||||
|         opponentMapView.addGrid(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds the initialized GUI components to the battle node. | ||||
|      * Currently, it attaches the opponent's map view to the node. | ||||
|      */ | ||||
|     private void addGuiComponents() { | ||||
|         battleNode.attachChild(opponentMapView.getNode()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Lays out the GUI components within the window, positioning them appropriately. | ||||
|      * The opponent's map view is positioned based on the window's dimensions and a specified gap. | ||||
|      */ | ||||
|     private void layoutGuiComponents() { | ||||
|         final AppSettings s = getApp().getContext().getSettings(); | ||||
|         final float mapWidth = opponentMapView.getWidth(); | ||||
|         final float mapHeight = opponentMapView.getHeight(); | ||||
|         final float windowWidth = s.getWidth(); | ||||
|         final float windowHeight = s.getHeight(); | ||||
|  | ||||
|         opponentMapView.getNode().setLocalTranslation(windowWidth - mapWidth - GAP, | ||||
|                                                       windowHeight - mapHeight - GAP, | ||||
|                                                       DEPTH); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handles click events in the battle interface. If the event indicates a click (not a release), | ||||
|      * it translates the cursor position to the model's coordinate system and triggers the game logic | ||||
|      * for interacting with the opponent's map. | ||||
|      * | ||||
|      * @param isPressed whether the mouse button is currently pressed (true) or released (false) | ||||
|      */ | ||||
|     private void click(boolean isPressed) { | ||||
|         if (!isPressed || showsDialog()) | ||||
|             return; | ||||
|         final IntPoint cursorPos = opponentMapView.mouseToModel(getApp().getInputManager().getCursorPosition()); | ||||
|         LOGGER.log(Level.DEBUG, "click: {0}", cursorPos); //NON-NLS | ||||
|         getGameLogic().clickOpponentMap(cursorPos); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,173 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client.gui; | ||||
|  | ||||
| import com.jme3.input.controls.ActionListener; | ||||
| import com.jme3.math.Vector2f; | ||||
| import com.jme3.scene.Node; | ||||
| import com.jme3.system.AppSettings; | ||||
| import com.simsilica.lemur.Button; | ||||
| import com.simsilica.lemur.Container; | ||||
| import pp.battleship.client.BattleshipAppState; | ||||
|  | ||||
| import java.lang.System.Logger; | ||||
| import java.lang.System.Logger.Level; | ||||
|  | ||||
| import static pp.battleship.Resources.lookup; | ||||
| import static pp.battleship.client.BattleshipApp.CLICK; | ||||
|  | ||||
| /** | ||||
|  * EditorState manages the editor mode in the Battleship game, | ||||
|  * allowing players to place and rotate ships. | ||||
|  */ | ||||
| public class EditorAppState extends BattleshipAppState { | ||||
|     private static final Logger LOGGER = System.getLogger(EditorAppState.class.getName()); | ||||
|     private static final float DEPTH = 0f; | ||||
|     private static final float GAP = 20f; | ||||
|  | ||||
|     private final ActionListener clickListener = (name, isPressed, tpf) -> click(isPressed); | ||||
|     private final Node editorNode = new Node("Editor"); //NON-NLS | ||||
|     private Container buttonContainer; | ||||
|     private Button rotateButton; | ||||
|     private Button readyButton; | ||||
|     private MapView ownMapView; | ||||
|     private MapView harborView; | ||||
|     private Vector2f oldCursorPosition; | ||||
|  | ||||
|     /** | ||||
|      * Enables the editor state by attaching necessary nodes and listeners. | ||||
|      */ | ||||
|     @Override | ||||
|     protected void enableState() { | ||||
|         editorNode.detachAllChildren(); | ||||
|         initializeGuiComponents(); | ||||
|         addGuiComponents(); | ||||
|         layoutGuiComponents(); | ||||
|         getApp().getGuiNode().attachChild(editorNode); | ||||
|         getApp().getInputManager().addListener(clickListener, CLICK); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Disables the editor state by detaching nodes and removing listeners. | ||||
|      */ | ||||
|     @Override | ||||
|     protected void disableState() { | ||||
|         getApp().getGuiNode().detachChild(editorNode); | ||||
|         getApp().getInputManager().removeListener(clickListener); | ||||
|         if (ownMapView != null) { | ||||
|             ownMapView.unregister(); | ||||
|             ownMapView = null; | ||||
|         } | ||||
|         if (harborView != null) { | ||||
|             harborView.unregister(); | ||||
|             harborView = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates the editor state, handling cursor movement and enabling buttons. | ||||
|      * | ||||
|      * @param tpf Time per frame | ||||
|      */ | ||||
|     @Override | ||||
|     public void update(float tpf) { | ||||
|         super.update(tpf); | ||||
|         cursorMovement(); | ||||
|         enableButtons(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Enables or disables buttons based on the logic state. | ||||
|      */ | ||||
|     private void enableButtons() { | ||||
|         readyButton.setEnabled(getGameLogic().isMapComplete()); | ||||
|         rotateButton.setEnabled(getGameLogic().movingShip()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handles cursor movement for previewing ship placement. | ||||
|      */ | ||||
|     private void cursorMovement() { | ||||
|         if (!getGameLogic().movingShip() || ownMapView == null || showsDialog()) | ||||
|             return; | ||||
|         final Vector2f cursorPosition = getApp().getInputManager().getCursorPosition(); | ||||
|         if (!cursorPosition.equals(oldCursorPosition)) { | ||||
|             oldCursorPosition = new Vector2f(cursorPosition); | ||||
|             getGameLogic().movePreview(ownMapView.mouseToModel(cursorPosition)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initializes the GUI components for the editor. | ||||
|      */ | ||||
|     private void initializeGuiComponents() { | ||||
|         ownMapView = new MapView(getGameLogic().getOwnMap(), getApp()); | ||||
|         harborView = new MapView(getGameLogic().getHarbor(), getApp()); | ||||
|         ownMapView.addGrid(); | ||||
|         rotateButton = new Button(lookup("button.rotate")); | ||||
|         readyButton = new Button(lookup("button.ready")); | ||||
|         rotateButton.addClickCommands(e -> { | ||||
|             if (!showsDialog()) | ||||
|                 getGameLogic().rotateShip(); | ||||
|         }); | ||||
|         readyButton.addClickCommands(e -> { | ||||
|             if (!showsDialog()) | ||||
|                 getGameLogic().mapFinished(); | ||||
|         }); | ||||
|         buttonContainer = new Container(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds the GUI components to the editor node. | ||||
|      */ | ||||
|     private void addGuiComponents() { | ||||
|         buttonContainer.addChild(rotateButton, 0, 0); | ||||
|         buttonContainer.addChild(readyButton, 0, 1); | ||||
|         editorNode.attachChild(ownMapView.getNode()); | ||||
|         editorNode.attachChild(harborView.getNode()); | ||||
|         editorNode.attachChild(buttonContainer); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Lays out the GUI components on the screen. | ||||
|      */ | ||||
|     private void layoutGuiComponents() { | ||||
|         final AppSettings s = getApp().getContext().getSettings(); | ||||
|         final float harborWidth = harborView.getWidth(); | ||||
|         final float harborHeight = harborView.getHeight(); | ||||
|         final float ownMapWidth = ownMapView.getWidth(); | ||||
|         final float ownMapHeight = ownMapView.getHeight(); | ||||
|         final float windowWidth = s.getWidth(); | ||||
|         final float windowHeight = s.getHeight(); | ||||
|  | ||||
|         ownMapView.getNode() | ||||
|                   .setLocalTranslation(0.5f * (windowWidth - harborWidth - ownMapWidth - GAP), | ||||
|                                        0.5f * (windowHeight - ownMapHeight), | ||||
|                                        DEPTH); | ||||
|  | ||||
|         harborView.getNode() | ||||
|                   .setLocalTranslation(0.5f * (windowWidth - harborWidth + ownMapWidth + GAP), | ||||
|                                        0.5f * (windowHeight - harborHeight), | ||||
|                                        DEPTH); | ||||
|  | ||||
|         buttonContainer.setLocalTranslation(0.5f * (windowWidth - harborWidth - ownMapWidth - GAP), | ||||
|                                             0.5f * (windowHeight - ownMapHeight - GAP), | ||||
|                                             DEPTH); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handles click events to place or rotate ships on the maps. | ||||
|      */ | ||||
|     private void click(boolean isPressed) { | ||||
|         if (!isPressed || showsDialog()) return; | ||||
|         final Vector2f cursorPos = getApp().getInputManager().getCursorPosition(); | ||||
|         LOGGER.log(Level.DEBUG, "click: {0}", cursorPos); //NON-NLS | ||||
|         getGameLogic().clickHarbor(harborView.mouseToModel(cursorPos)); | ||||
|         getGameLogic().clickOwnMap(ownMapView.mouseToModel(cursorPos)); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,122 @@ | ||||
| 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); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,191 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client.gui; | ||||
|  | ||||
| import com.jme3.material.Material; | ||||
| import com.jme3.material.RenderState.BlendMode; | ||||
| import com.jme3.math.ColorRGBA; | ||||
| import com.jme3.math.Vector2f; | ||||
| import com.jme3.math.Vector3f; | ||||
| import com.jme3.scene.Geometry; | ||||
| import com.jme3.scene.Node; | ||||
| import com.jme3.scene.Spatial.CullHint; | ||||
| import com.jme3.scene.shape.Quad; | ||||
| import pp.battleship.client.BattleshipApp; | ||||
| import pp.battleship.model.IntPoint; | ||||
| import pp.battleship.model.ShipMap; | ||||
| import pp.util.FloatPoint; | ||||
| import pp.util.Position; | ||||
|  | ||||
| /** | ||||
|  * Represents the visual view of a {@link ShipMap}, used to display the map structure such as the player's map, harbor, | ||||
|  * and opponent's map. This class handles the graphical representation of the map, including background setup, grid lines, | ||||
|  * and interaction between the model and the view. | ||||
|  */ | ||||
| class MapView { | ||||
|     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); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,154 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|     }       | ||||
| } | ||||
| @@ -0,0 +1,285 @@ | ||||
| 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; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,203 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client.gui; | ||||
|  | ||||
| import com.jme3.app.Application; | ||||
| import com.jme3.app.state.AppStateManager; | ||||
| import com.jme3.asset.AssetManager; | ||||
| import com.jme3.light.AmbientLight; | ||||
| import com.jme3.light.DirectionalLight; | ||||
| import com.jme3.material.Material; | ||||
| import com.jme3.math.ColorRGBA; | ||||
| import com.jme3.math.Vector2f; | ||||
| import com.jme3.math.Vector3f; | ||||
| import com.jme3.renderer.Camera; | ||||
| import com.jme3.renderer.queue.RenderQueue.ShadowMode; | ||||
| import com.jme3.scene.Geometry; | ||||
| import com.jme3.scene.Node; | ||||
| import com.jme3.scene.Spatial; | ||||
| import com.jme3.scene.shape.Box; | ||||
| import com.jme3.shadow.DirectionalLightShadowRenderer; | ||||
| import com.jme3.shadow.EdgeFilteringMode; | ||||
| import com.jme3.texture.Texture; | ||||
| import com.jme3.util.SkyFactory; | ||||
| import com.jme3.util.TangentBinormalGenerator; | ||||
| import pp.battleship.client.BattleshipAppState; | ||||
| import pp.battleship.model.ShipMap; | ||||
| import pp.util.FloatMath; | ||||
|  | ||||
| import static pp.util.FloatMath.TWO_PI; | ||||
| import static pp.util.FloatMath.cos; | ||||
| import static pp.util.FloatMath.sin; | ||||
| import static pp.util.FloatMath.sqrt; | ||||
|  | ||||
| /** | ||||
|  * Manages the rendering and visual aspects of the sea and sky in the Battleship game. | ||||
|  * This state is responsible for setting up and updating the sea, sky, and lighting | ||||
|  * conditions, and controls the camera to create a dynamic view of the game environment. | ||||
|  */ | ||||
| public class SeaAppState extends BattleshipAppState { | ||||
|     /** | ||||
|      * The path to the sea texture material. | ||||
|      */ | ||||
|     private static final String SEA_TEXTURE = "Textures/Terrain/Water/Water.j3m"; //NON-NLS | ||||
|     private static final float ABOVE_SEA_LEVEL = 4f; | ||||
|     private static final float INCLINATION = 2.5f; | ||||
|  | ||||
|     /** | ||||
|      * The root node for all visual elements in this state. | ||||
|      */ | ||||
|     private final Node viewNode = new Node("view"); //NON-NLS | ||||
|  | ||||
|     /** | ||||
|      * The node containing the scene elements, such as the sea surface. | ||||
|      */ | ||||
|     private final Node sceneNode = new Node("scene"); //NON-NLS | ||||
|  | ||||
|     /** | ||||
|      * Synchronizes the sea's visual representation with the game logic. | ||||
|      */ | ||||
|     private SeaSynchronizer synchronizer; | ||||
|  | ||||
|     /** | ||||
|      * The current angle of the camera around the center of the map. | ||||
|      */ | ||||
|     private float cameraAngle; | ||||
|  | ||||
|     /** | ||||
|      * Initializes the state by setting up the sky, lights, and other visual components. | ||||
|      * This method is called when the state is first attached to the state manager. | ||||
|      * | ||||
|      * @param stateManager the state manager | ||||
|      * @param application  the application | ||||
|      */ | ||||
|     @Override | ||||
|     public void initialize(AppStateManager stateManager, Application application) { | ||||
|         super.initialize(stateManager, application); | ||||
|         viewNode.attachChild(sceneNode); | ||||
|         setupLights(); | ||||
|         setupSky(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Enables the sea and sky state, setting up the scene and registering any necessary listeners. | ||||
|      * This method is called when the state is set to active. | ||||
|      */ | ||||
|     @Override | ||||
|     protected void enableState() { | ||||
|         sceneNode.detachAllChildren(); | ||||
|         setupScene(); | ||||
|         if (synchronizer == null) { | ||||
|             synchronizer = new SeaSynchronizer(getApp(), sceneNode, getGameLogic().getOwnMap()); | ||||
|             getGameLogic().addListener(synchronizer); | ||||
|         } | ||||
|         getApp().getRootNode().attachChild(viewNode); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Disables the sea and sky state, removing visual elements from the scene and unregistering listeners. | ||||
|      * This method is called when the state is set to inactive. | ||||
|      */ | ||||
|     @Override | ||||
|     protected void disableState() { | ||||
|         getApp().getRootNode().detachChild(viewNode); | ||||
|         if (synchronizer != null) { | ||||
|             getGameLogic().removeListener(synchronizer); | ||||
|             synchronizer = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates the state each frame, moving the camera to simulate it circling around the map. | ||||
|      * | ||||
|      * @param tpf the time per frame (seconds) | ||||
|      */ | ||||
|     @Override | ||||
|     public void update(float tpf) { | ||||
|         super.update(tpf); | ||||
|         cameraAngle += TWO_PI * 0.05f * tpf; | ||||
|         adjustCamera(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets up the lighting for the scene, including directional and ambient lights. | ||||
|      * Also configures shadows to enhance the visual depth of the scene. | ||||
|      */ | ||||
|     private void setupLights() { | ||||
|         final AssetManager assetManager = getApp().getAssetManager(); | ||||
|         final DirectionalLightShadowRenderer shRend = new DirectionalLightShadowRenderer(assetManager, 2048, 3); | ||||
|         shRend.setLambda(0.55f); | ||||
|         shRend.setShadowIntensity(0.6f); | ||||
|         shRend.setEdgeFilteringMode(EdgeFilteringMode.Bilinear); | ||||
|         getApp().getViewPort().addProcessor(shRend); | ||||
|  | ||||
|         final DirectionalLight sun = new DirectionalLight(); | ||||
|         sun.setDirection(new Vector3f(-1f, -0.7f, -1f).normalizeLocal()); | ||||
|         viewNode.addLight(sun); | ||||
|         shRend.setLight(sun); | ||||
|  | ||||
|         final AmbientLight ambientLight = new AmbientLight(new ColorRGBA(0.3f, 0.3f, 0.3f, 0f)); | ||||
|         viewNode.addLight(ambientLight); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets up the sky in the scene using a skybox with textures for all six directions. | ||||
|      * This creates a realistic and immersive environment for the sea. | ||||
|      */ | ||||
|     private void setupSky() { | ||||
|         final AssetManager assetManager = getApp().getAssetManager(); | ||||
|         final Texture west = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_west.jpg"); //NON-NLS | ||||
|         final Texture east = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_east.jpg"); //NON-NLS | ||||
|         final Texture north = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_north.jpg"); //NON-NLS | ||||
|         final Texture south = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_south.jpg"); //NON-NLS | ||||
|         final Texture up = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_up.jpg"); //NON-NLS | ||||
|         final Texture down = assetManager.loadTexture("Textures/Sky/Lagoon/lagoon_down.jpg"); //NON-NLS | ||||
|         final Spatial sky = SkyFactory.createSky(assetManager, west, east, north, south, up, down); | ||||
|         sky.rotate(0f, FloatMath.PI, 0f); | ||||
|         viewNode.attachChild(sky); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sets up the sea surface in the scene. This includes creating the sea mesh, | ||||
|      * applying textures, and enabling shadows. | ||||
|      */ | ||||
|     private void setupScene() { | ||||
|         final ShipMap ownMap = getGameLogic().getOwnMap(); | ||||
|         final float x = 0.5f * ownMap.getWidth(); | ||||
|         final float y = 0.5f * ownMap.getHeight(); | ||||
|         final Box seaMesh = new Box(y + 0.5f, 0.1f, x + 0.5f); | ||||
|         final Geometry seaGeo = new Geometry("sea", seaMesh); //NON-NLS | ||||
|         seaGeo.setLocalTranslation(new Vector3f(y, -0.1f, x)); | ||||
|         seaMesh.scaleTextureCoordinates(new Vector2f(4f, 4f)); | ||||
|         final Material seaMat = getApp().getAssetManager().loadMaterial(SEA_TEXTURE); | ||||
|         seaGeo.setMaterial(seaMat); | ||||
|         seaGeo.setShadowMode(ShadowMode.CastAndReceive); | ||||
|         TangentBinormalGenerator.generate(seaGeo); | ||||
|         sceneNode.attachChild(seaGeo); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adjusts the camera position and orientation to create a circular motion around | ||||
|      * the center of the map. This provides a dynamic view of the sea and surrounding environment. | ||||
|      */ | ||||
|     private void adjustCamera() { | ||||
|         final ShipMap ownMap = getGameLogic().getOwnMap(); | ||||
|         final float mx = 0.5f * ownMap.getWidth(); | ||||
|         final float my = 0.5f * ownMap.getHeight(); | ||||
|         final float radius = 2f * sqrt(mx * mx + my + my); | ||||
|         final float cos = radius * cos(cameraAngle); | ||||
|         final float sin = radius * sin(cameraAngle); | ||||
|         final float x = mx - cos; | ||||
|         final float y = my - sin; | ||||
|         final Camera camera = getApp().getCamera(); | ||||
|         camera.setLocation(new Vector3f(y, ABOVE_SEA_LEVEL, x)); | ||||
|         camera.getRotation().lookAt(new Vector3f(sin, -INCLINATION, cos), | ||||
|                                     Vector3f.UNIT_Y); | ||||
|         camera.update(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,378 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,57 @@ | ||||
| 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 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,58 @@ | ||||
| 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 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,106 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client.gui; | ||||
|  | ||||
| import com.jme3.math.Quaternion; | ||||
| import com.jme3.math.Vector3f; | ||||
| import com.jme3.renderer.RenderManager; | ||||
| import com.jme3.renderer.ViewPort; | ||||
| import com.jme3.scene.control.AbstractControl; | ||||
| import pp.battleship.model.Battleship; | ||||
|  | ||||
| import static pp.util.FloatMath.DEG_TO_RAD; | ||||
| import static pp.util.FloatMath.TWO_PI; | ||||
| import static pp.util.FloatMath.sin; | ||||
|  | ||||
| /** | ||||
|  * Controls the oscillating pitch motion of a battleship model in the game. | ||||
|  * The ship oscillates to simulate a realistic movement on water, based on its orientation and length. | ||||
|  */ | ||||
| class ShipControl extends AbstractControl { | ||||
|     /** | ||||
|      * The axis of rotation for the ship's pitch (tilting forward and backward). | ||||
|      */ | ||||
|     private final Vector3f axis; | ||||
|  | ||||
|     /** | ||||
|      * The duration of one complete oscillation cycle in seconds. | ||||
|      */ | ||||
|     private final float cycle; | ||||
|  | ||||
|     /** | ||||
|      * The amplitude of the pitch oscillation in radians, determining how much the ship tilts. | ||||
|      */ | ||||
|     private final float amplitude; | ||||
|  | ||||
|     /** | ||||
|      * A quaternion representing the ship's current pitch rotation. | ||||
|      */ | ||||
|     private final Quaternion pitch = new Quaternion(); | ||||
|  | ||||
|     /** | ||||
|      * The current time within the oscillation cycle, used to calculate the ship's pitch angle. | ||||
|      */ | ||||
|     private float time; | ||||
|  | ||||
|     /** | ||||
|      * Constructs a new ShipControl instance for the specified Battleship. | ||||
|      * The ship's orientation determines the axis of rotation, while its length influences | ||||
|      * the cycle duration and amplitude of the oscillation. | ||||
|      * | ||||
|      * @param ship the Battleship object to control | ||||
|      */ | ||||
|     public ShipControl(Battleship ship) { | ||||
|         // Determine the axis of rotation based on the ship's orientation | ||||
|         axis = switch (ship.getRot()) { | ||||
|             case LEFT, RIGHT -> Vector3f.UNIT_X; | ||||
|             case UP, DOWN -> Vector3f.UNIT_Z; | ||||
|         }; | ||||
|  | ||||
|         // Set the cycle duration and amplitude based on the ship's length | ||||
|         cycle = ship.getLength() * 2f; | ||||
|         amplitude = 5f * DEG_TO_RAD / ship.getLength(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates the ship's pitch oscillation each frame. The ship's pitch is adjusted | ||||
|      * to create a continuous tilting motion, simulating the effect of waves. | ||||
|      * | ||||
|      * @param tpf time per frame (in seconds), used to calculate the new pitch angle | ||||
|      */ | ||||
|     @Override | ||||
|     protected void controlUpdate(float tpf) { | ||||
|         // If spatial is null, do nothing | ||||
|         if (spatial == null) return; | ||||
|  | ||||
|         // Update the time within the oscillation cycle | ||||
|         time = (time + tpf) % cycle; | ||||
|  | ||||
|         // Calculate the current angle of the oscillation | ||||
|         final float angle = amplitude * sin(time * TWO_PI / cycle); | ||||
|  | ||||
|         // Update the pitch Quaternion with the new angle | ||||
|         pitch.fromAngleAxis(angle, axis); | ||||
|  | ||||
|         // Apply the pitch rotation to the spatial | ||||
|         spatial.setLocalRotation(pitch); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This method is called during the rendering phase, but it does not perform any | ||||
|      * operations in this implementation as the control only influences the spatial's | ||||
|      * transformation, not its rendering process. | ||||
|      * | ||||
|      * @param rm the RenderManager rendering the controlled Spatial (not null) | ||||
|      * @param vp the ViewPort being rendered (not null) | ||||
|      */ | ||||
|     @Override | ||||
|     protected void controlRender(RenderManager rm, ViewPort vp) { | ||||
|         // No rendering logic is needed for this control | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,89 @@ | ||||
| //////////////////////////////////////// | ||||
| // Programming project code | ||||
| // UniBw M, 2022, 2023, 2024 | ||||
| // www.unibw.de/inf2 | ||||
| // (c) Mark Minas (mark.minas@unibw.de) | ||||
| //////////////////////////////////////// | ||||
|  | ||||
| package pp.battleship.client.gui; | ||||
|  | ||||
| import com.jme3.scene.Node; | ||||
| import com.jme3.scene.Spatial; | ||||
| import pp.battleship.model.Item; | ||||
| import pp.battleship.model.ShipMap; | ||||
| import pp.battleship.model.Visitor; | ||||
| import pp.battleship.notification.GameEventListener; | ||||
| import pp.battleship.notification.ItemAddedEvent; | ||||
| import pp.battleship.notification.ItemRemovedEvent; | ||||
| import pp.view.ModelViewSynchronizer; | ||||
|  | ||||
| /** | ||||
|  * Abstract base class for synchronizing the visual representation of a {@link ShipMap} with its model state. | ||||
|  * This class handles the addition and removal of items from the ship map, ensuring that changes in the model | ||||
|  * are accurately reflected in the view. | ||||
|  * <p> | ||||
|  * Subclasses are responsible for providing the specific implementation of how each item in the map | ||||
|  * is represented visually by implementing the {@link Visitor} interface. | ||||
|  * </p> | ||||
|  */ | ||||
| abstract class ShipMapSynchronizer extends ModelViewSynchronizer<Item> implements Visitor<Spatial>, GameEventListener { | ||||
|     // The ship map that this synchronizer is responsible for | ||||
|     private final ShipMap shipMap; | ||||
|  | ||||
|     /** | ||||
|      * Constructs a new ShipMapSynchronizer. | ||||
|      * Initializes the synchronizer with the provided ship map and the root node for attaching view representations. | ||||
|      * | ||||
|      * @param map  the ship map to be synchronized | ||||
|      * @param root the root node to which the view representations of the ship map items are attached | ||||
|      */ | ||||
|     protected ShipMapSynchronizer(ShipMap map, Node root) { | ||||
|         super(root); | ||||
|         this.shipMap = map; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Translates a model item into its corresponding visual representation. | ||||
|      * The specific visual representation is determined by the concrete implementation of the {@link Visitor} interface. | ||||
|      * | ||||
|      * @param item the item from the model to be translated | ||||
|      * @return the visual representation of the item as a {@link Spatial} | ||||
|      */ | ||||
|     @Override | ||||
|     protected Spatial translate(Item item) { | ||||
|         return item.accept(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Adds the existing items from the ship map to the view. | ||||
|      * This method should be called during initialization to ensure that all current items in the ship map | ||||
|      * are visually represented. | ||||
|      */ | ||||
|     protected void addExisting() { | ||||
|         shipMap.getItems().forEach(this::add); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handles the event when an item is removed from the ship map. | ||||
|      * Removes the visual representation of the item from the view if it belongs to the synchronized ship map. | ||||
|      * | ||||
|      * @param event the event indicating that an item has been removed from the ship map | ||||
|      */ | ||||
|     @Override | ||||
|     public void receivedEvent(ItemRemovedEvent event) { | ||||
|         if (shipMap == event.map()) | ||||
|             delete(event.item()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handles the event when an item is added to the ship map. | ||||
|      * Adds the visual representation of the new item to the view if it belongs to the synchronized ship map. | ||||
|      * | ||||
|      * @param event the event indicating that an item has been added to the ship map | ||||
|      */ | ||||
|     @Override | ||||
|     public void receivedEvent(ItemAddedEvent event) { | ||||
|         if (shipMap == event.map()) | ||||
|             add(event.item()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,61 @@ | ||||
| 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 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,36 @@ | ||||
| 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); | ||||
|         } | ||||
|     } | ||||
|      | ||||
| } | ||||
| @@ -0,0 +1,182 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,17 @@ | ||||
| //////////////////////////////////////// | ||||
| // 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); | ||||
|     } | ||||
| } | ||||
| After Width: | Height: | Size: 16 KiB | 
| After Width: | Height: | Size: 46 KiB | 
| After Width: | Height: | Size: 63 KiB | 
| After Width: | Height: | Size: 2.0 KiB | 
| After Width: | Height: | Size: 16 KiB | 
| After Width: | Height: | Size: 32 KiB | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 28 KiB | 
| After Width: | Height: | Size: 16 KiB | 
| After Width: | Height: | Size: 168 KiB | 
| After Width: | Height: | Size: 56 KiB | 
| After Width: | Height: | Size: 166 KiB | 
| After Width: | Height: | Size: 98 KiB | 
| After Width: | Height: | Size: 35 KiB | 
| After Width: | Height: | Size: 54 KiB | 
| After Width: | Height: | Size: 9.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Projekte/battleship/client/src/main/resources/Models/CV/CV.j3o
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 66 KiB | 
| After Width: | Height: | Size: 3.2 KiB | 
| After Width: | Height: | Size: 30 KiB | 
| After Width: | Height: | Size: 10 KiB | 
| After Width: | Height: | Size: 9.4 KiB | 
| After Width: | Height: | Size: 264 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Projekte/battleship/client/src/main/resources/Models/CV/_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 80 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Projekte/battleship/client/src/main/resources/Models/CV/_6.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 91 KiB | 
| After Width: | Height: | Size: 33 KiB | 
| After Width: | Height: | Size: 98 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 78 B | 
| After Width: | Height: | Size: 79 B | 
| After Width: | Height: | Size: 96 KiB | 
| After Width: | Height: | Size: 82 KiB |