From f23e74f99ac249d2ba974cdcc8336b2cc7ebc9b9 Mon Sep 17 00:00:00 2001
From: jez04 <david.jezek@post.cz>
Date: Mon, 4 Nov 2024 02:58:36 +0100
Subject: [PATCH] feat: :tada: solution

---
 src/main/java/lab/App.java                    |  23 +-
 src/main/java/lab/Bullet.java                 |   4 +
 src/main/java/lab/BulletAnimated.java         |   4 +-
 src/main/java/lab/Cannon.java                 |  17 +-
 src/main/java/lab/DrawingThread.java          |   4 +
 src/main/java/lab/GameController.java         |  55 +++++
 src/main/java/lab/World.java                  |  17 +-
 src/main/resources/lab/gameWindow.fxml        |  42 ++++
 .../structure/test/ClassStructureTest.java    | 207 +++++++++++++-----
 9 files changed, 294 insertions(+), 79 deletions(-)
 create mode 100644 src/main/java/lab/GameController.java
 create mode 100644 src/main/resources/lab/gameWindow.fxml

diff --git a/src/main/java/lab/App.java b/src/main/java/lab/App.java
index 58fd3f3..735a8b4 100644
--- a/src/main/java/lab/App.java
+++ b/src/main/java/lab/App.java
@@ -1,10 +1,9 @@
 package lab;
 
-import javafx.animation.AnimationTimer;
 import javafx.application.Application;
-import javafx.scene.Group;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
 import javafx.scene.Scene;
-import javafx.scene.canvas.Canvas;
 import javafx.stage.Stage;
 import javafx.stage.WindowEvent;
 
