100 Commits

Author SHA1 Message Date
Filip Szepielewicz
a9e4afec08 Update 2 files
- /Projekte/monopoly/model/src/test/java/pp/monopoly/Testhandbuch.java
- /Projekte/monopoly/model/src/test/java/pp/monopoly/game/server/ServerGameLogicTest.java
2024-11-28 20:08:07 +00:00
Johannes Schmelz
6e5d9452b3 Merge branch 'main' into 'Testhandbuch'
# Conflicts:
#   Projekte/jme-common/src/main/resources/Interface/Lemur/pp-styles.groovy
#   Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyApp.java
#   Projekte/monopoly/model/src/test/java/pp/monopoly/client/ClientLogicTest.java
#   Projekte/monopoly/model/src/test/java/pp/monopoly/game/client/ClientGameLogicTest.java
2024-11-25 04:32:59 +00:00
Filip Szepielewicz
f1d52c445b Tests bis T075 überarbeitet 2024-11-22 21:45:22 +01:00
Johannes Schmelz
b85bbdd0ad updates gitignore for vscode 2024-11-19 21:29:48 +01:00
Johannes Schmelz
7246133363 refactor models 2024-11-18 17:10:36 +01:00
Johannes Schmelz
e38a7e8cdd Merge branch 'gui' into 'main'
Gui

See merge request progproj/gruppen-ht24/Gruppe-02!10
2024-11-18 15:46:18 +00:00
Johannes Schmelz
dbd9dd0001 refactor models 2024-11-18 16:43:54 +01:00
Dennis_Malkmus
ccb1743a01 Einfügen Übergangsmodell (6 gefärbte Spielsteine) 2024-11-18 14:57:02 +01:00
Dennis_Malkmus
4ac897693c Merge remote-tracking branch 'origin/gui' into gui 2024-11-18 14:56:48 +01:00
Dennis_Malkmus
dfb962e9fe Einfügen Übergangsmodell (6 gefärbte Spielsteine) 2024-11-18 14:53:51 +01:00
Luca Puderbach
f97cbd778d Hintergrund + Felder ID hinzugefügt 2024-11-18 09:09:05 +01:00
Luca Puderbach
e889a61d43 Merge branch 'gui' of https://athene2.informatik.unibw-muenchen.de/progproj/gruppen-ht24/Gruppe-02 into gui 2024-11-18 08:14:10 +01:00
Luca Puderbach
a33d422547 Protoyp Würfel eingefügt 2024-11-18 08:14:05 +01:00
Yvonne Schmidt
c4b4001b96 Merge remote-tracking branch 'origin/gui' into gui 2024-11-18 08:07:11 +01:00
Luca Puderbach
20949b641e Merge branch 'gui' of https://athene2.informatik.unibw-muenchen.de/progproj/gruppen-ht24/Gruppe-02 into gui 2024-11-18 08:05:35 +01:00
Luca Puderbach
56e2174b6d Figur ist jetzt flawless übers Spielfeld am flyen 2024-11-18 08:02:53 +01:00
Yvonne Schmidt
acfeebca14 Startmenü fertig 2024-11-18 07:52:47 +01:00
Yvonne Schmidt
cc47acbd3f Merge remote-tracking branch 'origin/gui' into gui 2024-11-18 07:27:21 +01:00
Yvonne Schmidt
869e16406d eigenes GUI Styling eingeführt 2024-11-18 07:26:31 +01:00
Luca Puderbach
1c99da4fc5 Hinzugefügt Kamera und 1. Spielfigut 2024-11-18 07:25:15 +01:00
Yvonne Schmidt
b74ca4e702 eigenes GUI Styling eingeführt 2024-11-18 07:04:44 +01:00
Luca Puderbach
5fde7451c8 Verbesserung TestWorld 2024-11-18 06:54:38 +01:00
Luca Puderbach
6fd9209336 Angleichen MonopolyApp 2024-11-18 05:58:18 +01:00
Luca Puderbach
0213dc3560 Merge branch 'gui' of https://athene2.informatik.unibw-muenchen.de/progproj/gruppen-ht24/Gruppe-02 into gui 2024-11-18 05:55:15 +01:00
Luca Puderbach
e73c434c1b Angleichen Monopolyapp 2024-11-18 05:55:10 +01:00
Johannes Schmelz
b135f3fa50 idk 2024-11-18 05:51:31 +01:00
Johannes Schmelz
0e580a3180 Figure Syncronizer now added 2024-11-18 05:38:04 +01:00
Yvonne Schmidt
8e3cb43244 eigenes GUI Styling eingeführt 2024-11-18 05:32:33 +01:00
Johannes Schmelz
6dcfb92dba resolve merge conflict 2024-11-18 05:07:41 +01:00
Johannes Schmelz
a7ea5773da resolve merge conflict 2024-11-18 05:05:37 +01:00
Johannes Schmelz
5b66131d30 Merge branch 'logic' into 'gui'
Refactor

See merge request progproj/gruppen-ht24/Gruppe-02!9
2024-11-18 04:01:41 +00:00
Johannes Schmelz
cac06dda7d Refactor 2024-11-18 04:01:41 +00:00
Johannes Schmelz
12c1c97a99 Merge branch 'logic' into 'gui'
# Conflicts:
#   Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientGameLogic.java
#   Projekte/monopoly/model/src/main/java/pp/monopoly/notification/Sound.java
2024-11-18 03:50:41 +00:00
Luca Puderbach
af4b4243ea Resolve merge conflicts 2024-11-18 04:42:30 +01:00
Johannes Schmelz
5189c74058 Merge branch 'gui' into 'main'
Gui

See merge request progproj/gruppen-ht24/Gruppe-02!7
2024-11-18 03:14:39 +00:00
Johannes Schmelz
fffd32e13c Merge branch 'main' into 'gui'
# Conflicts:
#   Projekte/jme-common/src/main/resources/Interface/Lemur/pp-styles.groovy
#   Projekte/monopoly/client/src/main/java/pp/monopoly/client/GameSound.java
#   Projekte/monopoly/client/src/main/java/pp/monopoly/client/MonopolyApp.java
#   Projekte/monopoly/client/src/main/java/pp/monopoly/client/StartMenu.java
#   Projekte/monopoly/model/src/main/java/pp/monopoly/game/client/ClientGameLogic.java
#   Projekte/monopoly/model/src/main/java/pp/monopoly/game/server/ServerGameLogic.java
#   Projekte/monopoly/model/src/main/resources/monopoly.properties
2024-11-18 03:14:07 +00:00
Johannes Schmelz
b37a5095f0 client recieve message logic 2024-11-18 03:20:39 +01:00
Johannes Schmelz
951b4c198c Server bug fix. Add player did not correspond with the params 2024-11-18 00:24:15 +01:00
Yvonne Schmidt
8698eed273 Tooolbar implementiert 2024-11-17 22:33:58 +01:00
Yvonne Schmidt
1af8614b35 Tooolbar in Testworld implementiert 2024-11-17 22:33:12 +01:00
Filip Szepielewicz
30e4298f88 Merge remote-tracking branch 'origin/Testhandbuch' into Testhandbuch 2024-11-17 20:38:30 +01:00
Filip Szepielewicz
7a9e84f49c Tests bis T071 erweitert 2024-11-17 20:06:47 +01:00
Filip Szepielewicz
7fce07ac19 Tests bis T070 erweitert 2024-11-17 20:00:05 +01:00
Johannes Schmelz
11faeeaf45 Merge branch 'Testhandbuch' into 'main'
Erste Tests

See merge request progproj/gruppen-ht24/Gruppe-02!5
2024-11-17 18:28:55 +00:00
Johannes Schmelz
9d0fcc3370 Erste Tests 2024-11-17 18:28:55 +00:00
Johannes Schmelz
2496ad812a Merge branch 'main' into 'Testhandbuch'
# Conflicts:
#   Projekte/monopoly/model/src/test/java/pp/monopoly/Testhandbuch.java
2024-11-17 18:27:54 +00:00
Johannes Schmelz
2f6d6037de Merge branch 'logic' into 'main'
Serverseitige Logik

See merge request progproj/gruppen-ht24/Gruppe-02!6
2024-11-17 18:25:32 +00:00
Johannes Schmelz
1d69bcc814 refactor and bug fixes 2024-11-17 17:43:05 +01:00
Johannes Schmelz
62c363068b fixed bug with client server messages 2024-11-16 18:57:52 +01:00
Johannes Schmelz
d2e0b8187b implemented PlayerStates 2024-11-16 18:42:05 +01:00
Johannes Schmelz
628b98af9b recive logic for messages 2024-11-16 18:03:19 +01:00
Johannes Schmelz
627e3dbd7f added fine to FineField 2024-11-16 17:53:07 +01:00
Filip Szepielewicz
f4fb04d17e T001 - T037 überarbeitet 2024-11-15 19:46:26 +01:00
Yvonne Schmidt
61b88f6bf8 Merge remote-tracking branch 'origin/gui' into gui 2024-11-15 18:39:47 +01:00
Yvonne Schmidt
8ab37e73c1 Anpassung des Button designs 2024-11-15 18:39:21 +01:00
Filip Szepielewicz
1b4fee2853 T001 - T034 erster versuch 2024-11-15 08:42:56 +01:00
Filip Szepielewicz
b65f458302 T001 - T034 erster versuch 2024-11-15 08:42:11 +01:00
Luca Puderbach
afdf43ebf1 Übergang zu TestMap verbessert 2024-11-15 08:36:16 +01:00
Luca Puderbach
2e0d1c059d TestWord eingefügt 2024-11-15 07:00:55 +01:00
Yvonne Schmidt
2b772199b0 Merge remote-tracking branch 'origin/gui' into gui
# Conflicts:
#	Projekte/monopoly/client/src/main/java/pp/monopoly/client/StartMenu.java
2024-11-15 06:49:13 +01:00
Yvonne Schmidt
c37b850798 Buttons im Startmenü angepasst 2024-11-15 06:48:25 +01:00
Luca Puderbach
c31f924d77 CreateGame fertig für feinschliff (Icon test) 2024-11-15 06:25:21 +01:00
Luca Puderbach
ed87a6167d Verbesserung SettingsMenu 2024-11-15 05:36:57 +01:00
Yvonne Schmidt
0d86ba0ca9 Merge remote-tracking branch 'origin/gui' into gui
# Conflicts:
#	Projekte/monopoly/client/src/main/java/pp/monopoly/client/StartMenu.java
2024-11-15 05:12:39 +01:00
Luca Puderbach
f2fd283d06 Positionierung StartMenu Buttons & SettingsMenu 2024-11-15 05:04:25 +01:00
Johannes Schmelz
10b978debf fixed addPlayer 2024-11-15 04:27:32 +01:00
Luca Puderbach
85756713df Erweitern des Setting und CreateGameMenü 2024-11-15 03:23:27 +01:00
Johannes Schmelz
3bdfd6a78a TradeHandler logic 2024-11-15 03:19:48 +01:00
Johannes Schmelz
e59ab4a320 cleintmessages logic 2024-11-15 03:06:07 +01:00
Luca Puderbach
15b3902bd3 SettingsMenü Anpassungen 2024-11-15 02:31:12 +01:00
Yvonne Schmidt
b622d66942 Buttons im Startmenü ausgerichtet 2024-11-15 02:28:10 +01:00
Luca Puderbach
12ef219064 Änderungs des StartBildschirms 2024-11-15 01:25:09 +01:00
Luca Puderbach
853b52b52d Implementierung eines grundlegenden Startmenüs 2024-11-15 00:39:47 +01:00
Johannes Schmelz
7a2ad1d31a corrected server states 2024-11-14 23:50:18 +01:00
Johannes Schmelz
c5ad476eaf added rent payment logic
added player on field logic
2024-11-14 23:50:06 +01:00
Filip Szepielewicz
c792a8b3fb T001 - T034 erster versuch 2024-11-14 21:58:47 +01:00
Filip Szepielewicz
12978ff410 T001 - T034 erster versuch 2024-11-14 21:54:27 +01:00
Yvonne Schmidt
caa45097c3 klassen angepasst 2024-11-14 21:31:47 +01:00
Johannes Schmelz
232e3a117c corrected rent prices 2024-11-14 01:08:22 +01:00
Johannes Schmelz
3668382911 FineField in Player 2024-11-13 23:50:06 +01:00
Johannes Schmelz
fafa53ffb7 rent calculation 2024-11-13 23:49:37 +01:00
Johannes Schmelz
44673fd57e createBoard method 2024-11-13 23:30:04 +01:00
Johannes Schmelz
f7149f225c Interpreter fuer messages hinzugefuegt 2024-11-13 18:54:24 +01:00
Johannes Schmelz
6773e18d34 Message Klassen hinzugefuegt 2024-11-13 14:11:20 +01:00
Johannes Schmelz
19a9b06f3c Merge branch 'gui' into 'main'
First compileable version of Client

See merge request progproj/gruppen-ht24/Gruppe-02!4
2024-11-13 12:42:37 +00:00
Johannes Schmelz
7ee2273761 First compileable version of Client 2024-11-13 12:42:37 +00:00
Johannes Schmelz
86b8297c9d fixed bugs, game now compiles 2024-11-13 13:31:02 +01:00
Yvonne Schmidt
17533112a1 Startmenü implementiert und Spielmenü Stummel erstellt 2024-11-12 23:30:44 +01:00
Johannes Schmelz
2cc1a338ec added Figure to Player for visual representation 2024-11-12 22:36:02 +01:00
Johannes Schmelz
81731247c7 added Fields 2024-11-12 22:35:41 +01:00
Johannes Schmelz
65c85aacf0 started UC-gameplay-15 2024-11-12 22:34:46 +01:00
Johannes Schmelz
dca23151a8 added Visitors 2024-11-12 22:33:10 +01:00
Johannes Schmelz
25305760c5 added Player 2024-11-12 22:32:55 +01:00
Yvonne Schmidt
3ef2459ad0 Hintergrundfarbe der Menüs zu Weiß geändert 2024-11-12 21:32:46 +01:00
Johannes Schmelz
29a56f42a8 Merge branch 'main' of https://athene2.informatik.unibw-muenchen.de/progproj/gruppen-ht24/Gruppe-02 2024-11-12 14:53:08 +01:00
Johannes Schmelz
3784348f91 init for monopoly 2024-11-12 14:53:04 +01:00
Johannes Schmelz
188ec03abd working version of battleship 2024-11-12 00:37:05 +01:00
Johannes Schmelz
f47cda7dc4 Merge branch 'b_schmelz_johannes' into 'main'
Revert "branched"

See merge request progproj/gruppen-ht24/Gruppe-02!1
2024-10-03 14:03:23 +00:00
Johannes Schmelz
c7269f032f Revert "branched"
This reverts commit 4581e611ca
2024-10-03 13:59:48 +00:00
Johannes Schmelz
4581e611ca branched 2024-10-03 15:55:58 +02:00
187 changed files with 9669 additions and 1429141 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ build
# VSC # VSC
bin bin
.vscode
# IntelliJ # IntelliJ
*.iml *.iml

View File

