mirror of
				https://athene2.informatik.unibw-muenchen.de/progproj/gruppen-ht24/Gruppe-02.git
				synced 2025-10-31 12:21:52 +01:00 
			
		
		
		
	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
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,7 @@ build | |||||||
|  |  | ||||||
| # VSC | # VSC | ||||||
| bin | bin | ||||||
|  | .vscode | ||||||
|  |  | ||||||
| # IntelliJ | # IntelliJ | ||||||
| *.iml | *.iml | ||||||
|   | |||||||
| @@ -20,8 +20,6 @@ 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 solidWhiteBackground = new QuadBackgroundComponent(color(1, 1, 1, 1)) // Solid white | ||||||
| def greyBackground = color(0.8, 0.8, 0.8, 1)  // Grey background color |  | ||||||
| def redBorderColor = color(1, 0, 0, 1)        // Red border color |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ import com.jme3.font.BitmapText; | |||||||
| import com.jme3.input.KeyInput; | import com.jme3.input.KeyInput; | ||||||
| import com.jme3.input.controls.ActionListener; | import com.jme3.input.controls.ActionListener; | ||||||
| import com.jme3.input.controls.KeyTrigger; | import com.jme3.input.controls.KeyTrigger; | ||||||
|  |  | ||||||
| import com.jme3.system.AppSettings; | import com.jme3.system.AppSettings; | ||||||
| import com.simsilica.lemur.GuiGlobals; | import com.simsilica.lemur.GuiGlobals; | ||||||
| import com.simsilica.lemur.Label; | import com.simsilica.lemur.Label; | ||||||
| @@ -25,7 +24,6 @@ import pp.monopoly.game.client.MonopolyClient; | |||||||
| import pp.monopoly.game.client.ServerConnection; | import pp.monopoly.game.client.ServerConnection; | ||||||
| import pp.monopoly.notification.GameEventListener; | import pp.monopoly.notification.GameEventListener; | ||||||
| import pp.monopoly.notification.InfoTextEvent; | import pp.monopoly.notification.InfoTextEvent; | ||||||
| import pp.monopoly.server.MonopolyServer; |  | ||||||
|  |  | ||||||
| public class MonopolyApp extends SimpleApplication implements MonopolyClient, GameEventListener { | public class MonopolyApp extends SimpleApplication implements MonopolyClient, GameEventListener { | ||||||
|     private BitmapText topText; |     private BitmapText topText; | ||||||
| @@ -40,8 +38,6 @@ public class MonopolyApp extends SimpleApplication implements MonopolyClient, Ga | |||||||
|     private TestWorld testWorld; |     private TestWorld testWorld; | ||||||
|     private boolean isSettingsMenuOpen = false; |     private boolean isSettingsMenuOpen = false; | ||||||
|     private boolean inputBlocked = false; |     private boolean inputBlocked = false; | ||||||
|     private MonopolyServer monopolyServer; |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Path to the styles script for GUI elements. |      * Path to the styles script for GUI elements. | ||||||
|      */ |      */ | ||||||
| @@ -98,10 +94,6 @@ public class MonopolyApp extends SimpleApplication implements MonopolyClient, Ga | |||||||
|         StartMenu.createStartMenu(this); |         StartMenu.createStartMenu(this); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private void setupGui() { |     private void setupGui() { | ||||||
|         BitmapFont normalFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); |         BitmapFont normalFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); | ||||||
|         topText = new BitmapText(normalFont); |         topText = new BitmapText(normalFont); | ||||||
| @@ -116,7 +108,7 @@ public class MonopolyApp extends SimpleApplication implements MonopolyClient, Ga | |||||||
|         inputManager.addListener(escapeListener, "ESC"); |         inputManager.addListener(escapeListener, "ESC"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     void handleEscape(boolean isPressed) { |     private void handleEscape(boolean isPressed) { | ||||||
|         if (isPressed) { |         if (isPressed) { | ||||||
|             if (settingsMenu != null && isSettingsMenuOpen) { |             if (settingsMenu != null && isSettingsMenuOpen) { | ||||||
|                 // Schließe das SettingsMenu |                 // Schließe das SettingsMenu | ||||||
| @@ -215,21 +207,4 @@ public class MonopolyApp extends SimpleApplication implements MonopolyClient, Ga | |||||||
|         guiNode.detachAllChildren(); // Entferne die GUI |         guiNode.detachAllChildren(); // Entferne die GUI | ||||||
|         StartMenu.createStartMenu(this); // Zeige das Startmenü erneut |         StartMenu.createStartMenu(this); // Zeige das Startmenü erneut | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Startet den Server in einem neuen Thread. |  | ||||||
|      */ |  | ||||||
|     public void startServer() { |  | ||||||
|         new Thread(() -> { |  | ||||||
|             try { |  | ||||||
|                 monopolyServer = new MonopolyServer(); // Erstelle Serverinstanz |  | ||||||
|             } catch (Exception e) { |  | ||||||
|                 e.printStackTrace(); |  | ||||||
|             } |  | ||||||
|         }).start(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public MonopolyServer getMonopolyServer() { |  | ||||||
|         return monopolyServer; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| package pp.monopoly.client; | package pp.monopoly.client; | ||||||
|  |  | ||||||
| import com.jme3.material.Material; | import com.jme3.material.Material; | ||||||
|  | import com.jme3.math.ColorRGBA; | ||||||
| import com.jme3.math.Vector3f; | import com.jme3.math.Vector3f; | ||||||
| import com.jme3.scene.Geometry; | import com.jme3.scene.Geometry; | ||||||
| import com.jme3.scene.shape.Quad; | import com.jme3.scene.shape.Quad; | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Projekte/monopoly/client/src/main/resources/Pictures/board.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Projekte/monopoly/client/src/main/resources/Pictures/board.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 857 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 750 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 7.1 MiB | 
							
								
								
									
										
											BIN
										
									
								
								Projekte/monopoly/client/src/main/resources/icons/test.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Projekte/monopoly/client/src/main/resources/icons/test.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.2 KiB | 
		Reference in New Issue
	
	Block a user