@@ -16,31 +15,25 @@ import javafx.stage.WindowEvent;
  */
 public class App extends Application {
 
+	private GameController gameController;
 	public static void main(String[] args) {
 		launch(args);
 	}
 
-	private Canvas canvas;
-	private AnimationTimer timer;
-
 	@Override
 	public void start(Stage primaryStage) {
 		try {
 			// Construct a main window with a canvas.
-			Group root = new Group();
-			canvas = new Canvas(800, 400);
-			root.getChildren().add(canvas);
-			Scene scene = new Scene(root, 800, 400);
-			scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
+			FXMLLoader gameLoader = new FXMLLoader(getClass().getResource("/lab/gameWindow.fxml"));
+			Parent root = gameLoader.load();
+			GameController gameController = gameLoader.getController();
+			Scene scene = new Scene(root);
 			primaryStage.setScene(scene);
 			primaryStage.resizableProperty().set(false);
 			primaryStage.setTitle("Java 1 - 1th laboratory");
 			primaryStage.show();
-
 			// Exit program when main window is closed
 			primaryStage.setOnCloseRequest(this::exitProgram);
-			timer = new DrawingThread(canvas);
-			timer.start();
 		} catch (Exception e) {
 			e.printStackTrace();
 		}
@@ -48,7 +41,7 @@ public class App extends Application {
 
 	@Override
 	public void stop() throws Exception {
-		timer.stop();
+		gameController.stop();
 		super.stop();
 	}
 
diff --git a/src/main/java/lab/Bullet.java b/src/main/java/lab/Bullet.java
index 2172d11..76f904f 100644
--- a/src/main/java/lab/Bullet.java
+++ b/src/main/java/lab/Bullet.java
@@ -44,4 +44,8 @@ public class Bullet extends WorldEntity implements Collisionable{
 		
 	}
 
+	public void setVelocity(Point2D velocity) {
+		this.velocity = velocity;
+	}
+
 }
diff --git a/src/main/java/lab/BulletAnimated.java b/src/main/java/lab/BulletAnimated.java
index 2134c12..d74427c 100644
--- a/src/main/java/lab/BulletAnimated.java
+++ b/src/main/java/lab/BulletAnimated.java
@@ -33,7 +33,7 @@ public class BulletAnimated extends Bullet {
 
 	public void reload() {
 		position = cannon.getPosition();
-		velocity = initVelocity;
+		velocity = new Point2D(0, 0);
 	}
-
+	
 }
\ No newline at end of file
diff --git a/src/main/java/lab/Cannon.java b/src/main/java/lab/Cannon.java
index 59f6d21..e31f1ed 100644
--- a/src/main/java/lab/Cannon.java
+++ b/src/main/java/lab/Cannon.java
@@ -27,9 +27,18 @@ public class Cannon extends WorldEntity{
 
 	@Override
 	public void simulate(double deltaT) {
-		angle += angleDelta * deltaT;
-		if (angle >= 0 || angle <= -90) {
-			angleDelta = -angleDelta;
-		}
+//		angle += angleDelta * deltaT;
+//		if (angle >= 0 || angle <= -90) {
+//			angleDelta = -angleDelta;
+//		}
 	}
+
+	public double getAngle() {
+		return angle;
+	}
+
+	public void setAngle(double angle) {
+		this.angle = -angle;
+	}
+	
 }
diff --git a/src/main/java/lab/DrawingThread.java b/src/main/java/lab/DrawingThread.java
index 50574b1..8c0f65f 100644
--- a/src/main/java/lab/DrawingThread.java
+++ b/src/main/java/lab/DrawingThread.java
@@ -31,4 +31,8 @@ public class DrawingThread extends AnimationTimer {
 		lastTime = now;
 	}
 
+	public World getWorld() {
+		return world;
+	}
+
 }
diff --git a/src/main/java/lab/GameController.java b/src/main/java/lab/GameController.java
new file mode 100644
index 0000000..3ad7263
--- /dev/null
+++ b/src/main/java/lab/GameController.java
@@ -0,0 +1,55 @@
+package lab;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
+import javafx.geometry.Point2D;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.control.Slider;
+
+public class GameController {
+
+	@FXML
+	private Slider angle;
+
+	@FXML
+	private Slider speed;
+
+	@FXML
+	private Canvas canvas;
+
+	private DrawingThread timer;
+
+	@FXML
+	void fire(ActionEvent event) {
+		double angle = timer.getWorld().getCannon().getAngle();
+		double angleRad = Math.toRadians(angle);
+		double speedValue = speed.getValue();
+		timer.getWorld().getBulletAnimated().reload();
+		Point2D velocity = new Point2D(
+				Math.cos(angleRad)*speedValue, 
+				Math.sin(angleRad)*speedValue); 
+		timer.getWorld().getBulletAnimated().setVelocity(velocity);
+	}
+
+	@FXML
+	void initialize() {
+		assert angle != null : "fx:id=\"angle\" was not injected: check your FXML file 'gameWindow.fxml'.";
+		assert canvas != null : "fx:id=\"canvas\" was not injected: check your FXML file 'gameWindow.fxml'.";
+		assert speed != null : "fx:id=\"speed\" was not injected: check your FXML file 'gameWindow.fxml'.";
+		timer = new DrawingThread(canvas);
+		timer.start();
+		angle.valueProperty().addListener(new ChangeListener<Number>() {
+			@Override
+			public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
+				timer.getWorld().getCannon().setAngle(newValue.doubleValue());
+			}
+		});
+	}
+
+	public void stop() {
+		timer.stop();
+	}
+
+}
diff --git a/src/main/java/lab/World.java b/src/main/java/lab/World.java
index 253af0e..1f7b4e5 100644
--- a/src/main/java/lab/World.java
+++ b/src/main/java/lab/World.java
@@ -12,16 +12,19 @@ public class World {
 	private final double height;
 
 	private DrawableSimulable[] entities;
+	private Cannon cannon;
+	private BulletAnimated bulletAnimated;
 
 	public World(double width, double height) {
 		this.width = width;
 		this.height = height;
 		entities = new DrawableSimulable[8];
-		Cannon cannon = new Cannon(this, new Point2D(0, height - 20), -45);
+		cannon = new Cannon(this, new Point2D(0, height - 20), -45);
+		bulletAnimated = new BulletAnimated(this, cannon, new Point2D(0, height), new Point2D(50, -80),
+				new Point2D(0, 9.81));
 		entities[0] = cannon;
 		entities[1] = new Bullet(this, new Point2D(0, height), new Point2D(30, -30), new Point2D(0, 9.81));
-		entities[2] = new BulletAnimated(this, cannon, new Point2D(0, height), new Point2D(50, -80),
-				new Point2D(0, 9.81));
+		entities[2] = bulletAnimated;
 		for (int i = 3; i < entities.length; i++) {
 			entities[i] = new Ufo(this);
 		}
@@ -63,4 +66,12 @@ public class World {
 		return height;
 	}
 
+	public Cannon getCannon() {
+		return cannon;
+	}
+
+	public BulletAnimated getBulletAnimated() {
+		return bulletAnimated;
+	}
+
 }
\ No newline at end of file
diff --git a/src/main/resources/lab/gameWindow.fxml b/src/main/resources/lab/gameWindow.fxml
new file mode 100644
index 0000000..b65e058
--- /dev/null
+++ b/src/main/resources/lab/gameWindow.fxml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.Cursor?>
+<?import javafx.scene.canvas.Canvas?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.Slider?>
+<?import javafx.scene.layout.BorderPane?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.text.Font?>
+
+<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="lab.GameController">
+   <bottom>
+      <HBox alignment="TOP_CENTER" prefHeight="100.0" prefWidth="200.0" BorderPane.alignment="CENTER">
+         <children>
+            <Slider fx:id="angle" majorTickUnit="15.0" max="90.0" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minorTickCount="5" showTickLabels="true" showTickMarks="true" HBox.hgrow="ALWAYS" />
+            <Button mnemonicParsing="false" onAction="#fire" style="-fx-background-color: RED;" text="Fire" textAlignment="CENTER">
+               <font>
+                  <Font name="System Bold" size="24.0" />
+               </font>
+               <cursor>
+                  <Cursor fx:constant="CROSSHAIR" />
+               </cursor>
+               <opaqueInsets>
+                  <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
+               </opaqueInsets>
+               <HBox.margin>
+                  <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+               </HBox.margin>
+            </Button>
+            <Slider fx:id="speed" max="200.0" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" min="50.0" minorTickCount="5" showTickLabels="true" showTickMarks="true" value="50.0" HBox.hgrow="ALWAYS" />
+         </children>
+      </HBox>
+   </bottom>
+   <center>
+      <Canvas fx:id="canvas" height="287.0" width="582.0" BorderPane.alignment="CENTER">
+         <BorderPane.margin>
+            <Insets />
+         </BorderPane.margin>
+      </Canvas>
+   </center>
+</BorderPane>
diff --git a/src/test/java/jez04/structure/test/ClassStructureTest.java b/src/test/java/jez04/structure/test/ClassStructureTest.java
index ac5f14c..c545ff8 100644
--- a/src/test/java/jez04/structure/test/ClassStructureTest.java
+++ b/src/test/java/jez04/structure/test/ClassStructureTest.java
@@ -1,92 +1,186 @@
 package jez04.structure.test;
 
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.reflections.Configuration;
 import org.reflections.Reflections;
 import org.reflections.scanners.Scanners;
 import org.reflections.util.ConfigurationBuilder;
 
+import javafx.event.ActionEvent;
+import javafx.fxml.FXML;
 import javafx.geometry.Rectangle2D;
+import javafx.scene.canvas.GraphicsContext;
 
 class ClassStructureTest {
 
+	private static final String drawableSimulableName = "DrawableSimulable";
+	private static final String collisionableName = "Collisionable";
+
+	Set<String> allClasses = getNameOfAllClasses();
+
 	@Test
-	void ufoClassEexistanceTest() {
-		Set<String> allClasses = getNameOfAllClasses();
-		assertTrue(allClasses.stream().anyMatch(c -> c.endsWith("Ufo")), "Class Ufo not found");
+	void gameControllerExistenceTest() {
+		classExist("GameController");
+		Class<?> c = getClass("GameController");
+		hasPropertyWithAnnotation(c, ".*", FXML.class);
+		hasMethodRegexp(c, ".*", void.class, ActionEvent.class);
 	}
-
 	@Test
-	void ufoClassPropertiesTest() {
-		Set<String> allClasses = getNameOfAllClasses();
-		String ufoClassName = allClasses.stream().filter(c -> c.endsWith("Ufo")).findAny().orElse(null);
-		assertNotNull(ufoClassName, "Class Ufo not found");
-		try {
-			Class<?> ufoClass = Class.forName(ufoClassName);
-			List<Method> methods = Arrays.asList(ufoClass.getMethods());
-			assertTrue(methods.stream().anyMatch(m -> m.getName().contains("draw")), "No method draw");
-			assertTrue(methods.stream().anyMatch(m -> m.getName().contains("simulate")), "No method simulate");
-			assertTrue(methods.stream().anyMatch(m -> m.getName().contains("getBoundingBox")),
-					"No method getBoundingBox");
-			assertTrue(
-					methods.stream().filter(m -> m.getName().contains("getBoundingBox"))
-							.anyMatch(m -> m.getReturnType().equals(Rectangle2D.class)),
-					"Method getBoundingBox not return Rectangle2D");
-		} catch (ClassNotFoundException e) {
-			e.printStackTrace();
-		}
+	void gameControllerFxmlTest() {
+		classExist("GameController");
+		Class<?> c = getClass("GameController");
+		hasPropertyWithAnnotation(c, ".*", FXML.class);
+	}
+	@Test
+	void gameControllerActionEventTest() {
+		classExist("GameController");
+		Class<?> c = getClass("GameController");
+		hasMethodRegexp(c, ".*", void.class, ActionEvent.class);
 	}
 
 	@Test
-	void worldClassPropertyTest() {
-		Set<String> allClasses = getNameOfAllClasses();
-		String ufoClassName = allClasses.stream().filter(c -> c.endsWith("Ufo")).findAny().orElse(null);
-		String worldClassName = allClasses.stream().filter(c -> c.endsWith("World")).findAny().orElse(null);
-		assertNotNull(worldClassName, "Class World not found");
+	void cannonExistenceTest() {
+		classExist("Cannon");
+	}
+	@Test
+	void cannonExistenceSetAngleTest() {
+		classExist("Cannon");
+		Class<?> c = getClass("Cannon");
+		hasMethod(c, "setAngle");
+	}
+
+
+	private void isInterface(Class<?> c) {
+		assertTrue(c.isInterface(), c.getName() + " have to be interface.");
+	}
+
+	private void classExist(String name) {
+		assertTrue(allClasses.stream().anyMatch(c -> c.endsWith(name)), "Interface " + name + " not found");
+	}
+
+	private Class<?> getClass(String name) {
+		String className = allClasses.stream().filter(c -> c.endsWith(name)).findAny().orElse(null);
+		if (className == null) {
+			Assertions.fail("Class " + name + " not found.");
+		}
 		try {
-			Class<?> worldClass = Class.forName(worldClassName);
-			List<Field> fields = Arrays.asList(worldClass.getDeclaredFields());
-			Class<?> ufoClass = Class.forName(ufoClassName);
-			assertTrue(
-					fields.stream()
-							.anyMatch(f -> f.getType().isArray() && f.getType().getComponentType().equals(ufoClass)),
-					"No array of Ufo in World class");
+			return Class.forName(className);
 		} catch (ClassNotFoundException e) {
-			e.printStackTrace();
+			final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+			try (PrintStream ps = new PrintStream(baos, true)) {
+				e.printStackTrace(ps);
+			} catch (Exception e2) {
+				Assertions.fail(e2.getMessage());
+			}
+			String stackTrace = baos.toString();
+			Assertions.fail("Class " + name + " not found.\n" + stackTrace);
+			return null;
 		}
 	}
 
-	@Test
-	void drawingClassExistenceTest() {
-		Set<String> allClasses = getNameOfAllClasses();
-		int detectedClassCount = 0;
-		for (String className : allClasses) {
-			try {
-				Class<?> clazz = Class.forName(className);
-				List<Constructor<?>> constructorList = Arrays.asList(clazz.getConstructors());
-				List<Method> methodList = Arrays.asList(clazz.getMethods());
-				long drawMethodCount = methodList.stream().filter(m -> m.getName().contains("draw")).count();
-				long simulateMethodCount = methodList.stream().filter(m -> m.getName().contains("sim")).count();
-				if (!constructorList.isEmpty() && (drawMethodCount > 0 || simulateMethodCount > 0)) {
-					System.out.println("DETECT:" + className);
-					detectedClassCount++;
+	private void hasProperty(Class<?> classDef, String propertyNameRegexp, Class<?> type, boolean array) {
+		List<Field> fields = Arrays.asList(classDef.getDeclaredFields());
+		assertTrue(fields.stream().anyMatch(f -> {
+			if (f.getName().matches(propertyNameRegexp)) {
+				if (array) {
+					return f.getType().isArray() && f.getType().getComponentType().equals(type);
+				} else {
+					return f.getType().equals(type);
 				}
-			} catch (ClassNotFoundException e) {
-				System.out.println(String.format("Class '%s' cannot be loaded: %s", className, e.getMessage()));
 			}
-		}
-		assertTrue(detectedClassCount >= 2, "");
+			return false;
+		}), "No field " + propertyNameRegexp + " of type " + type.getName() + " (is array " + array + ") in class "
+				+ classDef.getName());
+	}
+
+	private void hasPropertyWithAnnotation(Class<?> classDef, String propertyNameRegexp, Class<?> annotation) {
+		List<Field> fields = Arrays.asList(classDef.getDeclaredFields());
+		assertTrue(
+				fields.stream().filter(f -> f.getName().matches(propertyNameRegexp))
+						.flatMap(f -> Arrays.asList(
+								f.getAnnotations()).stream()).map(a -> a.annotationType()).anyMatch(a -> 
+								a.equals(annotation)),
+				"No field " + propertyNameRegexp + " with annotation " + annotation.getName() + " in class "
+						+ classDef.getName());
+	}
+
+	private void hasMethod(Class<?> interfaceDef, String methodName, Class<?> returnType) {
+		List<Method> methods = Arrays.asList(interfaceDef.getDeclaredMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().contains(methodName)), "No method " + methodName);
+		assertTrue(
+				methods.stream().filter(m -> m.getName().contains(methodName))
+						.anyMatch(m -> m.getReturnType().equals(returnType)),
+				"Method " + methodName + " not return " + returnType.getName());
+	}
+
+	private void hasMethod(Class<?> interfaceDef, String methodName, Class<?> returnType, Class<?>... params) {
+		List<Method> methods = Arrays.asList(interfaceDef.getDeclaredMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().contains(methodName)), "No method " + methodName);
+		assertTrue(
+				methods.stream().filter(m -> m.getName().contains(methodName))
+						.filter(m -> m.getReturnType().equals(returnType))
+						.anyMatch(m -> Arrays.asList(m.getParameterTypes()).containsAll(Arrays.asList(params))),
+				"Method " + methodName + " has no all parrams:"
+						+ Arrays.asList(params).stream().map(Class::getName).collect(Collectors.joining(", ")));
+	}
+
+	private void hasMethodRegexp(Class<?> interfaceDef, String methodNameRegexp, Class<?> returnType, Class<?>... params) {
+		List<Method> methods = Arrays.asList(interfaceDef.getDeclaredMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().matches(methodNameRegexp)), "No method " + methodNameRegexp);
+		assertTrue(
+				methods.stream().filter(m -> m.getName().matches(methodNameRegexp))
+						.filter(m -> m.getReturnType().equals(returnType))
+						.anyMatch(m -> 
+						Arrays.asList(m.getParameterTypes()).containsAll(Arrays.asList(params))),
+				"Method " + methodNameRegexp + " has no all parrams:"
+						+ Arrays.asList(params).stream().map(Class::getName).collect(Collectors.joining(", ")));
+	}
+	private void hasMethod(Class<?> interfaceDef, boolean finalTag, boolean abstractTag, String methodName,
+			Class<?> returnType, Class<?>... params) {
+		List<Method> methods = Arrays.asList(interfaceDef.getDeclaredMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().contains(methodName)), "No method " + methodName);
+		assertTrue(
+				methods.stream().filter(m -> m.getName().contains(methodName))
+						.filter(m -> m.getReturnType().equals(returnType)
+								&& (Modifier.isAbstract(m.getModifiers()) == abstractTag)
+								&& (Modifier.isFinal(m.getModifiers()) == finalTag))
+						.anyMatch(m -> Arrays.asList(m.getParameterTypes()).containsAll(Arrays.asList(params))),
+				"Method " + methodName + " has no all params:"
+						+ Arrays.asList(params).stream().map(Class::getName).collect(Collectors.joining(", ")));
+	}
+
+	private void hasImplements(Class<?> clazz, String... interfaceNames) {
+		List<Class<?>> interfaces = new ArrayList<>();
+		Arrays.asList(interfaceNames).stream().map(name -> getClass(name)).forEach(c -> interfaces.add(c));
+		assertTrue(Arrays.asList(clazz.getInterfaces()).containsAll(interfaces), "Class not implements all interfaces:"
+				+ interfaces.stream().map(Class::getName).collect(Collectors.joining(", ")));
+	}
+
+	private void hasExtends(Class<?> clazz, String parentName) {
+		Class<?> parent = getClass(parentName);
+		assertTrue(clazz.getSuperclass().equals(parent),
+				"Class " + clazz.getName() + " not extends class " + parentName);
+	}
+
+	private void hasMethod(Class<?> interfaceDef, String methodName) {
+		List<Method> methods = Arrays.asList(interfaceDef.getMethods());
+		assertTrue(methods.stream().anyMatch(m -> m.getName().contains(methodName)), "No method " + methodName);
 	}
 
 	private Set<String> getNameOfAllClasses() {
@@ -110,7 +204,10 @@ class ClassStructureTest {
 			Configuration conf = new ConfigurationBuilder().addScanners(Scanners.SubTypes.filterResultsBy(pc -> true))
 					.forPackages(p.getName());
 			Reflections reflections = new Reflections(conf);
-			allClasses.addAll(reflections.getAll(Scanners.SubTypes.filterResultsBy(c -> true)));
+			allClasses.addAll(reflections.getAll(Scanners.SubTypes.filterResultsBy(c -> {
+				System.out.println(c);
+				return true;
+			})));
 		}
 		return allClasses;
 	}
-- 
GitLab