@@ -7,18 +7,14 @@ description = 'Battleship Client'
dependencies { dependencies {
implementation project(":jme-common") implementation project(":jme-common")
implementation project(":battleship:model") implementation project(":battleship:model")
implementation 'org.jmonkeyengine:jme3-core:3.6.0-stable'
implementation 'org.jmonkeyengine:jme3-effects:3.6.0-stable'
implementation libs.jme3.desktop implementation libs.jme3.desktop
implementation libs.jme3.effects
runtimeOnly libs.jme3.awt.dialogs runtimeOnly libs.jme3.awt.dialogs
runtimeOnly libs.jme3.plugins runtimeOnly libs.jme3.plugins
runtimeOnly libs.jme3.jogg runtimeOnly libs.jme3.jogg
runtimeOnly libs.jme3.testdata runtimeOnly libs.jme3.testdata
} }
application { application {

View File

@@ -22,8 +22,8 @@ import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.style.BaseStyles; import com.simsilica.lemur.style.BaseStyles;
import pp.battleship.client.gui.BattleAppState; import pp.battleship.client.gui.BattleAppState;
import pp.battleship.client.gui.EditorAppState; import pp.battleship.client.gui.EditorAppState;
import pp.battleship.client.gui.GameMusic;
import pp.battleship.client.gui.SeaAppState; import pp.battleship.client.gui.SeaAppState;
import pp.battleship.client.GameMusic;
import pp.battleship.game.client.BattleshipClient; import pp.battleship.game.client.BattleshipClient;
import pp.battleship.game.client.ClientGameLogic; import pp.battleship.game.client.ClientGameLogic;
import pp.battleship.game.client.ServerConnection; import pp.battleship.game.client.ServerConnection;
@@ -269,7 +269,6 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
attachGameSound(); attachGameSound();
attachGameMusic(); attachGameMusic();
stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState()); stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
} }
@@ -284,12 +283,12 @@ public class BattleshipApp extends SimpleApplication implements BattleshipClient
} }
/** /**
* Attaches the music state and sets its initial enabled state. * Attaches the background music state and sets its initial enabled state.
*/ */
private void attachGameMusic() { private void attachGameMusic() {
final GameMusic gameMusic = new GameMusic(); final GameMusic gameSound = new GameMusic();
gameMusic.setEnabled(GameMusic.enabledInPreferences()); gameSound.setEnabled(GameMusic.enabledInPreferences());
stateManager.attach(gameMusic); stateManager.attach(gameSound);
} }
/** /**

View File

@@ -34,6 +34,7 @@ public class GameSound extends AbstractAppState implements GameEventListener {
private AudioNode splashSound; private AudioNode splashSound;
private AudioNode shipDestroyedSound; private AudioNode shipDestroyedSound;
private AudioNode explosionSound; private AudioNode explosionSound;
private AudioNode shellFlyingSound;
/** /**
* Checks if sound is enabled in the preferences. * Checks if sound is enabled in the preferences.
@@ -78,6 +79,7 @@ public class GameSound extends AbstractAppState implements GameEventListener {
shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS shipDestroyedSound = loadSound(app, "Sound/Effects/sunken.wav"); //NON-NLS
splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS splashSound = loadSound(app, "Sound/Effects/splash.wav"); //NON-NLS
explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS explosionSound = loadSound(app, "Sound/Effects/explosion.wav"); //NON-NLS
shellFlyingSound = loadSound(app, "Sound/Effects/shell_flying.wav");
} }
/** /**
@@ -124,12 +126,27 @@ public class GameSound extends AbstractAppState implements GameEventListener {
shipDestroyedSound.playInstance(); 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 @Override
public void receivedEvent(SoundEvent event) { public void receivedEvent(SoundEvent event) {
switch (event.sound()) { switch (event.sound()) {
case EXPLOSION -> explosion(); case EXPLOSION -> explosion();
case SPLASH -> splash(); case SPLASH -> splash();
case DESTROYED_SHIP -> shipDestroyed(); case DESTROYED_SHIP -> shipDestroyed();
case SHELL_FLYING -> shellFly();
} }
} }
} }

View File

@@ -7,20 +7,21 @@
package pp.battleship.client; package pp.battleship.client;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.style.ElementId;
import pp.battleship.client.gui.VolumeControl;
import pp.dialog.Dialog;
import pp.dialog.StateCheckboxModel;
import pp.dialog.TextInputDialog;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.prefs.Preferences; 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 static pp.battleship.Resources.lookup;
import pp.battleship.client.gui.GameMusic;
import pp.battleship.client.gui.VolumeSlider;
import pp.dialog.Dialog;
import pp.dialog.StateCheckboxModel;
import pp.dialog.TextInputDialog;
import static pp.util.PreferencesUtils.getPreferences; import static pp.util.PreferencesUtils.getPreferences;
/** /**
@@ -34,35 +35,29 @@ class Menu extends Dialog {
private final BattleshipApp app; private final BattleshipApp app;
private final Button loadButton = new Button(lookup("menu.map.load")); private final Button loadButton = new Button(lookup("menu.map.load"));
private final Button saveButton = new Button(lookup("menu.map.save")); private final Button saveButton = new Button(lookup("menu.map.save"));
private final VolumeControl volumeSlider; private final VolumeSlider slider;
/** /**
* Constructs the Menu dialog for the Battleship application. * Constructs the Menu dialog for the Battleship application.+
* *
* @param app the BattleshipApp instance * @param app the BattleshipApp instance
*/ */
public Menu(BattleshipApp app) { public Menu(BattleshipApp app) {
super(app.getDialogManager()); super(app.getDialogManager());
this.app = app; this.app = app;
slider = new VolumeSlider(app.getStateManager().getState(GameMusic.class));
addChild(new Label(lookup("battleship.name"), new ElementId("header")));//NON-NLS 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.sound-enabled"), new StateCheckboxModel(app, GameSound.class)));
addChild(new Checkbox(lookup("menu.background-sound-enabled"), new StateCheckboxModel(app, GameMusic.class))); addChild(new Checkbox(lookup("menu.background-sound-enabled"), new StateCheckboxModel(app, GameMusic.class)));
volumeSlider = new VolumeControl(app.getStateManager().getState(GameMusic.class)); addChild(slider);
addChild(volumeSlider); addChild(loadButton).addClickCommands(s -> ifTopDialog(this::loadDialog));
addChild(saveButton).addClickCommands(s -> ifTopDialog(this::saveDialog));
addChild(new Button(lookup("menu.return-to-game"))).addClickCommands(s -> ifTopDialog(this::close));
addChild(new Button(lookup("menu.quit"))).addClickCommands(s -> ifTopDialog(app::closeApp));
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(); update();
} }
@@ -75,15 +70,11 @@ class Menu extends Dialog {
saveButton.setEnabled(app.getGameLogic().maySaveMap()); saveButton.setEnabled(app.getGameLogic().maySaveMap());
} }
/**
* Updates the position of the volume control slider.
*/
@Override @Override
public void update(float delta) { public void update(float delta) {
volumeSlider.update(); slider.update();
} }
/** /**
* As an escape action, this method closes the menu if it is the top dialog. * As an escape action, this method closes the menu if it is the top dialog.
*/ */

View File

