diff --git a/docs/dev/guide.md b/docs/dev/guide.md index a08b227..5eb6434 100644 --- a/docs/dev/guide.md +++ b/docs/dev/guide.md @@ -48,3 +48,6 @@ Chi tiết: [Spotless Gradle Plugin](https://github.com/diffplug/spotless/tree/m * **Commits:** [Conventional Commits](https://www.conventionalcommits.org/) (và viết message bằng **Tiếng Anh**) * **Files Structure:** [Gradle project Structure](https://docs.gradle.org/current/userguide/organizing_gradle_projects.html) + +Ngoài ra: +* [Hướng dẫn viết javadoc](javadoc.md) diff --git a/docs/dev/javadoc.md b/docs/dev/javadoc.md new file mode 100644 index 0000000..cdaaeac --- /dev/null +++ b/docs/dev/javadoc.md @@ -0,0 +1,55 @@ +# 📄 Viết Javadoc + +### 💾 Format + +Javadoc được viết tuân theo các quy tắc javadoc cơ bản và viết trên format HTML. + +### 🏗️ Cấu trúc + +Trên IDE, khi bạn di chuyển lên trên đầu lớp/method và viết `/**` rồi Enter, IDE sẽ tự sinh ra cho bạn khung để bạn viết +javadoc. Cấu trúc của dự án chúng ta sẽ là: + +- Đối với Lớp: + +```java +/** + * + * + *

?{@link Tên Lớp}

+ * + * Mô tả lớp... Có thể sử dụng {@link Class nào đó#TP trong class} để liên kết
+ * Chú ý: Sử dụng thẻ br để xuống dòng
+ * Plot Twist: tui cũng ko bt viết gì ở đây nữa + * + * @see ... + */ + +``` + +trong đó `?` ở Tên lớp sẽ là: + +| Loại lớp | 📚 Class | 📱Interface | 🔢 Enum | ❗ Exception | 📍 Annotation | 📝 Record | +|----------|----------|-------------|---------|-------------|---------------|-----------| +| Kí hiệu | | % | # | ! | @ | $ | + +**VD:** `ThisIsClass`, `%Interface`, `!StackoverflowException`, `#EntityType` + +- Đối với method: + +```java +/** + * Hàm này thực hiện chức năng gì... . + * + * @params Input1 Đầu vào 1 + * @params Input2 Đầu vào 2 + * @return Kết quả của hàm + * @throws Exception nếu có lỗi xảy ra... + */ + +``` + +### ❗Chú ý + +- Formating docs sau khi viết code xong. (Phím tắt thường là `Alt` + `Shift` + `F`) + +### Bạn có thể xem source code trong dự án để xem ví dụ. \ No newline at end of file diff --git a/src/main/java/com/github/codestorm/bounceverse/Utils.java b/src/main/java/com/github/codestorm/bounceverse/Utils.java new file mode 100644 index 0000000..93f75f8 --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/Utils.java @@ -0,0 +1,279 @@ +package com.github.codestorm.bounceverse; + +import com.almasb.fxgl.dsl.FXGL; +import com.almasb.fxgl.entity.Entity; +import com.almasb.fxgl.time.TimerAction; +import com.github.codestorm.bounceverse.data.types.DirectionUnit; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import javafx.geometry.Rectangle2D; +import javafx.scene.shape.Circle; +import javafx.util.Duration; + +/** Utilities. */ +public final class Utils { + private Utils() {} + + /** Input/Output utilities. */ + public static final class IO { + private IO() {} + + /** + * Load .properties file. + * + * @param path Relative path + * @return Parsed properties + * @throws IOException if an error occurred when reading from the input stream. + */ + public static Properties loadProperties(String path) throws IOException { + InputStream fileStream = IO.class.getResourceAsStream(path); + if (fileStream == null) { + throw new IOException("Cannot open InputStream on " + path); + } + + Properties prop = new Properties(); + prop.load(fileStream); + fileStream.close(); + return prop; + } + + /** + * Convert an array of key=value pairs into a hashmap. The string "key=" maps key onto "", + * while just "key" maps key onto null. The value may contain '=' characters, only the first + * "=" is a delimiter.
+ * Source code from here. + * + * @param args command-line arguments in the key=value format (or just key= or key) + * @param defaults a map of default values, may be null. Mappings to null are not copied to + * the resulting map. + * @param whiteList if not null, the keys not present in this map cause an exception (and + * keys mapped to null are ok) + * @return a map that maps these keys onto the corresponding values. + */ + public static HashMap parseArgs( + String[] args, + HashMap defaults, + HashMap whiteList) { + // HashMap allows null values + HashMap res = new HashMap<>(); + if (defaults != null) { + for (Map.Entry e : defaults.entrySet()) { + if (e.getValue() != null) { + res.put(e.getKey(), e.getValue()); + } + } + } + for (String s : args) { + String[] kv = s.split("=", 2); + if (whiteList != null && !whiteList.containsKey(kv[0])) { + continue; + } + res.put(kv[0], kv.length < 2 ? null : kv[1]); + } + return res; + } + + /** + * Read text file (txt) and put all lines into {@link List}. + * + * @param path File path + * @return All lines in text file + */ + public static List readTextFile(String path) { + var res = new ArrayList(); + var scanner = new Scanner(path); + while (scanner.hasNext()) { + res.add(scanner.next()); + } + scanner.close(); + return res; + } + } + + public static final class Time { + /** + * Thời gian hồi để thực hiện lại gì đó. Thực hiện thông qua {@link #current} + * + * @see ActiveCooldown + */ + public static final class Cooldown { + private final ActiveCooldown current = new ActiveCooldown(); + private Duration duration = Duration.INDEFINITE; + + public Duration getDuration() { + return duration; + } + + /** + * Đặt thời lượng cooldown mới.
+ * Lưu ý: Chỉ áp dụng cho cooldown mới. + * + * @param duration Thời lượng mới + */ + public void setDuration(Duration duration) { + this.duration = duration; + } + + public ActiveCooldown getCurrent() { + return current; + } + + public Cooldown() {} + + public Cooldown(Duration duration) { + this.duration = duration; + } + + /** Cooldown thời điểm hiện tại. Giống như một wrapper của {@link TimerAction}. */ + public final class ActiveCooldown { + private TimerAction waiter = null; + private double timestamp = Double.NaN; + private Runnable onExpiredCallback = null; + + /** Hành động khi cooldown hết. */ + private void onExpired() { + timestamp = Double.NaN; + if (onExpiredCallback != null) { + onExpiredCallback.run(); + } + } + + /** + * Callback thực thi khi cooldown hết hạn. + * + * @param callback Callback sẽ thực thi + */ + public void setOnExpired(Runnable callback) { + this.onExpiredCallback = callback; + } + + /** + * Kiểm tra Cooldown hiện tại hết hạn chưa. + * + * @return {@code true} nếu hết hạn, ngược lại {@code false}. + */ + public boolean expired() { + return (waiter == null) || waiter.isExpired(); + } + + /** Khiến cooldown hết hạn ngay (nếu có). */ + public void expire() { + if (!expired()) { + waiter.expire(); + } + } + + /** Set một cooldown mới. */ + public void makeNew() { + expire(); + + final var gameTimer = FXGL.getGameTimer(); + waiter = gameTimer.runOnceAfter(this::onExpired, duration); + timestamp = gameTimer.getNow(); + } + + /** Tạm dừng cooldown. */ + public void pause() { + if (!expired()) { + waiter.pause(); + } + } + + /** Tiếp tục cooldown. */ + public void resume() { + if (!expired()) { + waiter.resume(); + } + } + + public boolean isPaused() { + return !expired() && waiter.isPaused(); + } + + /** + * Lấy thời gian còn lại của cooldown. + * + * @return Thời gian còn lại + */ + public Duration getTimeLeft() { + if (expired()) { + return Duration.ZERO; + } + final var elapsed = Duration.millis(FXGL.getGameTimer().getNow() - timestamp); + return duration.subtract(elapsed); + } + + /** + * Giảm thời gian hồi đi một lượng thời gian. + * + * @param duration Thời lượng giảm. + */ + public void reduce(Duration duration) { + if (!expired()) { + waiter.update(duration.toMillis()); + } + } + + private ActiveCooldown() {} + } + } + } + + public static final class Geometric { + /** + * Lọc các Entity trong phạm vi Hình tròn. + * + * @param circle Hình tròn + * @return Các entity + */ + public static List getEntityInCircle(Circle circle) { + final var cx = circle.getCenterX(); + final var cy = circle.getCenterY(); + final var radius = circle.getRadius(); + + return getEntityInCircle(cx, cy, radius); + } + + /** + * Lọc các Entity trong phạm vi Hình tròn. + * + * @param cx Tâm X + * @param cy Tâm Y + * @param radius Bán kính + * @return Các entity + */ + public static List getEntityInCircle(double cx, double cy, double radius) { + final Rectangle2D outRect = + new Rectangle2D(cx - radius, cy - radius, 2 * radius, 2 * radius); + return FXGL.getGameWorld().getEntitiesInRange(outRect).stream() + .filter( + e -> { + double nearestX = + Math.max(e.getX(), Math.min(cx, e.getX() + e.getWidth())); + double nearestY = + Math.max(e.getY(), Math.min(cy, e.getY() + e.getHeight())); + double dx = cx - nearestX; + double dy = cy - nearestY; + return (dx * dx + dy * dy) <= radius * radius; + }) + .toList(); + } + } + + public static final class Collision { + public static DirectionUnit getCollisionDirection(Entity source, Entity target) { + var fromBox = source.getBoundingBoxComponent(); + var toBox = target.getBoundingBoxComponent(); + + var fCenter = fromBox.getCenterWorld(); + var tCenter = toBox.getCenterWorld(); + + var direction = tCenter.subtract(fCenter); + + return Math.abs(direction.getX()) > Math.abs(direction.getY()) + ? direction.getX() > 0 ? DirectionUnit.RIGHT : DirectionUnit.LEFT + : direction.getY() > 0 ? DirectionUnit.DOWN : DirectionUnit.UP; + } + } +} diff --git a/src/main/java/com/github/codestorm/bounceverse/data/contracts/CanExecute.java b/src/main/java/com/github/codestorm/bounceverse/data/contracts/CanExecute.java new file mode 100644 index 0000000..3951ea1 --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/data/contracts/CanExecute.java @@ -0,0 +1,19 @@ +package com.github.codestorm.bounceverse.data.contracts; + +import java.util.List; + +/** + * + * + *

{@link CanExecute}

+ * + * Có thể thực thi hành động nào đó. + */ +public interface CanExecute { + /** + * Thực thi hành động. + * + * @param data Dữ liệu truyền vào + */ + void execute(List data); +} diff --git a/src/main/java/com/github/codestorm/bounceverse/data/contracts/CanUndo.java b/src/main/java/com/github/codestorm/bounceverse/data/contracts/CanUndo.java new file mode 100644 index 0000000..5d27151 --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/data/contracts/CanUndo.java @@ -0,0 +1,17 @@ +package com.github.codestorm.bounceverse.data.contracts; + +import com.github.codestorm.bounceverse.components.behaviors.UndoableBehavior; + +/** + * + * + *

{@link CanUndo}

+ * + * Có thể hoàn tác trạng thái về lúc trước khi thực thi. Chỉ hỗ trợ thực thi 1 lần. + * + * @see UndoableBehavior + */ +public interface CanUndo extends CanExecute { + /** Hoàn tác trạng thái về trước đó. */ + void undo(); +} diff --git a/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/ForEntity.java b/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/ForEntity.java new file mode 100644 index 0000000..2cf8f40 --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/ForEntity.java @@ -0,0 +1,24 @@ +package com.github.codestorm.bounceverse.data.meta.entities; + +import com.github.codestorm.bounceverse.data.types.EntityType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * + *

@{@link ForEntity}

+ * + * Đánh dấu class chỉ định là phù hợp cho entity nào.
+ * Nếu chỉ định tất cả entity, hãy truyền vào mảng rỗng {@code {}}. + * + * @see SuitableEntity + * @see EntityType + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface ForEntity { + EntityType[] value(); +} diff --git a/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/SuitableEntity.java b/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/SuitableEntity.java new file mode 100644 index 0000000..6c5c7ed --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/SuitableEntity.java @@ -0,0 +1,23 @@ +package com.github.codestorm.bounceverse.data.meta.entities; + +import com.github.codestorm.bounceverse.data.types.EntityType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * + *

@{@link SuitableEntity}

+ * + * Đánh dấu sự yêu cầu tham số truyền vào phải phù hợp với entity được chỉ định. + * + * @see ForEntity + * @see EntityType + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.PARAMETER, ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) +public @interface SuitableEntity { + EntityType[] value(); +} diff --git a/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/SuitableEntityProcessor.java b/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/SuitableEntityProcessor.java new file mode 100644 index 0000000..8991aa6 --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/data/meta/entities/SuitableEntityProcessor.java @@ -0,0 +1,71 @@ +package com.github.codestorm.bounceverse.data.meta.entities; + +import com.google.auto.service.AutoService; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.*; +import javax.tools.Diagnostic; + +/** + * + * + *

{@link SuitableEntityProcessor}

+ * + * {@link Processor} kiểm tra cho annotation {@link SuitableEntity}. + */ +@AutoService(Processor.class) +public final class SuitableEntityProcessor extends AbstractProcessor { + @Override + public Set getSupportedAnnotationTypes() { + return Set.of(SuitableEntity.class.getCanonicalName()); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + for (Element element : roundEnv.getElementsAnnotatedWith(SuitableEntity.class)) { + if (element.getKind() == ElementKind.PARAMETER) { + // Phía yêu cầu (parameter của @SuitableEntity) + final var requireParameter = (VariableElement) element; + final var requireAnnotation = requireParameter.getAnnotation(SuitableEntity.class); + final var requiredTypes = EnumSet.copyOf(Arrays.asList(requireAnnotation.value())); + + // Phía thực tế (parameter truyền vào trong hàm) + final var actualClassElement = + processingEnv.getTypeUtils().asElement(requireParameter.asType()); + if (actualClassElement == null) { + continue; + } + final var actualAnnotation = actualClassElement.getAnnotation(ForEntity.class); + + // Kiểm tra (bao hàm - loại trừ) + if (actualAnnotation != null) { + final var actualTypes = EnumSet.copyOf(Arrays.asList(actualAnnotation.value())); + if (actualTypes.isEmpty()) { + continue; // bao phủ cả (quy ước là mảng rỗng) + } + requiredTypes.removeAll(actualTypes); + if (requiredTypes.isEmpty()) { + continue; // đáp ứng hết + } + } + + for (var requiredType : requiredTypes) { + final var message = + String.format( + "Parameter '%s' must suitable for '%s', but '%s' does not.", + requireParameter.getSimpleName(), + requiredType.name(), + actualClassElement.getSimpleName()); + processingEnv + .getMessager() + .printMessage(Diagnostic.Kind.ERROR, message, requireParameter); + } + } + } + return true; + } +} diff --git a/src/main/java/com/github/codestorm/bounceverse/data/types/AnchorPoint.java b/src/main/java/com/github/codestorm/bounceverse/data/types/AnchorPoint.java new file mode 100644 index 0000000..8fa32d2 --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/data/types/AnchorPoint.java @@ -0,0 +1,52 @@ +package com.github.codestorm.bounceverse.data.types; + +import javafx.geometry.Point2D; +import javafx.geometry.Rectangle2D; +import javafx.scene.shape.Rectangle; + +/** + * + * + *

{@link AnchorPoint}

+ * + * Các cạnh neo phổ biến trên {@link Rectangle2D}. + */ +public enum AnchorPoint { + TOP_LEFT(0, 0), + TOP_CENTER(0.5, 0), + TOP_RIGHT(1, 0), + BOTTOM_LEFT(0, 1), + BOTTOM_CENTER(0.5, 1), + BOTTOM_RIGHT(1, 1), + CENTER_LEFT(0, 0.5), + CENTER(0.5, 0.5), + CENTER_RIGHT(1, 0.5); + + private final Point2D point; + + AnchorPoint(double nx, double ny) { + this.point = new Point2D(nx, ny); + } + + /** + * Lấy {@link AnchorPoint} trên một {@link Rectangle2D} + * + * @param rect Hình chữ nhật 2D + * @return Vị trí tương ứng trên hình + */ + public Point2D of(Rectangle2D rect) { + final var movement = + new Point2D(rect.getWidth() * point.getX(), rect.getHeight() * point.getY()); + return new Point2D(rect.getMinX(), rect.getMinY()).add(movement); + } + + /** + * Lấy {@link AnchorPoint} trên một {@link Rectangle} + * + * @param rect Hình chữ nhật + * @return Vị trí tương ứng trên hình + */ + public Point2D of(Rectangle rect) { + return of(new Rectangle2D(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight())); + } +} diff --git a/src/main/java/com/github/codestorm/bounceverse/data/types/DirectionUnit.java b/src/main/java/com/github/codestorm/bounceverse/data/types/DirectionUnit.java new file mode 100644 index 0000000..c73a55f --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/data/types/DirectionUnit.java @@ -0,0 +1,35 @@ +package com.github.codestorm.bounceverse.data.types; + +import com.almasb.fxgl.core.math.Vec2; + +/** + * + * + *

{@link DirectionUnit}

+ * + * Vector đơn vị đại diện cho hướng di chuyển. + * + * @see Vec2 + */ +public enum DirectionUnit { + LEFT(-1, 0), + RIGHT(1, 0), + UP(0, -1), + DOWN(0, 1), + STAND(0, 0); + + private final Vec2 vector; + + DirectionUnit(double vx, double vy) { + this.vector = new Vec2(vx, vy).normalize(); + } + + /** + * Lấy vector {@link Vec2} tương ứng. + * + * @return Vector + */ + public Vec2 getVector() { + return vector; + } +} diff --git a/src/main/java/com/github/codestorm/bounceverse/data/types/EntityType.java b/src/main/java/com/github/codestorm/bounceverse/data/types/EntityType.java new file mode 100644 index 0000000..7a8909b --- /dev/null +++ b/src/main/java/com/github/codestorm/bounceverse/data/types/EntityType.java @@ -0,0 +1,25 @@ +package com.github.codestorm.bounceverse.data.types; + +import com.almasb.fxgl.dsl.EntityBuilder; +import com.almasb.fxgl.entity.Entity; + +/** + * + * + *

{@link EntityType}

+ * + * Loại của {@link Entity}, dùng để phân biệt giữa các entity có loại khác nhau.
+ * Sử dụng {@link EntityBuilder#type(Enum)} để gán cho entity và {@link Entity#getType()} để truy + * xuất, hoặc {@link Entity#isType(Object)} để kiểm tra. + * + * @see EntityBuilder + * @see Entity + */ +public enum EntityType { + BRICK, + PADDLE, + BALL, + POWER_UP, + BULLET, + WALL +}