@@ -7,12 +7,13 @@
package pp.battleship.client; package pp.battleship.client;
import com.simsilica.lemur.Checkbox; import com.simsilica.lemur.Button;
import com.simsilica.lemur.Container; import com.simsilica.lemur.Container;
import com.simsilica.lemur.Label; import com.simsilica.lemur.Label;
import com.simsilica.lemur.TextField; import com.simsilica.lemur.TextField;
import com.simsilica.lemur.component.SpringGridLayout; import com.simsilica.lemur.component.SpringGridLayout;
import pp.battleship.client.server.BattleshipSelfhostServer;
import pp.battleship.server.BattleshipServer;
import pp.dialog.Dialog; import pp.dialog.Dialog;
import pp.dialog.DialogBuilder; import pp.dialog.DialogBuilder;
import pp.dialog.SimpleDialog; import pp.dialog.SimpleDialog;
@@ -35,7 +36,8 @@ class NetworkDialog extends SimpleDialog {
private final NetworkSupport network; private final NetworkSupport network;
private final TextField host = new TextField(LOCALHOST); private final TextField host = new TextField(LOCALHOST);
private final TextField port = new TextField(DEFAULT_PORT); private final TextField port = new TextField(DEFAULT_PORT);
private static final Checkbox HOST = new Checkbox(lookup("server.host")); // private final Button serverButton = new Button(lookup("client.server-star"));
private final Button serverButton = new Button(lookup("client.server-start"));
private String hostname; private String hostname;
private int portNumber; private int portNumber;
private Future<Object> connectionFuture; private Future<Object> connectionFuture;
@@ -59,7 +61,6 @@ class NetworkDialog extends SimpleDialog {
input.addChild(host, 1); input.addChild(host, 1);
input.addChild(new Label(lookup("port.number") + ": ")); input.addChild(new Label(lookup("port.number") + ": "));
input.addChild(port, 1); input.addChild(port, 1);
input.addChild(HOST).addClickCommands(s -> ifTopDialog(this::startClientServer));
DialogBuilder.simple(app.getDialogManager()) DialogBuilder.simple(app.getDialogManager())
.setTitle(lookup("server.dialog")) .setTitle(lookup("server.dialog"))
@@ -69,6 +70,9 @@ class NetworkDialog extends SimpleDialog {
.setOkClose(false) .setOkClose(false)
.setNoClose(false) .setNoClose(false)
.build(this); .build(this);
//Add the button to start the sever
addChild(serverButton).addClickCommands(s -> ifTopDialog(this::startServerInThread));
} }
/** /**
@@ -156,15 +160,15 @@ class NetworkDialog extends SimpleDialog {
} }
/** /**
* Starts the server of the client in a new thread and catches connectivity issues. * Starts the server in a separate thread.
*/ */
private void startClientServer() { private void startServerInThread() {
HOST.setEnabled(false); serverButton.setEnabled(false);
Thread serverThread = new Thread(() -> { Thread serverThread = new Thread(() -> {
try { try {
BattleshipSelfhostServer.main(null); BattleshipServer.main(null);
} catch (Exception e) { } catch (Exception e) {
HOST.setEnabled(true); serverButton.setEnabled(true);
LOGGER.log(Level.ERROR, "Server could not be started", e); LOGGER.log(Level.ERROR, "Server could not be started", e);
network.getApp().errorDialog("Could not start server: " + e.getMessage()); network.getApp().errorDialog("Could not start server: " + e.getMessage());
} }
@@ -172,4 +176,3 @@ class NetworkDialog extends SimpleDialog {
serverThread.start(); serverThread.start();
} }
} }

View File

@@ -1,5 +1,10 @@
package pp.battleship.client; 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.Application;
import com.jme3.app.state.AbstractAppState; import com.jme3.app.state.AbstractAppState;
@@ -9,25 +14,16 @@ import com.jme3.asset.AssetNotFoundException;
import com.jme3.audio.AudioData; import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode; import com.jme3.audio.AudioNode;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.prefs.Preferences;
import static pp.util.PreferencesUtils.getPreferences;
/** /**
* An application state that plays music. * 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 { public class GameMusic extends AbstractAppState{
private static final Logger LOGGER = System.getLogger(GameMusic.class.getName()); private static final Logger LOGGER = System.getLogger(GameMusic.class.getName());
private static final Preferences PREFERENCES = getPreferences(GameMusic.class); private static final Preferences PREFERENCES = getPreferences(GameMusic.class);
private static final String ENABLED_PREF = "enabled"; //NON-NLS private static final String ENABLED_PREF = "enabled"; //NON-NLS
private static final String VOLUME_PREF = "volume"; //NON-NLS private static final String VOLUME_PREF = "volume"; //NON-NLS
private AudioNode battleMusic; private AudioNode music;
private AudioNode menuMusicModern;
private AudioNode menuMusicTraditional;
/** /**
* Checks if sound is enabled in the preferences. * Checks if sound is enabled in the preferences.
@@ -37,47 +33,18 @@ public class GameMusic extends AbstractAppState {
public static boolean enabledInPreferences() { public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true); return PREFERENCES.getBoolean(ENABLED_PREF, true);
} }
/**
* Checks enabled volume in preferences.
*
* @return {@code float} if a volumePreference is set, the volume is set to the Value in PREFERENCES,
* {@code 0.25f} if no volumePreference is set in PREFERENCES, the Volume is set to a default of 0.25f
*
*/
public static float volumePreference() { /**
return PREFERENCES.getFloat(VOLUME_PREF, 0.25f); * 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);
} }
/** /**
* Toggles the game sound on or off. * Initializes the sound effects for the game.
*/
public void toggleSound() {
setEnabled(!isEnabled());
}
/**
* Sets the enabled state of this AppState.
* Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
*
* @param enabled {@code true} to enable the AppState, {@code false} to disable it.
*/
@Override
public void setEnabled(boolean enabled) {
if (isEnabled() == enabled) return;
else if (!isEnabled() && enabled) {
if (menuMusicModern != null) menuMusicModern.play();
} else if (isEnabled() && !enabled) {
if (menuMusicModern != null) menuMusicModern.stop();
}
super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Music enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
}
/**
* Initializes the music.
* Overrides {@link AbstractAppState#initialize(AppStateManager, Application)} * Overrides {@link AbstractAppState#initialize(AppStateManager, Application)}
* *
* @param stateManager The state manager * @param stateManager The state manager
@@ -86,17 +53,16 @@ public class GameMusic extends AbstractAppState {
@Override @Override
public void initialize(AppStateManager stateManager, Application app) { public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app); super.initialize(stateManager, app);
menuMusicModern =loadSound(app, "Sound/BackgroundMusic/menu-music-modern.ogg"); music = loadSound(app, "Sound/background.ogg");
setVolume(volumePreference()); setVolume(volumeInPreferences());
menuMusicModern.setLooping(true); music.setLooping(true);
if (isEnabled() && menuMusicModern != null) { if (isEnabled() && music != null) {
menuMusicModern.play(); music.play();
} }
} }
/** /**
* Loads the music from the specified file. * Loads a sound from the specified file.
* *
* @param app The application * @param app The application
* @param name The name of the sound file. * @param name The name of the sound file.
@@ -116,14 +82,41 @@ public class GameMusic extends AbstractAppState {
} }
/** /**
* Sets the vol param to the level set in PREFERENCES * Sets the enabled state of this AppState.
* * Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
* @param vol Volume level of the music as indicated by the Volume control Slider
* *
* @param enabled {@code true} to enable the AppState, {@code false} to disable it.
*/ */
public void setVolume(float vol){ @Override
menuMusicModern.setVolume(vol); public void setEnabled(boolean enabled) {
PREFERENCES.putFloat(VOLUME_PREF, vol); 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);
}
} }

View File

@@ -15,6 +15,7 @@ import com.jme3.scene.Geometry;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Sphere; import com.jme3.scene.shape.Sphere;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import pp.battleship.model.Shot; import pp.battleship.model.Shot;
@@ -22,6 +23,7 @@ import pp.util.Position;
import static com.jme3.material.Materials.UNSHADED; import static com.jme3.material.Materials.UNSHADED;
/** /**
* Synchronizes the visual representation of the ship map with the game model. * 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 * It handles the rendering of ships and shots on the map view, updating the view
@@ -130,7 +132,7 @@ class MapViewSynchronizer extends ShipMapSynchronizer {
return view.getApp().getDraw().makeFatLine(x1, y1, x2, y2, SHIP_DEPTH, color, SHIP_LINE_WIDTH); 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 * 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. * for 2D visualization in the game. The shell is represented as a circle.
* *
@@ -149,5 +151,4 @@ class MapViewSynchronizer extends ShipMapSynchronizer {
ellipse.addControl(new Shell2DControl(view, shell)); ellipse.addControl(new Shell2DControl(view, shell));
return ellipse; return ellipse;
} }
} }

View File

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

View File

@@ -8,13 +8,9 @@
package pp.battleship.client.gui; package pp.battleship.client.gui;
import com.jme3.effect.ParticleEmitter; 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.material.Material;
import com.jme3.material.RenderState.BlendMode; import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.ShadowMode; import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Node; import com.jme3.scene.Node;
@@ -34,16 +30,16 @@ import static pp.util.FloatMath.PI;
/** /**
* The {@code SeaSynchronizer} class is responsible for synchronizing the graphical * The {@code SeaSynchronizer} class is responsible for synchronizing the graphical
* representation of the ships and shots on the sea map with the underlying model. * representation of the ships and shots on the sea map with the underlying data model.
* It extends the {@link ShipMapSynchronizer} to provide specific synchronization * It extends the {@link ShipMapSynchronizer} to provide specific synchronization
* logic for the sea map. * logic for the sea map.
*/ */
class SeaSynchronizer extends ShipMapSynchronizer { class SeaSynchronizer extends ShipMapSynchronizer {
private static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md"; //NON-NLS 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 KING_GEORGE_V_MODEL = "Models/KingGeorgeV/KingGeorgeV.j3o"; //NON-NLS
private static final String SMALL_BOAT_MODEL = "Models/BoatSmall/12219_boat_v2_L2.j3o"; private static final String BOAT_SMALL_MODEL = "Models/BoatSmall/12219_boat_v2_L2.j3o"; //NON-NLS
private static final String BATTLE_MODEL = "Models/Battle/Battle.j3o"; //NON-NLS
private static final String CV_MODEL = "Models/CV/CV.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 LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
private static final String COLOR = "Color"; //NON-NLS private static final String COLOR = "Color"; //NON-NLS
private static final String SHIP = "ship"; //NON-NLS private static final String SHIP = "ship"; //NON-NLS
@@ -54,273 +50,7 @@ class SeaSynchronizer extends ShipMapSynchronizer {
private final ShipMap map; private final ShipMap map;
private final BattleshipApp app; private final BattleshipApp app;
private Shell shell; private final ParticleEffectFactory particleFactory;
private Spatial shellModel;
private ParticleEmitter flame, flash, spark, roundspark, smoketrail, debris,
shockwave;
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;
/**
* Creates a Flame texture with indefinite Lifetime
* @return flame texture
*/
private ParticleEmitter createFlame(){
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 texture
* @return flash texture
*/
private ParticleEmitter createFlash(){
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 small spark particles, that dissipate into the air
* @return spark texture
*/
private ParticleEmitter createRoundSpark(){
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 small, thin smoke trails that enhance the flying debris
* @return crates a thin smoke trail texture
*/
private ParticleEmitter createSpark(){
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 dynamic smoke trail
* @return smoke texture
*/
private ParticleEmitter createSmokeTrail(){
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.setShape(new EmitterSphereShape(Vector3f.ZERO, 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 flying debris particles
* @return debris texture
*/
private ParticleEmitter createDebris(){
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.setShape(new EmitterSphereShape(Vector3f.ZERO, .05f));
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 an expanding circular shockwave
* @return shockwave texture
*/
private ParticleEmitter createShockwave(){
shockwave = new ParticleEmitter("Shockwave", Type.Triangle, 1 * COUNT_FACTOR);
// shockwave.setRandomAngle(true);
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 an animated smoke column.
* @return moving smoke texture
*/
private 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
// Set the material for the emitter
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 small burst of bubbles.
*
* @return bubbling water texture.
*/
public ParticleEmitter createWaterSplash() {
ParticleEmitter waterSplash = new ParticleEmitter("WaterSplash", Type.Triangle, 30);
waterSplash.setShape(new EmitterSphereShape(Vector3f.ZERO, 0.2f));
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
waterSplash.setStartSize(0.05f);
waterSplash.setEndSize(0.1f);
waterSplash.setLowLife(0.5f);
waterSplash.setHighLife(1f);
waterSplash.setGravity(0, 0, 0); // No gravity to simulate a horizontal water splash
waterSplash.getParticleInfluencer().setInitialVelocity(new Vector3f(1f, 0f, 1f)); // Horizontal spread
waterSplash.getParticleInfluencer().setVelocityVariation(1f); // Add randomness to splash
waterSplash.setParticlesPerSec(0);
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;
}
/** /**
* Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map. * Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map.
@@ -333,6 +63,7 @@ class SeaSynchronizer extends ShipMapSynchronizer {
super(app.getGameLogic().getOwnMap(), root); super(app.getGameLogic().getOwnMap(), root);
this.app = app; this.app = app;
this.map = map; this.map = map;
this.particleFactory = new ParticleEffectFactory(app);
addExisting(); addExisting();
} }
@@ -349,13 +80,49 @@ class SeaSynchronizer extends ShipMapSynchronizer {
return shot.isHit() ? handleHit(shot) : handleMiss(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 * 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. * contains the ship model as a child so that it moves with the ship.
* *
* @param shot a hit * @param shot a hit
* @return always null to prevent the representation from being attached * @return always null to prevent the representation from being attached to the items node as well
* to the items node as well
*/ */
private Spatial handleHit(Shot shot) { private Spatial handleHit(Shot shot) {
final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship"); final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship");
@@ -367,14 +134,14 @@ class SeaSynchronizer extends ShipMapSynchronizer {
Node hitEffectNode = new Node("HitEffectNode"); Node hitEffectNode = new Node("HitEffectNode");
// Create particle effects // Create particle effects
ParticleEmitter flame = createFlame(); ParticleEmitter flame = particleFactory.createFlame();
ParticleEmitter flash = createFlash(); ParticleEmitter flash = particleFactory.createFlash();
ParticleEmitter spark = createSpark(); ParticleEmitter spark = particleFactory.createSpark();
ParticleEmitter roundSpark = createRoundSpark(); ParticleEmitter roundSpark = particleFactory.createRoundSpark();
ParticleEmitter smokeTrail = createSmokeTrail(); ParticleEmitter smokeTrail = particleFactory.createSmokeTrail();
ParticleEmitter debris = createDebris(); ParticleEmitter debris = particleFactory.createDebris();
ParticleEmitter shockwave = createShockwave(); ParticleEmitter shockwave = particleFactory.createShockwave();
ParticleEmitter movingSmoke = createMovingSmokeEmitter(); // New moving smoke emitter ParticleEmitter movingSmoke = particleFactory.createMovingSmokeEmitter();
// Attach all effects to the hitEffectNode // Attach all effects to the hitEffectNode
hitEffectNode.attachChild(flame); hitEffectNode.attachChild(flame);
@@ -403,36 +170,14 @@ class SeaSynchronizer extends ShipMapSynchronizer {
flame.emitAllParticles(); flame.emitAllParticles();
roundSpark.emitAllParticles(); roundSpark.emitAllParticles();
//Checks if ship is destroyed and triggers animation accordingly
if (ship.isDestroyed()) { if (ship.isDestroyed()) {
// Add ShipSinkingControl to the shipNode sinkAndRemoveShip(ship);
ShipSinkingControl sinkingControl = new ShipSinkingControl(2f, 5f, -5f, 60f);
shipNode.addControl(sinkingControl);
sinkingControl.startSinking(); // Start the sinking process
} }
return null; return null;
} }
/**
* 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 = createWaterSplash();
waterSplash.setLocalTranslation(shot.getY() + 0.5f, 0f, shot.getX() + 0.5f);
shotNode.attachChild(waterSplash);
waterSplash.emitAllParticles();
return shotNode;
}
/** /**
* Creates a cylinder geometry representing the specified shot. * Creates a cylinder geometry representing the specified shot.
* The appearance of the cylinder depends on whether the shot is a hit or a miss. * The appearance of the cylinder depends on whether the shot is a hit or a miss.
@@ -484,17 +229,11 @@ class SeaSynchronizer extends ShipMapSynchronizer {
*/ */
private Spatial createShip(Battleship ship) { private Spatial createShip(Battleship ship) {
switch (ship.getLength()) { switch (ship.getLength()) {
case 4: case 4: return createBattleship(ship);
return createBattleship(ship); case 3: return createCV(ship);
case 3: case 2: return createBattle(ship);
return createCV(ship); case 1: return createSmallship(ship);
case 2: default: return createBox(ship);
return createBattle(ship);
case 1:
return createSmallShip(ship);
default:
return createBox(ship);
} }
} }
@@ -538,26 +277,27 @@ class SeaSynchronizer extends ShipMapSynchronizer {
* Creates a detailed 3D model to represent a "King George V" battleship. * Creates a detailed 3D model to represent a "King George V" battleship.
* *
* @param ship the battleship to be represented * @param ship the battleship to be represented
* @return the spatial representing the "King George V" battleship. * @return the spatial representing the "King George V" battleship
*/ */
private Spatial createBattleship(Battleship ship) { private Spatial createBattleship(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(KING_GEORGE_V_MODEL); final Spatial model = app.getAssetManager().loadModel(KING_GEORGE_V_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f); model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(1.48f); model.scale(1.48f);
// model.scale(0.0007f);
model.setShadowMode(ShadowMode.CastAndReceive); model.setShadowMode(ShadowMode.CastAndReceive);
return model; return model;
} }
/** /**
* Creates a detailed 3D model to represent a small tugboat. * Creates a detailed 3D model to represent a small tug boat.
* *
* @param ship the battleship to be represented * @param ship the battleship to be represented
* @return the spatial representing the small tugboat. * @return the spatial representing a small tug boat
*/ */
private Spatial createSmallShip(Battleship ship) { private Spatial createSmallship(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(SMALL_BOAT_MODEL); final Spatial model = app.getAssetManager().loadModel(BOAT_SMALL_MODEL);
model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f); model.rotate(-HALF_PI, calculateRotationAngle(ship.getRot()), 0f);
model.scale(0.0005f); model.scale(0.0005f);
@@ -567,27 +307,10 @@ class SeaSynchronizer extends ShipMapSynchronizer {
} }
/** /**
* Creates a detailed 3D model to represent a U-Boat . * Creates a detailed 3D model to represent a "German WWII UBoat".
* *
* @param ship the battleship to be represented * @param ship the battleship to be represented
* @return the spatial representing the U-Boat. * @return the spatial representing the "German WWII UBoat"
*/
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.05f, 0f);
model.scale(0.27f);
model.setShadowMode(ShadowMode.CastAndReceive);
return model;
}
/**
* Creates a detailed 3D model to represent an aircraft carrier
*
* @param ship the battleship to be represented
* @return the spatial representing the aircraft carrier.
*/ */
private Spatial createCV(Battleship ship) { private Spatial createCV(Battleship ship) {
final Spatial model = app.getAssetManager().loadModel(CV_MODEL); final Spatial model = app.getAssetManager().loadModel(CV_MODEL);
@@ -600,6 +323,23 @@ class SeaSynchronizer extends ShipMapSynchronizer {
return model; 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. * Calculates the rotation angle for the specified rotation.
* *
@@ -635,5 +375,4 @@ class SeaSynchronizer extends ShipMapSynchronizer {
model.addControl(new ShellControl(shell)); model.addControl(new ShellControl(shell));
return model; return model;
} }
} }

View File

@@ -42,9 +42,16 @@ public class Shell2DControl extends AbstractControl {
spatial.setLocalTranslation(viewPos.getX() + MapView.FIELD_SIZE / 2, viewPos.getY() + MapView.FIELD_SIZE / 2, 0); 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 @Override
protected void controlRender(RenderManager rm, ViewPort vp) { protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering-specific behavior required for this control // No rendering-specific behavior required for this control
} }
} }

View File

@@ -43,9 +43,16 @@ public class ShellControl extends AbstractControl {
spatial.rotate(PI/2,0,0); 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 @Override
protected void controlRender(RenderManager rm, ViewPort vp) { protected void controlRender(RenderManager rm, ViewPort vp) {
// No rendering-specific behavior required for this control // No rendering-specific behavior required for this control
} }
} }

View File

@@ -1,116 +0,0 @@
package pp.battleship.client.gui;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
/**
* Controls the burning, tilting, and sinking behavior of a battleship.
* The ship will burn and tilt for a specified duration, then sink below the water surface.
*/
public class ShipSinkingControl extends AbstractControl {
private float elapsedTime = 0f;
private final float burnTiltDuration;
private final float sinkDuration;
private final float sinkDepth;
private final float tiltAngle;
private final Vector3f initialPosition;
private boolean sinkingStarted = false;
/**
* Constructs a new ShipSinkingControl instance.
*
* @param burnTiltDuration Time in seconds for the ship to burn and tilt on the surface
* @param sinkDuration Time in seconds for the ship to fully sink
* @param sinkDepth Depth below the water to sink the ship
* @param tiltAngle Final tilt angle in degrees
*/
public ShipSinkingControl(float burnTiltDuration, float sinkDuration, float sinkDepth, float tiltAngle) {
this.burnTiltDuration = burnTiltDuration;
this.sinkDuration = sinkDuration;
this.sinkDepth = sinkDepth;
this.tiltAngle = tiltAngle;
this.initialPosition = new Vector3f(); // Placeholder; will be set in controlUpdate
}
/**
* Overrides controlUpdate in AbstractControl
* regulates the burn and tilt timeframe
* @param tpf time per frame (in seconds)
*/
@Override
protected void controlUpdate(float tpf) {
if (spatial == null) return;
elapsedTime += tpf;
if (elapsedTime < burnTiltDuration) {
float progress = elapsedTime / burnTiltDuration;
float angleInRadians = FastMath.DEG_TO_RAD * FastMath.interpolateLinear(progress, 0f, -tiltAngle);
Quaternion tiltRotation = new Quaternion().fromAngles(angleInRadians, 0, 0);
spatial.setLocalRotation(tiltRotation);
return;
}
// Start sinking if it hasn't started yet
if (!sinkingStarted) {
sinkingStarted = true;
// Save the initial position when sinking starts
initialPosition.set(spatial.getLocalTranslation());
// Remove the hitEffectNode
Node parentNode = (Node) spatial;
Spatial hitEffects = parentNode.getChild("HitEffectNode");
if (hitEffects != null) {
parentNode.detachChild(hitEffects);
}
}
// Calculate the progress of the sinking (0 to 1)
float progress = Math.min((elapsedTime - burnTiltDuration) / sinkDuration, 1f);
// Apply the tilt angle (remains constant during sinking)
Quaternion tiltRotation = new Quaternion().fromAngles(-FastMath.DEG_TO_RAD * tiltAngle, 0, 0);
spatial.setLocalRotation(tiltRotation);
// Sink the ship by interpolating the Y position
float currentY = FastMath.interpolateLinear(progress, initialPosition.y, sinkDepth);
spatial.setLocalTranslation(initialPosition.x, currentY, initialPosition.z);
if (currentY <= sinkDepth) {
Node parentNode = (Node) spatial.getParent();
if (parentNode != null) {
parentNode.detachChild(spatial);
}
spatial.removeControl(this);
}
}
/**
* 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
}
/**
* Starts the sinking process for the ship.
*/
public void startSinking() {
// Nothing to do here as the control update handles the timing and sequence
}
}

View File

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

View File

@@ -1,34 +0,0 @@
package pp.battleship.client.gui;
import com.simsilica.lemur.Slider;
import pp.battleship.client.GameMusic;
public class VolumeControl extends Slider {
private final GameMusic menuMusicModern;
private double vol;
public VolumeControl(GameMusic music) {
super();
this.menuMusicModern = music;
vol = GameMusic.volumePreference();
getModel().setPercent(vol);
}
/**
* Updates the volume of the music to the appropriate level set by the User through the Volume control Slider
*
*/
public void update() {
if (vol != getModel().getPercent()) {
vol = getModel().getPercent();
menuMusicModern.setVolume( (float) vol);
}
}
}

View File

@@ -0,0 +1,35 @@
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);
}
}
}

View File

@@ -5,7 +5,7 @@
// (c) Mark Minas (mark.minas@unibw.de) // (c) Mark Minas (mark.minas@unibw.de)
//////////////////////////////////////// ////////////////////////////////////////
package pp.battleship.client.server; package pp.battleship.server;
import com.jme3.network.ConnectionListener; import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection; import com.jme3.network.HostedConnection;
@@ -42,14 +42,14 @@ import java.util.logging.LogManager;
/** /**
* Server implementing the visitor pattern as MessageReceiver for ClientMessages * Server implementing the visitor pattern as MessageReceiver for ClientMessages
*/ */
public class BattleshipSelfhostServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender { public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
private static final Logger LOGGER = System.getLogger(BattleshipSelfhostServer.class.getName()); private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName());
private static final File CONFIG_FILE = new File("server.properties"); private static final File CONFIG_FILE = new File("server.properties");
private final BattleshipConfig config = new BattleshipConfig(); private final BattleshipConfig config = new BattleshipConfig();
private Server myServer; private Server myServer;
private final ServerGameLogic logic; private final ServerGameLogic logic;
private final BlockingQueue<ReceivedMessageSelfhost> pendingMessages = new LinkedBlockingQueue<>(); private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();
static { static {
// Configure logging // Configure logging
@@ -64,16 +64,16 @@ public class BattleshipSelfhostServer implements MessageListener<HostedConnectio
} }
/** /**
* Starts the server. * Starts the Battleships server.
*/ */
public static void main(String[] args) { public static void main(String[] args) {
new BattleshipSelfhostServer().run(); new BattleshipServer().run();
} }
/** /**
* Creates the server. * Creates the server.
*/ */
BattleshipSelfhostServer() { BattleshipServer() {
config.readFromIfExists(CONFIG_FILE); config.readFromIfExists(CONFIG_FILE);
LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
logic = new ServerGameLogic(this, config); logic = new ServerGameLogic(this, config);
@@ -133,7 +133,7 @@ public class BattleshipSelfhostServer implements MessageListener<HostedConnectio
public void messageReceived(HostedConnection source, Message message) { public void messageReceived(HostedConnection source, Message message) {
LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS
if (message instanceof ClientMessage clientMessage) if (message instanceof ClientMessage clientMessage)
pendingMessages.add(new ReceivedMessageSelfhost(clientMessage, source.getId())); pendingMessages.add(new ReceivedMessage(clientMessage, source.getId()));
} }
@Override @Override

View File

@@ -5,12 +5,12 @@
// (c) Mark Minas (mark.minas@unibw.de) // (c) Mark Minas (mark.minas@unibw.de)
//////////////////////////////////////// ////////////////////////////////////////
package pp.battleship.client.server; package pp.battleship.server;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.ClientMessage; import pp.battleship.message.client.ClientMessage;
record ReceivedMessageSelfhost(ClientMessage message, int from) { record ReceivedMessage(ClientMessage message, int from) {
void process(ClientInterpreter interpreter) { void process(ClientInterpreter interpreter) {
message.accept(interpreter, from); message.accept(interpreter, from);
} }

View File

@@ -1,16 +0,0 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 29.03.2012 14:25:39
newmtl default
Ns 35.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.5400 0.5400 0.5400
Ke 0.0000 0.0000 0.0000
map_Ka 14084_WWII_ship_German_Type_II_U-boat_diff.jpg
map_Kd 14084_WWII_ship_German_Type_II_U-boat_diff.jpg

View File

@@ -1,104 +0,0 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 16.12.2011 14:18:52
newmtl white
Ns 53.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.6667 0.6667 0.6667
Kd 0.6667 0.6667 0.6667
Ks 0.1800 0.1800 0.1800
Ke 0.0000 0.0000 0.0000
newmtl boat_elements_black
Ns 55.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.0000 0.0000 0.0000
Kd 0.0000 0.0000 0.0000
Ks 0.3600 0.3600 0.3600
Ke 0.0000 0.0000 0.0000
newmtl boat_glass
Ns 60.0000
Ni 7.0000
d 0.4000
Tr 0.6000
Tf 0.4000 0.4000 0.4000
illum 2
Ka 0.1059 0.1569 0.1451
Kd 0.1059 0.1569 0.1451
Ks 0.6750 0.6750 0.6750
Ke 0.0000 0.0000 0.0000
newmtl boat_screw_hooks_bronze
Ns 80.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.2941 0.2157 0.0510
Kd 0.2941 0.2157 0.0510
Ks 0.7200 0.7200 0.7200
Ke 0.0000 0.0000 0.0000
newmtl boat_silver
Ns 80.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 0.3333 0.3333 0.3333
Kd 0.3333 0.3333 0.3333
Ks 0.7200 0.7200 0.7200
Ke 0.0000 0.0000 0.0000
newmtl boat_buffer
Ns 10.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.2700 0.2700 0.2700
Ke 0.0000 0.0000 0.0000
map_Ka boat_buffer_diffuse.jpg
map_Kd boat_buffer_diffuse.jpg
newmtl boat_roof_accessory
Ns 15.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.3600 0.3600 0.3600
Ke 0.0000 0.0000 0.0000
map_Ka boat_roof_accessory_diffuse.jpg
map_Kd boat_roof_accessory_diffuse.jpg
newmtl boat_body
Ns 55.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.3600 0.3600 0.3600
Ke 0.0000 0.0000 0.0000
map_Ka boat_body_diffuse.jpg
map_Kd boat_body_diffuse.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 B

View File

@@ -1,180 +0,0 @@
newmtl Model001_Material001
map_Kd Yorktown_paint6.png
map_bump bumpmap_flat.png
newmtl Model001_Material002
map_Kd diff_null_7.png
map_bump bumpmap_flat.png
newmtl Model001_Material003
map_Kd diff_null_14.png
map_bump bumpmap_flat.png
newmtl Model001_Material004
map_Kd diff_null_Color005.png
map_bump bumpmap_flat.png
newmtl Model001_Material005
map_Kd diff_null_8.png
map_bump bumpmap_flat.png
newmtl Model001_Material006
map_Kd diff_null_Color007.png
map_bump bumpmap_flat.png
newmtl Model001_Material007
map_Kd diff_null_17.png
map_bump bumpmap_flat.png
newmtl Model001_Material008
map_Kd diff_null_FrontColor.png
map_bump bumpmap_flat.png
newmtl Model001_Material009
map_Kd diff_null_BackColor.png
map_bump bumpmap_flat.png
newmtl Model001_Material010
map_Kd diff_null_4.png
map_bump bumpmap_flat.png
newmtl Model001_Material011
map_Kd Color_004.png
map_bump bumpmap_flat.png
newmtl Model001_Material012
map_Kd diff_null_Gray1.png
map_bump bumpmap_flat.png
newmtl Model001_Material013
map_Kd diff_null_3.png
map_bump bumpmap_flat.png
newmtl Model001_Material014
map_Kd Metal_Rough.png
map_bump bumpmap_flat.png
newmtl Model001_Material015
map_Kd diff_null_12.png
map_bump bumpmap_flat.png
newmtl Model001_Material016
map_Kd diff_null_19.png
map_bump bumpmap_flat.png
newmtl Model001_Material017
map_Kd diff_null_Color003.png
map_bump bumpmap_flat.png
newmtl Model001_Material018
map_Kd diff_null_mat1.png
map_bump bumpmap_flat.png
newmtl Model001_Material019
map_Kd diff_null_1.png
map_bump bumpmap_flat.png
newmtl Model001_Material020
map_Kd diff_null_2.png
map_bump bumpmap_flat.png
newmtl Model001_Material021
map_Kd diff_null_Black1.png
map_bump bumpmap_flat.png
newmtl Model001_Material022
map_Kd diff_null_Model001Materia.png
map_bump bumpmap_flat.png
newmtl Model001_Material023
map_Kd diff_null_Model001Mate1.png
map_bump bumpmap_flat.png
newmtl Model001_Material024
map_Kd diff_null_13.png
map_bump bumpmap_flat.png
newmtl Model001_Material025
map_Kd _6.png
map_bump bumpmap_flat.png
newmtl Model001_Material026
map_Kd Metal_Aluminum_Anodized.png
map_bump bumpmap_flat.png
newmtl Model001_Material027
map_Kd diff_null_Color006.png
map_bump bumpmap_flat.png
newmtl Model001_Material028
map_Kd Blinds_Vertical_Stripe_Gray.png
map_bump bumpmap_flat.png
newmtl Model001_Material029
map_Kd diff_null_15.png
map_bump bumpmap_flat.png
newmtl Model001_Material030
map_Kd diff_null_Color002.png
map_bump bumpmap_flat.png
newmtl Model001_Material031
map_Kd Cladding_Siding_Tan.png
map_bump bumpmap_flat.png
newmtl Model001_Material032
map_Kd diff_null_Color008.png
map_bump bumpmap_flat.png
newmtl Model001_Material033
map_Kd diff_null_16.png
map_bump bumpmap_flat.png
newmtl Model001_Material034
map_Kd _2.png
map_bump bumpmap_flat.png
newmtl Model001_Material035
map_Kd Blinds_Roman_Hobbled_Blue.png
map_bump bumpmap_flat.png
newmtl Model001_Material036
map_Kd Blinds_Wood_White.png
map_bump bumpmap_flat.png
newmtl Model001_Material037
map_Kd Metal_Seamed.png
map_bump bumpmap_flat.png
newmtl Model001_Material038
map_Kd image_16.png
map_bump bumpmap_flat.png
newmtl Model001_Material039
map_Kd diff_null_9.png
map_bump bumpmap_flat.png
newmtl Model001_Material040
map_Kd image_15.png
map_bump bumpmap_flat.png
newmtl Model001_Material041
map_Kd _Blinds_Roman_Hobbled_Blue_1.png
map_bump bumpmap_flat.png
newmtl Model001_Material042
map_Kd diff_null_10.png
map_bump bumpmap_flat.png
newmtl Model001_Material043
map_Kd diff_null_Material5.png
map_bump bumpmap_flat.png
newmtl Model001_Material044
map_Kd diff_null_Material1.png
map_bump bumpmap_flat.png
newmtl Model001_Material045
map_Kd diff_null_11.png
map_bump bumpmap_flat.png

Binary file not shown.

View File

@@ -41,14 +41,16 @@ public class ModelExporter extends SimpleApplication {
*/ */
@Override @Override
public void simpleInitApp() { public void simpleInitApp() {
export("Models/KingGeorgeV/King_George_V.obj", "KingGeorgeV.j3o");//NON-NLS export("Models/KingGeorgeV/King_George_V.obj", "KingGeorgeV.j3o"); //NON-NLS
export("Models/BoatSmall/12219_boat_v2_L2.obj", "12219_boat_v2_L2.j3o"); export("Models/BoatSmall/12219_boat_v2_L2.obj", "BoatSmall.j3o"); //NON-NLS
export("Models/Battle/14084_WWII_Ship_German_Type_II_U-boat_v2_L1.obj", "Battle.j3o"); //NON-NLS export("Models/Battle/14084_WWII_Ship_German_Type_II_U-boat_v2_L1.obj", "Battle.j3o"); //NON-NLS
export("Models/CV/essex_scb-125_generic.obj", "CV.j3o"); //NON-NLS export("Models/CV/essex_scb-125_generic.obj", "CV.j3o"); //NON-NLS
export("Models/Shell/Shell/45.obj", "Shell.j3o"); export("Models/Figures/Würfel_blau.obj", "Würfel_blau.j30");
export("Models/Figures/Würfel_gelb.obj", "Würfel_gelb.j30");
export("Models/Figures/Würfel_grün.obj", "Würfel_grün.j30");
export("Models/Figures/Würfel_rosa.obj", "Würfel_rosa.j30");
export("Models/Figures/Würfel_rot.obj", "Würfel_rot.j30");
export("Models/Figures/Würfel_schwarz.obj", "Würfel_schwarz.j30");
stop(); stop();
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

View File

@@ -10,8 +10,8 @@ package pp.battleship.game.client;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.model.IntPoint; import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.model.Shell; import pp.battleship.model.Shell;
import pp.battleship.model.ShipMap;
import pp.battleship.notification.Sound; import pp.battleship.notification.Sound;
import java.lang.System.Logger.Level; import java.lang.System.Logger.Level;
@@ -54,13 +54,13 @@ class BattleState extends ClientState {
@Override @Override
public void receivedEffect(EffectMessage msg) { public void receivedEffect(EffectMessage msg) {
ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS ClientGameLogic.LOGGER.log(Level.INFO, "report effect: {0}", msg); //NON-NLS
myTurn = msg.isMyTurn(); myTurn = msg.isMyTurn();
logic.setInfoText(msg.getInfoTextKey()); logic.setInfoText(msg.getInfoTextKey());
Shell shell = new Shell(msg.getShot()); Shell shell = new Shell(msg.getShot());
affectedMap(msg).add(shell); affectedMap(msg).add(shell);
logic.playSound(Sound.SHELL_FLYING); logic.playSound(Sound.SHELL_FLYING);
logic.setState(new ShootingState(logic, shell, myTurn, msg)); logic.setState(new ShootingState(logic, shell, myTurn, msg));
} }
/** /**

View File

@@ -8,10 +8,10 @@
package pp.battleship.game.server; package pp.battleship.game.server;
import pp.battleship.BattleshipConfig; import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails; import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage; import pp.battleship.message.server.ServerMessage;
@@ -36,10 +36,10 @@ public class ServerGameLogic implements ClientInterpreter {
private final BattleshipConfig config; private final BattleshipConfig config;
private final List<Player> players = new ArrayList<>(2); private final List<Player> players = new ArrayList<>(2);
private final Set<Player> readyPlayers = new HashSet<>(); private final Set<Player> readyPlayers = new HashSet<>();
private Set<Player> waitPlayers = new HashSet<>();
private final ServerSender serverSender; private final ServerSender serverSender;
private Player activePlayer; private Player activePlayer;
private ServerState state = ServerState.WAIT; private ServerState state = ServerState.WAIT;
private Set<Player> waitPlayers = new HashSet<>();
/** /**
* Constructs a ServerGameLogic with the specified sender and configuration. * Constructs a ServerGameLogic with the specified sender and configuration.
@@ -136,7 +136,6 @@ public class ServerGameLogic implements ClientInterpreter {
/** /**
* Handles the reception of a MapMessage. * Handles the reception of a MapMessage.
* Also tests valid ship placement on the Map.
* *
* @param msg the received MapMessage * @param msg the received MapMessage
* @param from the ID of the sender client * @param from the ID of the sender client
@@ -154,85 +153,8 @@ public class ServerGameLogic implements ClientInterpreter {
send(players.get(from), new GameDetails(config)); send(players.get(from), new GameDetails(config));
} }
} }
} }
/**
* Handles the reception of a ShootMessage.
*
* @param msg the received ShootMessage
* @param from the ID of the sender client
*/
@Override
public void received(ShootMessage msg, int from) {
if (state != ServerState.BATTLE)
LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS
else{
setState(ServerState.ANIMATION);
shoot(getPlayerById(from), msg.getPosition());
}
}
/**
* Marks the player as ready and sets their ships.
* Transitions the state to PLAY if both players are ready.
*
* @param player the player who is ready
* @param ships the list of ships placed by the player
*/
void playerReady(Player player, List<Battleship> ships) {
if (!readyPlayers.add(player)) {
LOGGER.log(Level.ERROR, "{0} was already ready", player); //NON-NLS
return;
}
ships.forEach(player.getMap()::add);
if (readyPlayers.size() == 2) {
for (Player p : players)
send(p, new StartBattleMessage(p == activePlayer));
setState(ServerState.BATTLE);
}
}
/**
* Handles the shooting action by the player.
*
* @param p the player who shot
* @param pos the position of the shot
*/
void shoot(Player p, IntPoint pos) {
if (p != activePlayer) return;
final Player otherPlayer = getOpponent(activePlayer);
final Battleship selectedShip = otherPlayer.getMap().findShipAt(pos);
if (selectedShip == null) {
// shot missed
send(activePlayer, EffectMessage.miss(true, pos));
send(otherPlayer, EffectMessage.miss(false, pos));
activePlayer = otherPlayer;
}
else {
// shot hit a ship
selectedShip.hit(pos);
if (otherPlayer.getMap().getRemainingShips().isEmpty()) {
// game is over
send(activePlayer, EffectMessage.won(pos, selectedShip));
send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips()));
setState(ServerState.GAME_OVER);
}
else if (selectedShip.isDestroyed()) {
// ship has been destroyed, but game is not yet over
send(activePlayer, EffectMessage.shipDestroyed(true, pos, selectedShip));
send(otherPlayer, EffectMessage.shipDestroyed(false, pos, selectedShip));
}
else {
// ship has been hit, but it hasn't been destroyed
send(activePlayer, EffectMessage.hit(true, pos));
send(otherPlayer, EffectMessage.hit(false, pos));
}
}
}
/** /**
* Handles the reception of a AnimationFinishedMessage. * Handles the reception of a AnimationFinishedMessage.
* *
@@ -290,4 +212,79 @@ public class ServerGameLogic implements ClientInterpreter {
} }
return true; return true;
} }
/**
* Handles the reception of a ShootMessage.
*
* @param msg the received ShootMessage
* @param from the ID of the sender client
*/
@Override
public void received(ShootMessage msg, int from) {
if (state != ServerState.BATTLE)
LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS
else{
setState(ServerState.ANIMATION);
shoot(getPlayerById(from), msg.getPosition());
}
}
/**
* Marks the player as ready and sets their ships.
* Transitions the state to PLAY if both players are ready.
*
* @param player the player who is ready
* @param ships the list of ships placed by the player
*/
void playerReady(Player player, List<Battleship> ships) {
if (!readyPlayers.add(player)) {
LOGGER.log(Level.ERROR, "{0} was already ready", player); //NON-NLS
return;
}
ships.forEach(player.getMap()::add);
if (readyPlayers.size() == 2) {
for (Player p : players)
send(p, new StartBattleMessage(p == activePlayer));
setState(ServerState.BATTLE);
}
}
/**
* Handles the shooting action by the player.
*
* @param p the player who shot
* @param pos the position of the shot
*/
void shoot(Player p, IntPoint pos) {
if (p != activePlayer) return;
final Player otherPlayer = getOpponent(activePlayer);
final Battleship selectedShip = otherPlayer.getMap().findShipAt(pos);
if (selectedShip == null) {
// shot missed
send(activePlayer, EffectMessage.miss(true, pos));
send(otherPlayer, EffectMessage.miss(false, pos));
activePlayer = otherPlayer;
}
else {
// shot hit a ship
selectedShip.hit(pos);
if (otherPlayer.getMap().getRemainingShips().isEmpty()) {
// game is over
send(activePlayer, EffectMessage.won(pos, selectedShip));
send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips()));
setState(ServerState.GAME_OVER);
}
else if (selectedShip.isDestroyed()) {
// ship has been destroyed, but game is not yet over
send(activePlayer, EffectMessage.shipDestroyed(true, pos, selectedShip));
send(otherPlayer, EffectMessage.shipDestroyed(false, pos, selectedShip));
}
else {
// ship has been hit, but it hasn't been destroyed
send(activePlayer, EffectMessage.hit(true, pos));
send(otherPlayer, EffectMessage.hit(false, pos));
}
}
}
} }

View File

@@ -31,9 +31,8 @@ enum ServerState {
*/ */
ANIMATION, ANIMATION,
/** /**
* The game has ended because all the ships of one player have been destroyed. * The game has ended because all the ships of one player have been destroyed.
*/ */
GAME_OVER, GAME_OVER
} }

View File

@@ -7,11 +7,11 @@
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.client.ClientInterpreter; import pp.battleship.message.client.ClientInterpreter;
import pp.battleship.message.client.ClientMessage; import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.model.Battleship; import pp.battleship.model.Battleship;
/** /**
@@ -69,7 +69,6 @@ class Copycat implements ClientInterpreter {
copiedMessage = msg; copiedMessage = msg;
} }
/** /**
* Creates a copy of the provided {@link Battleship}. * Creates a copy of the provided {@link Battleship}.
* *
@@ -79,5 +78,4 @@ class Copycat implements ClientInterpreter {
private static Battleship copy(Battleship ship) { private static Battleship copy(Battleship ship) {
return new Battleship(ship.getLength(), ship.getX(), ship.getY(), ship.getRot()); return new Battleship(ship.getLength(), ship.getX(), ship.getY(), ship.getRot());
} }
} }

View File

@@ -1,9 +1,9 @@
package pp.battleship.game.singlemode; package pp.battleship.game.singlemode;
import pp.battleship.game.client.BattleshipClient; import pp.battleship.game.client.BattleshipClient;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails; import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter; import pp.battleship.message.server.ServerInterpreter;
@@ -72,6 +72,7 @@ class RobotClient implements ServerInterpreter {
* Makes the RobotClient take a shot by sending a ShootMessage with the target position. * Makes the RobotClient take a shot by sending a ShootMessage with the target position.
*/ */
private void robotShot() { private void robotShot() {
connection.sendRobotMessage(new ShootMessage(getShotPosition())); connection.sendRobotMessage(new ShootMessage(getShotPosition()));
} }

View File

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

View File

@@ -123,4 +123,3 @@ public class Shell implements Item {
visitor.visit(this); visitor.visit(this);
} }
} }

View File

@@ -78,6 +78,15 @@ public class ShipMap {
addItem(ship); addItem(ship);
} }
/**
* Registers a shot on the map and triggers an item addition event.
*
* @param shell the shell to be registered on the map
*/
public void add(Shell shell) {
addItem(shell);
}
/** /**
* Registers a shot on the map, updates the state of the affected ship (if any), * Registers a shot on the map, updates the state of the affected ship (if any),
* and triggers an item addition event. * and triggers an item addition event.
@@ -91,16 +100,6 @@ public class ShipMap {
addItem(shot); addItem(shot);
} }
/**
* Registers a shot on the map and triggers an item addition event.
*
* @param shell the shell to be registered on the map
*/
public void add(Shell shell) {
addItem(shell);
}
/** /**
* Removes an item from the map and triggers an item removal event. * Removes an item from the map and triggers an item removal event.
* *

View File

@@ -28,14 +28,12 @@ public interface Visitor<T> {
* @return the result of visiting the Battleship element * @return the result of visiting the Battleship element
*/ */
T visit(Battleship ship); T visit(Battleship ship);
/** /**
* Visits a Shell element * Visits a Shell element
* *
* @param shell the Shell element to visit * @param shell the Shell element to visit
* @return the result of visitung the Battleship element * @return the result of visitung the Battleship element
*/ */
T visit(Shell shell); T visit(Shell shell);
} }

View File

@@ -27,9 +27,8 @@ public interface VoidVisitor {
void visit(Battleship ship); void visit(Battleship ship);
/** /**
* * Visits a Shell element * Visits a Shell element
* @param shell the Shell element to visit * @param shell the Shell element to visit
*/ */
void visit(Shell shell); void visit(Shell shell);
} }

View File

@@ -27,5 +27,4 @@ public enum Sound {
* Sound of a shell flying * Sound of a shell flying
*/ */
SHELL_FLYING SHELL_FLYING
} }

View File

@@ -25,12 +25,11 @@ button.cancel=Cancel
server.dialog=Server server.dialog=Server
host.name=Host host.name=Host
port.number=Port port.number=Port
server.host= Self-Host Game
wait.its.not.your.turn=Wait, it's not your turn!! wait.its.not.your.turn=Wait, it's not your turn!!
menu.quit=Quit game menu.quit=Quit game
menu.return-to-game=Return to game menu.return-to-game=Return to game
menu.sound-enabled=Sound switched on menu.sound-enabled=Sound switched on
menu.background-sound-enabled=Music switched on menu.background-sound-enabled=Background music switched on
menu.map.load=Load map from file... menu.map.load=Load map from file...
menu.map.save=Save map in file... menu.map.save=Save map in file...
label.file=File: label.file=File:
@@ -39,3 +38,4 @@ dialog.error=Error
dialog.question=Question dialog.question=Question
port.must.be.integer=Port must be an integer number port.must.be.integer=Port must be an integer number
map.doesnt.fit=The map doesn't fit to this game map.doesnt.fit=The map doesn't fit to this game
client.server-start=Start server

View File

@@ -25,12 +25,11 @@ button.cancel=Abbruch
server.dialog=Server server.dialog=Server
host.name=Host host.name=Host
port.number=Port port.number=Port
server.host= Spiel selbst hosten
wait.its.not.your.turn=Warte, Du bist nicht dran!! wait.its.not.your.turn=Warte, Du bist nicht dran!!
menu.quit=Spiel beenden menu.quit=Spiel beenden
menu.return-to-game=Zurück zum Spiel menu.return-to-game=Zurück zum Spiel
menu.sound-enabled=Sound eingeschaltet menu.sound-enabled=Sound eingeschaltet
menu.background-sound-enabled=Musik eingeschaltet menu.background-sound-enabled=Hintergrundmusik eingeschaltet
menu.map.load=Karte von Datei laden... menu.map.load=Karte von Datei laden...
menu.map.save=Karte in Datei speichern... menu.map.save=Karte in Datei speichern...
label.file=Datei: label.file=Datei:
@@ -39,3 +38,4 @@ dialog.error=Fehler
dialog.question=Frage dialog.question=Frage
port.must.be.integer=Der Port muss eine ganze Zahl sein port.must.be.integer=Der Port muss eine ganze Zahl sein
map.doesnt.fit=Diese Karte passt nicht zu diesem Spiel map.doesnt.fit=Diese Karte passt nicht zu diesem Spiel
client.server-start=Server starten

View File

@@ -18,10 +18,10 @@ import pp.battleship.BattleshipConfig;
import pp.battleship.game.server.Player; import pp.battleship.game.server.Player;
import pp.battleship.game.server.ServerGameLogic; import pp.battleship.game.server.ServerGameLogic;
import pp.battleship.game.server.ServerSender; import pp.battleship.game.server.ServerSender;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.client.ClientMessage; import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage; import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage; import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.client.AnimationFinishedMessage;
import pp.battleship.message.server.EffectMessage; import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails; import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage; import pp.battleship.message.server.ServerMessage;
@@ -81,9 +81,8 @@ public class BattleshipServer implements MessageListener<HostedConnection>, Conn
public void run() { public void run() {
startServer(); startServer();
while (true) { while (true)
processNextMessage(); processNextMessage();
}
} }
private void startServer() { private void startServer() {
@@ -121,7 +120,6 @@ public class BattleshipServer implements MessageListener<HostedConnection>, Conn
Serializer.registerClass(Battleship.class); Serializer.registerClass(Battleship.class);
Serializer.registerClass(IntPoint.class); Serializer.registerClass(IntPoint.class);
Serializer.registerClass(Shot.class); Serializer.registerClass(Shot.class);
} }
private void registerListeners() { private void registerListeners() {

View File

@@ -111,7 +111,7 @@ public class DialogManager {
* *
* @param dialog the dialog to open * @param dialog the dialog to open
*/ */
void open(Dialog dialog) { public void open(Dialog dialog) {
dialogStack.push(dialog); dialogStack.push(dialog);
dialog.update(); dialog.update();
app.getGuiNode().attachChild(dialog); app.getGuiNode().attachChild(dialog);
@@ -133,7 +133,7 @@ public class DialogManager {
* @param dialog the dialog to close * @param dialog the dialog to close
* @throws IllegalArgumentException if the specified dialog is not the top dialog * @throws IllegalArgumentException if the specified dialog is not the top dialog
*/ */
void close(Dialog dialog) { public void close(Dialog dialog) {
if (!isTop(dialog)) if (!isTop(dialog))
throw new IllegalArgumentException(dialog + " is not the top dialog"); throw new IllegalArgumentException(dialog + " is not the top dialog");
dialogStack.pop(); dialogStack.pop();

View File

@@ -1,7 +1,8 @@
// Styling of Lemur components // Styling of Lemur components
// For documentation, see // For documentation, see:
// https://github.com/jMonkeyEngine-Contributions/Lemur/wiki/Styling // https://github.com/jMonkeyEngine-Contributions/Lemur/wiki/Styling
import com.simsilica.lemur.*
import com.simsilica.lemur.component.QuadBackgroundComponent
import com.simsilica.lemur.Button import com.simsilica.lemur.Button
import com.simsilica.lemur.Button.ButtonAction import com.simsilica.lemur.Button.ButtonAction
import com.simsilica.lemur.Command import com.simsilica.lemur.Command
@@ -10,14 +11,17 @@ import com.simsilica.lemur.Insets3f
import com.simsilica.lemur.component.QuadBackgroundComponent import com.simsilica.lemur.component.QuadBackgroundComponent
import com.simsilica.lemur.component.TbtQuadBackgroundComponent import com.simsilica.lemur.component.TbtQuadBackgroundComponent
def bgColor = color(0.25, 0.5, 0.5, 1) def bgColor = color(1, 1, 1, 1)
def buttonEnabledColor = color(0.8, 0.9, 1, 1) def buttonEnabledColor = color(0, 0, 0, 1)
def buttonDisabledColor = color(0.8, 0.9, 1, 0.2) def buttonDisabledColor = color(0.8, 0.9, 1, 0.2)
def buttonBgColor = color(0, 0.75, 0.75, 1) def buttonBgColor = color(1, 1, 1, 1)
def sliderColor = color(0.6, 0.8, 0.8, 1) def sliderColor = color(0.6, 0.8, 0.8, 1)
def sliderBgColor = color(0.5, 0.75, 0.75, 1) def sliderBgColor = color(0.5, 0.75, 0.75, 1)
def gradientColor = color(0.5, 0.75, 0.85, 0.5) def gradientColor = color(0.5, 0.75, 0.85, 0.5)
def tabbuttonEnabledColor = color(0.4, 0.45, 0.5, 1) def tabbuttonEnabledColor = color(0.4, 0.45, 0.5, 1)
def solidWhiteBackground = new QuadBackgroundComponent(color(1, 1, 1, 1)) // Solid white
def gradient = TbtQuadBackgroundComponent.create( def gradient = TbtQuadBackgroundComponent.create(
texture(name: "/com/simsilica/lemur/icons/bordered-gradient.png", texture(name: "/com/simsilica/lemur/icons/bordered-gradient.png",
@@ -27,7 +31,14 @@ def gradient = TbtQuadBackgroundComponent.create(
def doubleGradient = new QuadBackgroundComponent(gradientColor) def doubleGradient = new QuadBackgroundComponent(gradientColor)
doubleGradient.texture = texture(name: "/com/simsilica/lemur/icons/double-gradient-128.png", doubleGradient.texture = texture(name: "/com/simsilica/lemur/icons/double-gradient-128.png",
generateMips: false) generateMips: false)
def orangeBorder = TbtQuadBackgroundComponent.create(
texture(name: "/com/simsilica/lemur/icons/bordered-gradient.png", // Replace with an appropriate texture if needed
generateMips: false),
1, 1, 1, 126, 126,
1f, false)
orangeBorder.color = color(1, 0.5, 0, 1) // Orange color
selector("pp") { selector("pp") {
font = font("Interface/Fonts/Metropolis/Metropolis-Regular-32.fnt") font = font("Interface/Fonts/Metropolis/Metropolis-Regular-32.fnt")
@@ -46,10 +57,26 @@ selector("header", "pp") {
} }
selector("container", "pp") { selector("container", "pp") {
background = gradient.clone() background = solidWhiteBackground.clone()
background.setColor(bgColor) background.setColor(bgColor)
} }
selector("toolbar") {
// Set the grey background
background = new QuadBackgroundComponent(greyBackground)
// Add a red border using a TbtQuadBackgroundComponent
def redBorder = TbtQuadBackgroundComponent.create(
texture(name: "/com/simsilica/lemur/icons/bordered-gradient.png",
generateMips: false),
1, 1, 1, 1, 1,
1f, false)
redBorder.color = redBorderColor
background = greyBackground
// Optional: Set padding inside the toolbar
insets = new Insets3f(10, 10, 10, 10)
}
selector("slider", "pp") { selector("slider", "pp") {
background = gradient.clone() background = gradient.clone()
background.setColor(bgColor) background.setColor(bgColor)
@@ -115,7 +142,7 @@ selector("title", "pp") {
shadowOffset = vec3(2, -2, -1) shadowOffset = vec3(2, -2, -1)
background = new QuadBackgroundComponent(color(0.5, 0.75, 0.85, 1)) background = new QuadBackgroundComponent(color(0.5, 0.75, 0.85, 1))
background.texture = texture(name: "/com/simsilica/lemur/icons/double-gradient-128.png", background.texture = texture(name: "/com/simsilica/lemur/icons/double-gradient-128.png",
generateMips: false) generateMips: false)
insets = new Insets3f(2, 2, 2, 2) insets = new Insets3f(2, 2, 2, 2)
buttonCommands = stdButtonCommands buttonCommands = stdButtonCommands
@@ -123,11 +150,14 @@ selector("title", "pp") {
selector("button", "pp") { selector("button", "pp") {
background = gradient.clone() def outerBackground = new QuadBackgroundComponent(color(1, 0.5, 0, 1)) // Orange border
color = buttonEnabledColor def innerBackground = new QuadBackgroundComponent(buttonBgColor) // Inner button background
background.setColor(buttonBgColor)
insets = new Insets3f(2, 2, 2, 2)
// Apply the outer border as the main background
background = outerBackground
// Use insets to create a margin/padding effect for the inner background
insets = new Insets3f(3, 3, 3, 3) // Adjust the border thickness
buttonCommands = stdButtonCommands buttonCommands = stdButtonCommands
} }
@@ -176,6 +206,7 @@ selector("slider.down.button", "pp") {
selector("checkbox", "pp") { selector("checkbox", "pp") {
color = buttonEnabledColor color = buttonEnabledColor
fontSize = 20
} }
selector("rollup", "pp") { selector("rollup", "pp") {
@@ -192,10 +223,14 @@ selector("tabbedPanel.container", "pp") {
} }
selector("tab.button", "pp") { selector("tab.button", "pp") {
background = gradient.clone() background = solidWhiteBackground.clone()
background.setColor(bgColor) background.setColor(bgColor)
color = tabbuttonEnabledColor color = tabbuttonEnabledColor
insets = new Insets3f(4, 2, 0, 2) insets = new Insets3f(4, 2, 0, 2)
buttonCommands = stdButtonCommands buttonCommands = stdButtonCommands
} }
selector("settings-title", "pp") {
fontSize = 48 // Set font size
background = new QuadBackgroundComponent(color(0.4157f, 0.4235f, 0.4392f, 1.0f)) // Grey background
}

View File

@@ -0,0 +1,23 @@
plugins {
id 'buildlogic.jme-application-conventions'
}
description = 'Monopoly Client'
dependencies {
implementation project(":jme-common")
implementation project(":monopoly:model")
implementation project(":monopoly:server")
implementation libs.jme3.desktop
runtimeOnly libs.jme3.awt.dialogs
runtimeOnly libs.jme3.plugins
runtimeOnly libs.jme3.jogg
runtimeOnly libs.jme3.testdata
}
application {
mainClass = 'pp.monopoly.client.MonopolyApp'
applicationName = 'monopoly'
}

View File

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

View File

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

View File

@@ -0,0 +1,129 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.monopoly.client;
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;
import pp.monopoly.notification.GameEventListener;
import pp.monopoly.notification.SoundEvent;
import static pp.util.PreferencesUtils.getPreferences;
/**
* An application state that plays sounds.
*/
public class GameSound extends AbstractAppState implements GameEventListener {
private static final Logger LOGGER = System.getLogger(GameSound.class.getName());
private static final Preferences PREFERENCES = getPreferences(GameSound.class);
private static final String ENABLED_PREF = "enabled"; //NON-NLS
private AudioNode splashSound;
private AudioNode shipDestroyedSound;
private AudioNode explosionSound;
/**
* Checks if sound is enabled in the preferences.
*
* @return {@code true} if sound is enabled, {@code false} otherwise.
*/
public static boolean enabledInPreferences() {
return PREFERENCES.getBoolean(ENABLED_PREF, true);
}
/**
* Toggles the game sound on or off.
*/
public void toggleSound() {
setEnabled(!isEnabled());
}
/**
* Sets the enabled state of this AppState.
* Overrides {@link com.jme3.app.state.AbstractAppState#setEnabled(boolean)}
*
* @param enabled {@code true} to enable the AppState, {@code false} to disable it.
*/
@Override
public void setEnabled(boolean enabled) {
if (isEnabled() == enabled) return;
super.setEnabled(enabled);
LOGGER.log(Level.INFO, "Sound enabled: {0}", enabled); //NON-NLS
PREFERENCES.putBoolean(ENABLED_PREF, enabled);
}
/**
* Initializes the sound effects for the game.
* Overrides {@link AbstractAppState#initialize(AppStateManager, Application)}
*
* @param stateManager The state manager
* @param app The application
*/
@Override
public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app);
}
/**
* Loads a sound from the specified file.
*
* @param app The application
* @param name The name of the sound file.
* @return The loaded AudioNode.
*/
private AudioNode loadSound(Application app, String name) {
try {
final AudioNode sound = new AudioNode(app.getAssetManager(), name, AudioData.DataType.Buffer);
sound.setLooping(false);
sound.setPositional(false);
return sound;
}
catch (AssetLoadException | AssetNotFoundException ex) {
LOGGER.log(Level.ERROR, ex.getMessage(), ex);
}
return null;
}
/**
* Plays the splash sound effect.
*/
public void splash() {
if (isEnabled() && splashSound != null)
splashSound.playInstance();
}
/**
* Plays the explosion sound effect.
*/
public void explosion() {
if (isEnabled() && explosionSound != null)
explosionSound.playInstance();
}
/**
* Plays sound effect when a ship has been destroyed.
*/
public void shipDestroyed() {
if (isEnabled() && shipDestroyedSound != null)
shipDestroyedSound.playInstance();
}
@Override
public void receivedEvent(SoundEvent event) {
switch (event.sound()) {
}
}
}

View File

@@ -0,0 +1,51 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.monopoly.client;
import java.util.prefs.Preferences;
import pp.dialog.Dialog;
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 MonopolyApp app;
/**
* Constructs the Menu dialog for the Battleship application.
*
* @param app the BattleshipApp instance
*/
public Menu(MonopolyApp app) {
super(app.getDialogManager());
this.app = app;
}
/**
* Updates the state of the load and save buttons based on the game logic.
*/
@Override
public void update() {
}
/**
* As an escape action, this method closes the menu if it is the top dialog.
*/
@Override
public void escape() {
close();
}
}

View File

@@ -0,0 +1,210 @@
package pp.monopoly.client;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.jme3.app.SimpleApplication;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.system.AppSettings;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.style.BaseStyles;
import pp.dialog.DialogBuilder;
import pp.dialog.DialogManager;
import pp.graphics.Draw;
import pp.monopoly.client.gui.SettingsMenu;
import pp.monopoly.client.gui.TestWorld;
import pp.monopoly.game.client.ClientGameLogic;
import pp.monopoly.game.client.MonopolyClient;
import pp.monopoly.game.client.ServerConnection;
import pp.monopoly.notification.GameEventListener;
import pp.monopoly.notification.InfoTextEvent;
public class MonopolyApp extends SimpleApplication implements MonopolyClient, GameEventListener {
private BitmapText topText;
private final ServerConnection serverConnection;
private final ClientGameLogic logic;
private final MonopolyAppConfig config;
private final ActionListener escapeListener = (name, isPressed, tpf) -> handleEscape(isPressed);
private final DialogManager dialogManager = new DialogManager(this);
private final ExecutorService executor = Executors.newCachedThreadPool();
private final Draw draw;
private SettingsMenu settingsMenu;
private TestWorld testWorld;
private boolean isSettingsMenuOpen = false;
private boolean inputBlocked = false;
/**
* 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
public static void main(String[] args) {
new MonopolyApp().start();
}
public MonopolyApp() {
this.draw = new Draw(assetManager);
config = new MonopolyAppConfig();
serverConnection = new NetworkSupport(this);
logic = new ClientGameLogic(serverConnection);
logic.addListener(this);
setShowSettings(config.getShowSettings());
setSettings(makeSettings());
}
@Override
public MonopolyAppConfig getConfig() {
return config;
}
@Override
public ClientGameLogic getGameLogic() {
return logic;
}
private AppSettings makeSettings() {
final AppSettings settings = new AppSettings(true);
settings.setTitle("Monopoly Game");
settings.setResolution(config.getResolutionWidth(), config.getResolutionHeight());
settings.setFullscreen(config.fullScreen());
return settings;
}
@Override
public void simpleInitApp() {
GuiGlobals.initialize(this);
BaseStyles.loadStyleResources(STYLES_SCRIPT);
GuiGlobals.getInstance().getStyles().setDefaultStyle("pp"); //NON-NLS
final BitmapFont normalFont = assetManager.loadFont(FONT); //NON-NLS
setupInput();
setupGui();
// Zeige das Startmenü
StartMenu.createStartMenu(this);
}
private void setupGui() {
BitmapFont normalFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
topText = new BitmapText(normalFont);
topText.setLocalTranslation(10, settings.getHeight() - 10, 0);
guiNode.attachChild(topText);
}
private void setupInput() {
inputManager.deleteMapping(INPUT_MAPPING_EXIT);
inputManager.setCursorVisible(true);
inputManager.addMapping("ESC", new KeyTrigger(KeyInput.KEY_ESCAPE));
inputManager.addListener(escapeListener, "ESC");
}
private void handleEscape(boolean isPressed) {
if (isPressed) {
if (settingsMenu != null && isSettingsMenuOpen) {
// Schließe das SettingsMenu
System.out.println("Schließe SettingsMenu...");
settingsMenu.close();
settingsMenu = null;
setSettingsMenuOpen(false);
} else {
// Öffne das SettingsMenu
System.out.println("Öffne SettingsMenu...");
settingsMenu = new SettingsMenu(this);
settingsMenu.open();
setSettingsMenuOpen(true);
}
}
}
private void blockInputs() {
if (!inputBlocked) {
System.out.println("Blockiere Eingaben...");
inputManager.setCursorVisible(true); // Cursor sichtbar machen
inputManager.clearMappings(); // Alle Mappings entfernen
inputBlocked = true;
}
}
public void unblockInputs() {
if (inputBlocked) {
System.out.println("Aktiviere Eingaben...");
setupInput(); // Standard-Eingaben neu registrieren
inputBlocked = false;
}
}
public void setInfoText(String text) {
topText.setText(text);
}
@Override
public void receivedEvent(InfoTextEvent event) {
setInfoText(event.key());
}
@Override
public void stop(boolean waitFor) {
if (executor != null) executor.shutdownNow();
serverConnection.disconnect();
super.stop(waitFor);
}
public DialogManager getDialogManager() {
return dialogManager;
}
public Draw getDraw() {
return draw;
}
public ExecutorService getExecutor() {
return executor;
}
public void closeApp() {
stop();
}
public void errorDialog(String errorMessage) {
DialogBuilder.simple(dialogManager)
.setTitle("Fehler")
.setText(errorMessage)
.setOkButton("OK")
.build()
.open();
}
public void setSettingsMenuOpen(boolean isOpen) {
this.isSettingsMenuOpen = isOpen;
}
@Override
public void simpleUpdate(float tpf) {
if (testWorld != null) {
testWorld.update(tpf); // Aktualisiere die Kamera in der TestWorld
}
}
public void startTestWorld() {
guiNode.detachAllChildren(); // Entferne GUI
testWorld = new TestWorld(this); // Erstelle eine Instanz von TestWorld
testWorld.initializeScene(); // Initialisiere die Szene
}
public void returnToMenu() {
guiNode.detachAllChildren(); // Entferne die GUI
StartMenu.createStartMenu(this); // Zeige das Startmenü erneut
}
}

View File

@@ -0,0 +1,190 @@
package pp.monopoly.client;
import com.jme3.math.ColorRGBA;
import pp.monopoly.game.client.MonopolyClientConfig;
/**
* Provides access to the Monopoly application configuration.
* Extends {@link MonopolyClientConfig} 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 MonopolyAppConfig extends MonopolyClientConfig {
/**
* 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 MonopolyAppConfig} with predefined values.
*/
public MonopolyAppConfig() {
// Default constructor
}
/**
* Returns the width of the game view resolution in pixels.
*
* @return the width of the game view resolution in pixels
*/
public int getResolutionWidth() {
return resolutionWidth;
}
/**
* Returns the height of the game view resolution in pixels.
*
* @return the height of the game view resolution in pixels
*/
public int getResolutionHeight() {
return resolutionHeight;
}
/**
* Returns whether the game should start in full-screen mode.
*
* @return {@code true} if the game should start in full-screen mode; {@code false} otherwise
*/
public boolean fullScreen() {
return fullScreen;
}
/**
* Returns whether gamma correction is enabled.
* If enabled, the main framebuffer is configured for sRGB colors,
* and sRGB images are linearized.
*
* @return {@code true} if gamma correction is enabled; {@code false} otherwise
*/
public boolean useGammaCorrection() {
return useGammaCorrection;
}
/**
* Returns whether full resolution framebuffers should be used on Retina displays.
* This setting is ignored on non-Retina platforms.
*
* @return {@code true} if full resolution framebuffers should be used on Retina displays; {@code false} otherwise
*/
public boolean useRetinaFrameBuffer() {
return useRetinaFrameBuffer;
}
/**
* Returns whether the settings window should be shown for configuring the game.
*
* @return {@code true} if the settings window should be shown; {@code false} otherwise
*/
public boolean getShowSettings() {
return showSettings;
}
/**
* Returns whether the JME statistics window should be shown in the lower left corner of the screen.
*
* @return {@code true} if the statistics window should be shown; {@code false} otherwise
*/
public boolean getShowStatistics() {
return showStatistics;
}
/**
* Returns the color of the top text during gameplay as a {@link ColorRGBA} object.
*
* @return the color of the top text during gameplay
*/
public ColorRGBA getTopColor() {
return topColor;
}
}

View File

@@ -0,0 +1,84 @@
package pp.monopoly.client;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import pp.monopoly.game.client.ClientGameLogic;
/**
* Abstract class representing a state in the Monopoly game.
* Extends the AbstractAppState from jMonkeyEngine to manage state behavior.
*/
public abstract class MonopolyAppState extends AbstractAppState {
private MonopolyApp app;
/**
* Creates a new MonopolyAppState that is initially disabled.
*/
protected MonopolyAppState() {
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 = (MonopolyApp) application;
if (isEnabled()) {
enableState();
}
}
/**
* Returns the MonopolyApp instance associated with this MonopolyAppState.
*
* @return The MonopolyApp instance.
*/
public MonopolyApp getApp() {
return app;
}
/**
* Returns the client game logic handler.
*
* @return the client game logic handler
*/
public ClientGameLogic getGameLogic() {
return app.getGameLogic();
}
/**
* Sets the enabled state of the MonopolyAppState.
* 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();
}
}
}
/**
* Called when the state is enabled. Override to define specific behavior.
*/
protected abstract void enableState();
/**
* Called when the state is disabled. Override to define specific behavior.
*/
protected abstract void disableState();
}

View File

@@ -0,0 +1,146 @@
package pp.monopoly.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 pp.dialog.Dialog;
import pp.dialog.DialogBuilder;
import pp.dialog.SimpleDialog;
/**
* Represents a dialog for setting up a network connection in the Monopoly 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";
private static final String DEFAULT_PORT = "1234";
private final NetworkSupport network;
private final TextField host = new TextField(LOCALHOST);
private final TextField port = new TextField(DEFAULT_PORT);
private String hostname;
private int portNumber;
private Future<Object> connectionFuture;
private 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;
initializeDialog();
}
/**
* Initializes the dialog with input fields and connection buttons.
*/
private void initializeDialog() {
final MonopolyApp app = network.getApp();
Container inputContainer = new Container();
// Titel und Eingabefelder für Host und Port
inputContainer.addChild(new Label("Server-Adresse"));
inputContainer.addChild(host);
inputContainer.addChild(new Label("Port"));
inputContainer.addChild(port);
Button connectButton = inputContainer.addChild(new Button("Verbinden"));
connectButton.addClickCommands(source -> connect());
Button cancelButton = inputContainer.addChild(new Button("Abbrechen"));
cancelButton.addClickCommands(source -> app.closeApp());
app.getGuiNode().attachChild(inputContainer);
}
/**
* Initiates the connection attempt based on the entered host and port.
*/
private void connect() {
LOGGER.log(Level.INFO, "Connecting to host={0}, port={1}", host, port);
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("Port muss eine Zahl sein.");
}
}
/**
* Opens a progress dialog while connecting.
*/
private void openProgressDialog() {
progressDialog = DialogBuilder.simple(network.getApp().getDialogManager())
.setText("Verbinde zum Server...")
.build();
progressDialog.open();
}
/**
* Attempts 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);
}
}
/**
* Updates the connection status and handles completion or failure.
*/
@Override
public void update(float delta) {
if (connectionFuture != null && connectionFuture.isDone()) {
try {
connectionFuture.get();
onSuccess();
} catch (ExecutionException e) {
onFailure(e.getCause());
} catch (InterruptedException e) {
LOGGER.log(Level.WARNING, "Connection interrupted.", e);
Thread.currentThread().interrupt();
}
}
}
/**
* Handles a successful connection to the game server.
*/
private void onSuccess() {
connectionFuture = null;
progressDialog.close();
this.close();
network.getApp().setInfoText("Warte auf einen Gegner...");
}
/**
* Handles a failed connection attempt.
*
* @param e The cause of the failure.
*/
private void onFailure(Throwable e) {
connectionFuture = null;
progressDialog.close();
network.getApp().errorDialog("Verbindung zum Server fehlgeschlagen.");
network.getApp().setInfoText(e.getLocalizedMessage());
}
}

View File

@@ -0,0 +1,144 @@
package pp.monopoly.client;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
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.monopoly.game.client.ServerConnection;
import pp.monopoly.message.client.ClientMessage;
import pp.monopoly.message.server.ServerMessage;
/**
* Manages the network connection for the Monopoly 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 MonopolyApp app;
private Client client;
/**
* Constructs a NetworkSupport instance for the Monopoly application.
*
* @param app The Monopoly application instance.
*/
public NetworkSupport(MonopolyApp app) {
this.app = app;
}
/**
* Returns the Monopoly application instance.
*
* @return Monopoly application instance
*/
MonopolyApp 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 connection closed.");
}
/**
* 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("Already connected to the game server.");
}
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);
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, "Successfully connected to server: {0}", client);
}
/**
* 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, "Disconnected from server: {0}", disconnectInfo);
this.client = null;
app.enqueue(() -> app.setInfoText("Verbindung zum Server verloren."));
}
/**
* 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 message to server: {0}", message);
if (client == null) {
app.errorDialog("Verbindung zum Server verloren.");
} else {
client.send(message);
}
}
}

View File

@@ -0,0 +1,182 @@
package pp.monopoly.client;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import com.simsilica.lemur.Axis;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.HAlignment;
import com.simsilica.lemur.component.QuadBackgroundComponent;
import com.simsilica.lemur.component.SpringGridLayout;
import pp.dialog.Dialog;
import pp.monopoly.client.gui.CreateGameMenu;
import pp.monopoly.client.gui.SettingsMenu;
/**
* Constructs the startup menu dialog for the Monopoly application.
import pp.monopoly.client.gui.GameMenu;
*/
public class StartMenu extends Dialog {
private final MonopolyApp app;
private Container logoContainer;
private Container unibwLogoContainer;
/**
* Constructs the Startup Menu dialog for the Monopoly application.
*
* @param app the MonopolyApp instance
*/
public StartMenu(MonopolyApp app) {
super(app.getDialogManager());
this.app = app;
}
/**
* Creates and displays the Start Menu with buttons for starting the game,
* opening settings, and quitting the application.
*/
public static void createStartMenu(MonopolyApp app) {
int screenWidth = app.getContext().getSettings().getWidth();
int screenHeight = app.getContext().getSettings().getHeight();
// Set up the background image
Texture backgroundImage = app.getAssetManager().loadTexture("Pictures/unibw-Bib2.png");
Quad quad = new Quad(screenWidth, screenHeight);
Geometry background = new Geometry("Background", quad);
Material backgroundMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
backgroundMaterial.setTexture("ColorMap", backgroundImage);
background.setMaterial(backgroundMaterial);
background.setLocalTranslation(0, 0, -1); // Ensure it is behind other GUI elements
app.getGuiNode().attachChild(background);
createMonopolyLogo(app);
createUnibwLogo(app);
// Center container for title and play button
Container centerMenu = new Container(new SpringGridLayout(Axis.Y, Axis.X));
Button startButton = new Button("Spielen");
startButton.setPreferredSize(new Vector3f(190, 60, 0)); // Increase button size (width, height)
startButton.setFontSize(40); // Set the font size for the button text
startButton.setTextHAlignment(HAlignment.Center); // Center the text horizontally
startButton.addClickCommands(source -> startGame(app));
centerMenu.addChild(startButton);
// Position the center container in the middle of the screen
centerMenu.setLocalTranslation(new Vector3f(screenWidth / 2f - centerMenu.getPreferredSize().x / 2f,
screenHeight / 2f - 280 + centerMenu.getPreferredSize().y / 2f,
0));
app.getGuiNode().attachChild(centerMenu);
// Lower-left container for "Spiel beenden" button
Container lowerLeftMenu = new Container();
lowerLeftMenu.setLocalTranslation(new Vector3f(100, 90, 0));
Button quitButton = new Button("Spiel beenden");
quitButton.setPreferredSize(new Vector3f(130, 40, 0)); // Increase button size slightly (width, height)
quitButton.setFontSize(18);
quitButton.addClickCommands(source -> quitGame());
lowerLeftMenu.addChild(quitButton);
app.getGuiNode().attachChild(lowerLeftMenu);
// Lower-right container for "Einstellungen" button
Container lowerRightMenu = new Container();
lowerRightMenu.setLocalTranslation(new Vector3f(screenWidth - 200, 90, 0));
Button settingsButton = new Button("Einstellungen");
settingsButton.setPreferredSize(new Vector3f(130, 40, 0)); // Increase button size slightly (width, height)
settingsButton.setFontSize(18); // Increase the font size for the text
settingsButton.addClickCommands(source -> openSettings(app));
lowerRightMenu.addChild(settingsButton);
app.getGuiNode().attachChild(lowerRightMenu);
}
/**
* Creates and positions the Monopoly logo container in the center of the screen.
*/
private static void createMonopolyLogo(MonopolyApp app) {
int screenWidth = app.getContext().getSettings().getWidth();
int screenHeight = app.getContext().getSettings().getHeight();
// Load the Monopoly logo as a texture
Texture logoTexture = app.getAssetManager().loadTexture("Pictures/logo-monopoly.png");
// Create a container for the logo
Container logoContainer = new Container();
QuadBackgroundComponent logoBackground = new QuadBackgroundComponent(logoTexture);
logoContainer.setBackground(logoBackground);
// Set the size of the container to fit the logo
float logoWidth = 512; // Adjust these values based on the logo dimensions
float logoHeight = 128; // Adjust these values based on the logo dimensions
logoContainer.setPreferredSize(new Vector3f(logoWidth, logoHeight, 0));
// Position the container at the center of the screen
logoContainer.setLocalTranslation(new Vector3f(
screenWidth / 2f - logoWidth / 2f,
screenHeight / 2f + 200, // Adjust this value for vertical position
0
));
// Attach the container to the GUI node
app.getGuiNode().attachChild(logoContainer);
}
/**
* Creates and positions the Unibw logo container in the center of the screen.
*/
private static void createUnibwLogo(MonopolyApp app) {
int screenWidth = app.getContext().getSettings().getWidth();
int screenHeight = app.getContext().getSettings().getHeight();
// Load the Unibw logo as a texture
Texture unibwTexture = app.getAssetManager().loadTexture("Pictures/logo-unibw.png");
// Create a container for the Unibw logo
Container unibwContainer = new Container();
QuadBackgroundComponent unibwBackground = new QuadBackgroundComponent(unibwTexture);
unibwContainer.setBackground(unibwBackground);
// Set the size of the container to fit the Unibw logo
float unibwWidth = 512; // Adjust these values based on the logo dimensions
float unibwHeight = 128; // Adjust these values based on the logo dimensions
unibwContainer.setPreferredSize(new Vector3f(unibwWidth, unibwHeight, 0));
// Position the container slightly below the Monopoly logo
unibwContainer.setLocalTranslation(new Vector3f(
screenWidth / 2f - unibwWidth / 2f,
screenHeight / 2f + 100, // Adjust this value for vertical position
0
));
// Attach the container to the GUI node
app.getGuiNode().attachChild(unibwContainer);
}
/**
* Starts the game by transitioning to the CreateGameMenu.
*/
private static void startGame(MonopolyApp app) {
app.getGuiNode().detachAllChildren();
new CreateGameMenu(app);
}
/**
* Opens the settings menu.
*/
private static void openSettings(MonopolyApp app) {
app.getGuiNode().detachAllChildren();
new SettingsMenu(app);
}
/**
* Quits the game application.
*/
private static void quitGame() {
System.exit(0);
}
}

View File

@@ -0,0 +1,74 @@
package pp.monopoly.client.gui;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import pp.monopoly.model.Board;
import pp.monopoly.model.Item;
import pp.monopoly.model.Visitor;
import pp.monopoly.notification.GameEventListener;
import pp.monopoly.notification.ItemAddedEvent;
import pp.monopoly.notification.ItemRemovedEvent;
import pp.view.ModelViewSynchronizer;
/**
* Abstract base class for synchronizing the visual representation of a {@link Board} with its model state.
* This class handles the addition and removal of items from the map, ensuring that changes in the model
* are accurately reflected in the view.
*/
abstract class BoardSynchronizer extends ModelViewSynchronizer<Item> implements Visitor<Spatial>, GameEventListener {
protected final Board board;
/**
* Constructs a new BoardSynchronizer.
*
* @param board the game board to synchronize
* @param root the root node to which the view representations of the board items are attached
*/
protected BoardSynchronizer(Board board, Node root) {
super(root);
this.board = board;
}
/**
* Translates a model item into its corresponding visual representation.
*
* @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 board to the view during initialization.
*/
protected void addExisting() {
board.getItems().forEach(this::add);
}
/**
* Handles the event when an item is removed from the board.
*
* @param event the event indicating that an item has been removed from the board
*/
@Override
public void receivedEvent(ItemRemovedEvent event) {
if (board == event.getBoard()) {
delete(event.getItem());
}
}
/**
* Handles the event when an item is added to the board.
*
* @param event the event indicating that an item has been added to the board
*/
@Override
public void receivedEvent(ItemAddedEvent event) {
if (board == event.getBoard()) {
add(event.getItem());
}
}
}

View File

@@ -0,0 +1,59 @@
package pp.monopoly.client.gui;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
/**
* Steuert die Kamerabewegung in der Szene.
*/
public class CameraController {
private final Camera camera;
private final Vector3f center; // Fokuspunkt der Kamera
private final float radius; // Radius der Kreisbewegung
private final float height; // Höhe der Kamera über dem Spielfeld
private final float speed; // Geschwindigkeit der Kamerabewegung
private float angle; // Aktueller Winkel in der Kreisbewegung
/**
* Konstruktor für den CameraController.
*
* @param camera Die Kamera, die gesteuert werden soll
* @param center Der Mittelpunkt der Kreisbewegung (Fokuspunkt)
* @param radius Der Radius der Kreisbewegung
* @param height Die Höhe der Kamera über dem Fokuspunkt
* @param speed Die Geschwindigkeit der Kamerabewegung
*/
public CameraController(Camera camera, Vector3f center, float radius, float height, float speed) {
this.camera = camera;
this.center = center;
this.radius = radius;
this.height = height;
this.speed = speed;
this.angle = 0; // Starte bei Winkel 0
}
/**
* Aktualisiert die Kameraposition und -ausrichtung.
*
* @param tpf Zeit pro Frame
*/
public void update(float tpf) {
// Aktualisiere den Winkel basierend auf der Geschwindigkeit
angle += speed * tpf;
if (angle >= FastMath.TWO_PI) {
angle -= FastMath.TWO_PI; // Winkel zurücksetzen, um Überläufe zu vermeiden
}
// Berechne die neue Position der Kamera
float x = center.x + radius * FastMath.cos(angle);
float z = center.z + radius * FastMath.sin(angle);
float y = center.y + height;
// Setze die Kameraposition
camera.setLocation(new Vector3f(x, y, z));
// Lasse die Kamera auf den Fokuspunkt blicken
camera.lookAt(center, Vector3f.UNIT_Y);
}
}

View File

@@ -0,0 +1,121 @@
package pp.monopoly.client.gui;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import com.simsilica.lemur.Axis;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.TextField;
import com.simsilica.lemur.component.SpringGridLayout;
import pp.monopoly.client.MonopolyApp;
import pp.monopoly.client.StartMenu;
/**
* CreateGameMenu class represents the menu for creating a new game.
*/
public class CreateGameMenu {
private final MonopolyApp app;
private final Container menuContainer;
private Geometry background;
/**
* Konstruktor für das CreateGameMenu.
*
* @param app Die Hauptanwendung (MonopolyApp)
*/
public CreateGameMenu(MonopolyApp app) {
this.app = app;
// Hintergrundbild laden und hinzufügen
addBackgroundImage();
// Hauptcontainer für das Menü
menuContainer = new Container(new SpringGridLayout(Axis.Y, Axis.X));
menuContainer.setPreferredSize(new Vector3f(600, 400, 0)); // Feste Größe des Containers
// Titel
Label title = menuContainer.addChild(new Label("Neues Spiel"));
title.setFontSize(48);
// Eingabefelder-Container
Container inputContainer = menuContainer.addChild(new Container(new SpringGridLayout(Axis.Y, Axis.X)));
inputContainer.setPreferredSize(new Vector3f(200, 150, 0)); // Eingabefelder nicht ganz so breit
inputContainer.setLocalTranslation(20, 0, 0); // Abstand vom Rand
inputContainer.addChild(new Label("Server-Adresse:"));
TextField serverAddressField = inputContainer.addChild(new TextField("localhost"));
serverAddressField.setPreferredWidth(400); // Breite des Textfelds
inputContainer.addChild(new Label("Port:"));
TextField portField = inputContainer.addChild(new TextField("42069"));
portField.setPreferredWidth(400); // Breite des Textfelds
// Button-Container
Container buttonContainer = menuContainer.addChild(new Container(new SpringGridLayout(Axis.X, Axis.Y)));
buttonContainer.setPreferredSize(new Vector3f(400, 50, 0));
buttonContainer.setLocalTranslation(20, 0, 0); // Abstand vom Rand
// "Abbrechen"-Button
Button cancelButton = buttonContainer.addChild(new Button("Abbrechen"));
cancelButton.setPreferredSize(new Vector3f(120, 40, 0));
cancelButton.addClickCommands(source -> goBackToStartMenu());
// "Spiel hosten"-Button
Button hostButton = buttonContainer.addChild(new Button("Spiel hosten"));
hostButton.setPreferredSize(new Vector3f(120, 40, 0));
hostButton.addClickCommands(source -> {
closeCreateGameMenu(); // Schließt das Menü
app.startTestWorld(); // Starte die TestWorld im selben Fenster
});
// "Beitreten"-Button
Button joinButton = buttonContainer.addChild(new Button("Beitreten"));
joinButton.setPreferredSize(new Vector3f(120, 40, 0));
// Placeholder für die Beitrittslogik
// Zentrierung des Containers
menuContainer.setLocalTranslation(
(app.getCamera().getWidth() - menuContainer.getPreferredSize().x) / 2,
(app.getCamera().getHeight() + menuContainer.getPreferredSize().y) / 2,
1 // Höhere Z-Ebene für den Vordergrund
);
app.getGuiNode().attachChild(menuContainer);
}
/**
* Lädt das Hintergrundbild und fügt es als geometrische Ebene hinzu.
*/
private void addBackgroundImage() {
Texture backgroundImage = app.getAssetManager().loadTexture("Pictures/unibw-Bib2.png");
Quad quad = new Quad(app.getCamera().getWidth(), app.getCamera().getHeight());
background = new Geometry("Background", quad);
Material backgroundMaterial = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
backgroundMaterial.setTexture("ColorMap", backgroundImage);
background.setMaterial(backgroundMaterial);
background.setLocalTranslation(0, 0, -1); // Hintergrundebene
app.getGuiNode().attachChild(background);
}
/**
* Geht zum Startmenü zurück, wenn "Abbrechen" angeklickt wird.
*/
private void goBackToStartMenu() {
closeCreateGameMenu(); // Schließt das Menü
StartMenu.createStartMenu(app); // Zeige das Startmenü
}
/**
* Entfernt das CreateGameMenu und dessen Hintergrund.
*/
private void closeCreateGameMenu() {
app.getGuiNode().detachChild(menuContainer); // Entfernt den Menü-Container
app.getGuiNode().detachChild(background); // Entfernt das Hintergrundbild
}
}

View File

@@ -0,0 +1,124 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.monopoly.client.gui;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import pp.monopoly.client.MonopolyApp;
import pp.monopoly.game.server.PlayerColor;
import pp.monopoly.model.Board;
import pp.monopoly.model.Figure;
import pp.monopoly.model.Rotation;
import static pp.util.FloatMath.HALF_PI;
import static pp.util.FloatMath.PI;
/**
* The {@code GameBoardSynchronizer} 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 BoardSynchronizer} to provide specific synchronization
* logic for the sea map.
*/
class GameBoardSynchronizer extends BoardSynchronizer {
private static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md"; //NON-NLS
private static final String LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
private static final String COLOR = "Color"; //NON-NLS
private static final String FIGURE = "figure"; //NON-NLS
private final MonopolyApp app;
private final ParticleEffectFactory particleFactory;
/**
* Constructs a {@code GameBoardSynchronizer} 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 GameBoardSynchronizer(MonopolyApp app, Node root, Board board) {
super(board, root);
this.app = app;
this.particleFactory = new ParticleEffectFactory(app);
addExisting();
}
/**
* 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
*/
public Spatial visit(Figure figure) {
final Node node = new Node(FIGURE);
node.attachChild(createBox(figure));
// compute the center of the ship in world coordinates
final float x = 1;
final float z = 1;
node.setLocalTranslation(x, 0f, z);
return node;
}
/**
* 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(Figure figure) {
final Box box = new Box(0.5f * (figure.getMaxY() - figure.getMinY()) + 0.3f,
0.3f,
0.5f * (figure.getMaxX() - figure.getMinX()) + 0.3f);
final Geometry geometry = new Geometry(FIGURE, box);
geometry.setMaterial(createColoredMaterial(PlayerColor.PINK.getColor()));
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;
}
/**
* Calculates the rotation angle for the specified rotation.
*
* @param rot the rotation of the battleship
* @return the rotation angle in radians
*/
private static float calculateRotationAngle(Rotation rot) {
return switch (rot) {
case RIGHT -> HALF_PI;
case DOWN -> 0f;
case LEFT -> -HALF_PI;
case UP -> PI;
};
}
}

View File

@@ -0,0 +1,51 @@
package pp.monopoly.client.gui;
import com.jme3.math.ColorRGBA;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.style.ElementId;
import pp.dialog.Dialog;
import pp.monopoly.client.MonopolyApp;
public class GameMenu extends Dialog {
private final MonopolyApp app;
/**
* Constructs the SettingsMenu dialog for the Monopoly application.
*
* @param app the MonopolyApp instance
*/
public GameMenu(MonopolyApp app) {
super(app.getDialogManager());
this.app = app;
// Add a title label for Settings
Label settingsTitle = new Label("Einstellungen", new ElementId("settings-title"));
settingsTitle.setFontSize(48); // Set font size for the title
settingsTitle.setColor(ColorRGBA.White);
// Add any settings-related components here, such as volume control, toggles, etc.
// Add a back button to return to StartMenu
Button backButton = new Button("Zurück", new ElementId("menu-button"));
backButton.setColor(ColorRGBA.White);
backButton.setFontSize(24);
backButton.addClickCommands(source -> returnToStartMenu());
// Add components to this dialog
addChild(settingsTitle);
addChild(backButton);
// You can add more settings components here, like checkboxes or sliders.
}
/**
* Returns to the StartMenu when the back button is clicked.
*/
private void returnToStartMenu() {
app.getDialogManager().close(this); // Close the current settings dialog
//TODO return zum Ausgangsmenü
}
}

View File

@@ -0,0 +1,75 @@
package pp.monopoly.client.gui;
import com.jme3.material.Material;
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.CullHint;
import com.jme3.scene.shape.Quad;
import pp.monopoly.client.MonopolyApp;
import pp.monopoly.model.Board;
/**
* Represents the visual view of a {@link Board}, used to display the map structure and elements.
* This class handles the graphical representation of the board, including background setup and grid lines.
*/
class MapView {
private static final float FIELD_SIZE = 40f;
private static final float BACKGROUND_DEPTH = -4f;
private static final ColorRGBA BACKGROUND_COLOR = new ColorRGBA(0, 0.05f, 0.05f, 0.5f);
private final MonopolyApp app;
private final Node mapNode = new Node("map");
private final Board board;
private final MapViewSynchronizer synchronizer;
/**
* Constructs a new MapView for a given {@link Board} and {@link MonopolyApp}.
*
* @param board the board to visualize
* @param app the main application instance
*/
MapView(Board board, MonopolyApp app) {
this.board = board;
this.app = app;
this.synchronizer = new MapViewSynchronizer(this);
setupBackground();
app.getGameLogic().addListener(synchronizer);
}
/**
* Unregisters the {@link MapViewSynchronizer} from listening to board changes.
*/
void unregister() {
app.getGameLogic().removeListener(synchronizer);
}
/**
* Sets up the background of the map view using a quad geometry.
*/
private void setupBackground() {
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", BACKGROUND_COLOR);
mat.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
Geometry background = new Geometry("MapBackground", new Quad(board.getWidth() * FIELD_SIZE, board.getHeight() * FIELD_SIZE));
background.setMaterial(mat);
background.setLocalTranslation(0f, 1f, BACKGROUND_DEPTH);
background.setCullHint(CullHint.Never);
mapNode.attachChild(background);
}
/**
* 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;
}
public Board getBoard() {
return board;
}
}

View File

@@ -0,0 +1,45 @@
package pp.monopoly.client.gui;
import com.jme3.scene.Spatial;
import pp.monopoly.model.Figure;
/**
* Synchronizes the visual representation of the board with the game model.
* Handles updates for items on the board.
*/
class MapViewSynchronizer extends BoardSynchronizer {
private final MapView view;
/**
* Constructs a new MapViewSynchronizer for the given MapView.
*
* @param view the MapView to synchronize with the game model
*/
public MapViewSynchronizer(MapView view) {
super(view.getBoard(), view.getNode());
this.view = view;
addExisting();
}
/**
* Enables the state by performing initial setup, such as adding any items to the view.
*/
protected void enableState() {
// Platz für zusätzliche Initialisierungen
}
/**
* Disables the state by clearing the view.
*/
protected void disableState() {
view.getNode().detachAllChildren(); // Entfernt alle visuellen Elemente vom Knoten
}
public Spatial visit(Figure figure) {
return figure.accept(this);
}
}

View File

@@ -0,0 +1,22 @@
package pp.monopoly.client.gui;
import com.jme3.effect.ParticleMesh.Type;
import pp.monopoly.client.MonopolyApp;
/**
* 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 MonopolyApp app;
ParticleEffectFactory(MonopolyApp app) {
this.app = app;
}
}

View File

@@ -0,0 +1,100 @@
package pp.monopoly.client.gui;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Quad;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Checkbox;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.Slider;
import com.simsilica.lemur.component.QuadBackgroundComponent;
import com.simsilica.lemur.style.ElementId;
import pp.dialog.Dialog;
import pp.monopoly.client.MonopolyApp;
/**
* SettingsMenu ist ein Overlay-Menü, das durch ESC aufgerufen werden kann.
*/
public class SettingsMenu extends Dialog {
private final MonopolyApp app;
private final Geometry overlayBackground;
private final Container settingsContainer;
public SettingsMenu(MonopolyApp app) {
super(app.getDialogManager());
this.app = app;
// Halbtransparentes Overlay hinzufügen
overlayBackground = createOverlayBackground();
app.getGuiNode().attachChild(overlayBackground);
// Hauptcontainer für das Menü
settingsContainer = new Container();
settingsContainer.setBackground(new QuadBackgroundComponent(new ColorRGBA(0.1f, 0.1f, 0.1f, 0.9f)));
// Titel
Label settingsTitle = settingsContainer.addChild(new Label("Einstellungen", new ElementId("settings-title")));
settingsTitle.setFontSize(48);
// Effekt-Sound: Slider und Checkbox
Container effectSoundContainer = settingsContainer.addChild(new Container());
effectSoundContainer.addChild(new Label("Effekt Sound", new ElementId("label")));
effectSoundContainer.addChild(new Slider());
effectSoundContainer.addChild(new Checkbox("Aktivieren")).setChecked(true);
// Hintergrundmusik: Slider und Checkbox
Container backgroundMusicContainer = settingsContainer.addChild(new Container());
backgroundMusicContainer.addChild(new Label("Hintergrund Musik", new ElementId("label")));
backgroundMusicContainer.addChild(new Slider());
backgroundMusicContainer.addChild(new Checkbox("Aktivieren")).setChecked(true);
// Beenden-Button
Button quitButton = settingsContainer.addChild(new Button("Beenden", new ElementId("menu-button")));
quitButton.setFontSize(32);
quitButton.addClickCommands(source -> app.stop());
// Zentriere das Menü
settingsContainer.setLocalTranslation(
(app.getCamera().getWidth() - settingsContainer.getPreferredSize().x) / 2,
(app.getCamera().getHeight() + settingsContainer.getPreferredSize().y) / 2,
1
);
app.getGuiNode().attachChild(settingsContainer);
}
/**
* Erstellt einen halbtransparenten Hintergrund für das Menü.
*
* @return Geometrie des Overlays
*/
private Geometry createOverlayBackground() {
Quad quad = new Quad(app.getCamera().getWidth(), app.getCamera().getHeight());
Geometry overlay = new Geometry("Overlay", quad);
Material material = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
material.setColor("Color", new ColorRGBA(0, 0, 0, 0.5f)); // Halbtransparent
material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
overlay.setMaterial(material);
overlay.setLocalTranslation(0, 0, 0);
return overlay;
}
/**
* Schließt das Menü und entfernt die GUI-Elemente.
*/
@Override
public void close() {
System.out.println("Schließe SettingsMenu..."); // Debugging-Ausgabe
app.getGuiNode().detachChild(settingsContainer); // Entferne das Menü
app.getGuiNode().detachChild(overlayBackground); // Entferne das Overlay
app.setSettingsMenuOpen(false); // Menü als geschlossen markieren
app.unblockInputs(); // Eingaben wieder aktivieren
System.out.println("SettingsMenu geschlossen."); // Debugging-Ausgabe
}
}

View File

@@ -0,0 +1,107 @@
package pp.monopoly.client.gui;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.texture.Texture;
import pp.monopoly.client.MonopolyApp;
/**
* TestWorld zeigt eine einfache Szene mit einem texturierten Quadrat.
* Die Kamera wird durch den CameraController gesteuert.
*/
public class TestWorld {
private final MonopolyApp app;
private CameraController cameraController; // Steuert die Kamera
private Geometry cube; // Spielfigur
/**
* Konstruktor für TestWorld.
*
* @param app Die Hauptanwendung (MonopolyApp)
*/
public TestWorld(MonopolyApp app) {
this.app = app;
}
/**
* Initialisiert die Szene und startet die Kamerabewegung.
*/
public void initializeScene() {
app.getGuiNode().detachAllChildren(); // Entferne GUI
app.getRootNode().detachAllChildren(); // Entferne andere Szenenobjekte
setSkyColor(); // Setze den Himmel auf hellblau
createBoard(); // Erstelle das Spielfeld
createCube(); // Füge den Würfel hinzu
// Erstelle den CameraController
cameraController = new CameraController(
app.getCamera(), // Die Kamera der App
Vector3f.ZERO, // Fokus auf die Mitte des Spielfelds
5, // Radius des Kreises
3, // Höhe der Kamera
0.5f // Geschwindigkeit der Bewegung
);
// Füge die Toolbar hinzu
new Toolbar(app, cube);
}
/**
* Aktualisiert die Kameraposition.
*
* @param tpf Zeit pro Frame
*/
public void update(float tpf) {
if (cameraController != null) {
cameraController.update(tpf);
}
}
/**
* Setzt die Hintergrundfarbe der Szene auf hellblau.
*/
private void setSkyColor() {
app.getViewPort().setBackgroundColor(new ColorRGBA(0.5f, 0.7f, 1.0f, 1.0f)); // Hellblauer Himmel
}
/**
* Erstelle das Spielfeld.
*/
private void createBoard() {
// Erstelle ein Quadrat
Box box = new Box(1, 0.01f, 1); // Dünnes Quadrat für die Textur
Geometry geom = new Geometry("Board", box);
// Setze das Material mit Textur
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
Texture texture = app.getAssetManager().loadTexture("Pictures/board.png");
mat.setTexture("ColorMap", texture);
geom.setMaterial(mat);
app.getRootNode().attachChild(geom);
}
/**
* Erstellt den Würfel (Spielfigur) in der Szene.
*/
private void createCube() {
Box box = new Box(0.05f, 0.05f, 0.05f); // Kleinere Größe für Spielfigur
cube = new Geometry("Cube", box);
// Setze das Material für den Würfel
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.Blue); // Blau gefärbter Würfel
cube.setMaterial(mat);
// Setze den Startpunkt des Würfels
cube.setLocalTranslation(0.8999999f, 0.1f, -0.9f);
app.getRootNode().attachChild(cube);
}
}

View File

@@ -0,0 +1,168 @@
package pp.monopoly.client.gui;
import java.util.Random;
import com.jme3.font.BitmapText;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.simsilica.lemur.Axis;
import com.simsilica.lemur.Button;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.component.SpringGridLayout;
import pp.monopoly.client.MonopolyApp;
/**
* Toolbar Klasse, die am unteren Rand der Szene angezeigt wird.
* Die Buttons bewegen den Würfel auf dem Spielfeld.
*/
public class Toolbar {
private final MonopolyApp app;
private final Container toolbarContainer;
private final Geometry cube; // Referenz auf den Würfel
private final BitmapText positionText; // Anzeige für die aktuelle Position
private final float boardLimit = 0.95f; // Grenzen des Bretts
private final float stepSize = 0.18f; // Schrittgröße pro Bewegung
private int currentPosition = 0; // Aktuelle Position auf dem Spielfeld
private final int positionsPerSide = 10; // Anzahl der Positionen pro Seite
private final Random random = new Random(); // Zufallsgenerator für den Würfelwurf
/**
* Konstruktor für die Toolbar.
*
* @param app Die Hauptanwendung (MonopolyApp)
* @param cube Der Würfel, der bewegt werden soll
*/
public Toolbar(MonopolyApp app, Geometry cube) {
this.app = app;
this.cube = cube;
// Erstelle die Toolbar
toolbarContainer = new Container(new SpringGridLayout(Axis.X, Axis.Y));
// Setze die Position am unteren Rand und die Breite
toolbarContainer.setLocalTranslation(
0, // Links bündig
100, // Höhe über dem unteren Rand
0 // Z-Ebene
);
toolbarContainer.setPreferredSize(new Vector3f(app.getCamera().getWidth(), 100, 0)); // Volle Breite
// Füge Buttons zur Toolbar hinzu
initializeButtons();
// Füge die Toolbar zur GUI hinzu
app.getGuiNode().attachChild(toolbarContainer);
// Erstelle die Position-Anzeige
positionText = createPositionDisplay();
updatePositionDisplay(); // Initialisiere die Anzeige mit der Startposition
}
/**
* Initialisiert die Buttons in der Toolbar.
*/
private void initializeButtons() {
addButton("Vorwärts", 1); // Bewegung nach vorne
addButton("Rückwärts", -1); // Bewegung nach hinten
addDiceRollButton(); // Würfel-Button
}
/**
* Fügt einen Button mit einer Bewegung hinzu.
*
* @param label Der Text des Buttons
* @param step Schrittweite (+1 für vorwärts, -1 für rückwärts)
*/
private void addButton(String label, int step) {
Button button = new Button(label);
button.setPreferredSize(new Vector3f(150, 50, 0)); // Größe der Buttons
button.addClickCommands(source -> moveCube(step));
toolbarContainer.addChild(button);
}
/**
* Fügt den Würfel-Button hinzu, der die Figur entsprechend der gewürfelten Zahl bewegt.
*/
private void addDiceRollButton() {
Button diceButton = new Button("Würfeln");
diceButton.setPreferredSize(new Vector3f(150, 50, 0)); // Größe des Buttons
diceButton.addClickCommands(source -> rollDice());
toolbarContainer.addChild(diceButton);
}
/**
* Simuliert einen Würfelwurf und bewegt die Figur entsprechend.
*/
private void rollDice() {
int diceRoll = random.nextInt(6) + 1; // Zahl zwischen 1 und 6
System.out.println("Gewürfelt: " + diceRoll);
moveCube(diceRoll); // Bewege die Figur um die gewürfelte Zahl
}
/**
* Bewegt den Würfel basierend auf der aktuellen Position auf dem Brett.
*
* @param step Schrittweite (+1 für vorwärts, -1 für rückwärts oder andere Werte)
*/
private void moveCube(int step) {
currentPosition = (currentPosition + step + 4 * positionsPerSide) % (4 * positionsPerSide);
Vector3f newPosition = calculatePosition(currentPosition);
cube.setLocalTranslation(newPosition);
updatePositionDisplay(); // Aktualisiere die Positionsanzeige
System.out.println("Würfelposition: " + newPosition + " (Feld-ID: " + currentPosition + ")");
}
/**
* Berechnet die neue Position des Würfels basierend auf der aktuellen Brettseite und Position.
*
* @param position Aktuelle Position auf dem Spielfeld
* @return Die berechnete Position als Vector3f
*/
private Vector3f calculatePosition(int position) {
int side = position / positionsPerSide; // Seite des Bretts (0 = unten, 1 = rechts, 2 = oben, 3 = links)
int offset = position % positionsPerSide; // Position auf der aktuellen Seite
switch (side) {
case 0: // Unten (positive x-Achse)
return new Vector3f(-boardLimit + offset * stepSize, 0.1f, -boardLimit + 0.05f);
case 1: // Rechts (positive z-Achse)
return new Vector3f(boardLimit - 0.05f, 0.1f, -boardLimit + offset * stepSize);
case 2: // Oben (negative x-Achse)
return new Vector3f(boardLimit - offset * stepSize, 0.1f, boardLimit - 0.05f);
case 3: // Links (negative z-Achse)
return new Vector3f(-boardLimit + 0.05f, 0.1f, boardLimit - offset * stepSize);
default:
throw new IllegalArgumentException("Ungültige Position: " + position);
}
}
/**
* Erstellt die Anzeige für die aktuelle Position.
*
* @return Das BitmapText-Objekt für die Anzeige
*/
private BitmapText createPositionDisplay() {
BitmapText text = new BitmapText(app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"), false);
text.setSize(20); // Schriftgröße
text.setLocalTranslation(10, app.getCamera().getHeight() - 10, 0); // Oben links
app.getGuiNode().attachChild(text);
return text;
}
/**
* Aktualisiert die Anzeige für die aktuelle Position.
*/
private void updatePositionDisplay() {
positionText.setText("Feld-ID: " + currentPosition);
}
/**
* Entfernt die Toolbar.
*/
public void remove() {
app.getGuiNode().detachChild(toolbarContainer);
app.getGuiNode().detachChild(positionText);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

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