diff --git a/.github/workflows/buildRelease.yml b/.github/workflows/buildRelease.yml
index 004bdf2..daa5083 100644
--- a/.github/workflows/buildRelease.yml
+++ b/.github/workflows/buildRelease.yml
@@ -21,7 +21,7 @@ jobs:
- name: Build release
run: |
chmod +x gradlew
- ./gradlew clean buildRelease
+ ./gradlew clean release
- name: Upload to Release assets
uses: softprops/action-gh-release@v2
diff --git a/.gitignore b/.gitignore
index 480ccad..6ea09bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,6 @@ target/
### IntelliJ IDEA ###
.idea/
-!.idea/misc.xml
*.iws
*.iml
*.ipr
@@ -53,4 +52,5 @@ bin/
**/cache/
**/prefs/
-### Workspace
\ No newline at end of file
+### Workspace
+/settings.*.toml
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 4901ee9..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.run/build.run.xml b/.run/build.run.xml
new file mode 100644
index 0000000..8ff4ec3
--- /dev/null
+++ b/.run/build.run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+ false
+
+
+
\ No newline at end of file
diff --git a/.run/bytecode.run.xml b/.run/bytecode.run.xml
deleted file mode 100644
index e40b2cb..0000000
--- a/.run/bytecode.run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/release.run.xml b/.run/release.run.xml
index 532ab9c..6f7638a 100644
--- a/.run/release.run.xml
+++ b/.run/release.run.xml
@@ -1,24 +1,24 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- true
- false
- false
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+ false
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c9af3fa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,121 @@
+# Bounceverse - A Modern Arkanoid Clone
+### Báo cáo Bài tập lớn OOP
+
+**Bounceverse** là một bản làm lại hiện đại của tựa game brick-breaker kinh điển, được xây dựng hoàn toàn bằng **Java** và game engine **FXGL**. Dự án này tập trung vào việc áp dụng kiến trúc **Entity-Component-System (ECS)** tiên tiến, được hỗ trợ bởi các nguyên tắc Lập trình Hướng đối tượng, để tạo ra một cấu trúc game linh hoạt, dễ bảo trì và mở rộng.
+
+Game không chỉ tái hiện lối chơi gốc mà còn bổ sung nhiều tính năng nâng cao như hệ thống sinh màn chơi ngẫu nhiên, hệ thống vật phẩm (power-up) đa dạng, kỹ năng chủ động cho Paddle và các hiệu ứng "game feel" hiện đại để mang lại trải nghiệm hấp dẫn.
+
+---
+
+
+
+---
+
+## ✨ Tính năng nổi bật
+
+Dự án Bounceverse có các tính năng nâng cao như sau:
+
+* **Sinh màn chơi ngẫu nhiên:** Mỗi lần chơi là một trải nghiệm mới với thuật toán sinh map dựa trên `FastNoiseLite`, tạo ra các layout gạch độc đáo (`GameSystem.java`).
+
+* **Đa dạng các loại gạch:**
+ * **Gạch Thường (Normal Brick):** Gạch 1 HP.
+ * **Gạch Cứng (Strong Brick):** Gạch nhiều HP, có hiệu ứng nứt vỡ trực quan (`StrongBrickTextureUpdater`) khi nhận sát thương.
+ * **Gạch Khiên (Shield Brick):** Chỉ có thể bị phá từ một hướng nhất định.
+ * **Gạch Nổ (Exploding Brick):** Khi bị phá hủy sẽ tạo ra một vụ nổ (`Explosion`), gây sát thương cho các gạch xung quanh.
+ * **Gạch Chìa khóa (Key Brick):** Luôn luôn rơi ra vật phẩm (`Power-Up`) khi bị phá hủy.
+
+* **Hệ thống Vật phẩm (Power-Ups) phong phú:**
+ * `GUN`: Trang bị súng cho Paddle, tự động bắn đạn lên trên để phá gạch.
+ * `EXPAND_PADDLE`: Tăng kích thước thanh đỡ tạm thời.
+ * `SHRINK_PADDLE`: Thu nhỏ kích thước thanh đỡ tạm thời (vật phẩm bất lợi).
+ * `REVERSE_PADDLE`: Đảo ngược điều khiển của thanh đỡ (vật phẩm bất lợi).
+ * `MULTIPLE_BALL`: Nhân đôi tất cả bóng đang có trên màn hình.
+ * `FAST_BALL` / `SLOW_BALL`: Tăng hoặc giảm tốc độ của bóng.
+ * `SHIELD`: Tạo một tấm khiên ở đáy màn hình, ngăn bóng không bị rơi ra ngoài trong một thời gian.
+
+* **Hệ thống UI/Menu hoàn chỉnh và hiện đại:**
+ * Menu chính và menu tạm dừng được thiết kế riêng (`scenes/Menu.java`) với hiệu ứng hạt (particle effects) và hoạt ảnh mượt mà.
+ * Màn hình Game Over (`DeathSubscene`) hiển thị điểm số và cho phép nhập tên nếu lọt vào top 10.
+ * Giao diện trong game (HUD) hiển thị điểm số và mạng sống (`Hearts`, `HorizontalPositiveInteger`).
+
+* **Hệ thống Điểm cao (Leaderboard):**
+ * Sử dụng `LeaderboardManager` để tự động **lưu và tải 10 điểm số cao nhất** vào file `leaderboard.dat`.
+
+* **Hiệu ứng & "Game Feel" nâng cao:**
+ * **Nền động:** Màu nền của game thay đổi mượt mà (`BackgroundColorManager`) dựa trên tiến độ phá gạch của người chơi.
+ * **Giao diện Synthwave:** Toàn bộ giao diện game được thiết kế với phong cách neon-synthwave độc đáo (`UISystem.java`).
+ * **Âm thanh:** Quản lý âm thanh đầy đủ cho các hành động trong game.
+
+---
+
+## 🎮 Hướng dẫn chơi
+
+* **Mục tiêu:** Phá vỡ tất cả gạch để đạt điểm cao nhất.
+* **Điều khiển:**
+ * `Mũi tên Trái/Phải`: Di chuyển thanh đỡ (Paddle).
+ * `SPACE`: Phóng bóng khi bóng đang dính vào thanh đỡ (đầu màn/sau khi mất mạng).
+ * `ESC`: Tạm dừng game.
+ * `S`: Kích hoạt kỹ năng chủ động của Paddle.
+---
+
+## 🏗 Cấu trúc dự án (Thiết kế ECS & OOP)
+
+Dự án được xây dựng trên nền tảng game engine **FXGL**, tuân thủ chặt chẽ kiến trúc **Entity-Component-System (ECS)**, kết hợp với các nguyên tắc OOP (Đóng gói, Kế thừa, Đa hình) và các Mẫu thiết kế phần mềm (Design Patterns).
+
+### 1. Gói `systems` (Hệ thống & Vòng đời game)
+* **Bộ não của game:** Thay vì một lớp `GameManager` (God Class) khổng lồ, logic cốt lõi được chia nhỏ thành các hệ thống độc lập, kế thừa từ `InitialSystem`.
+* `Bounceverse.java`: Lớp chính điều phối các vòng đời của FXGL (`initGame`, `initPhysics`, `initUI`...).
+* `GameSystem`: Khởi tạo các thực thể ban đầu (gạch, paddle, bóng).
+* `PhysicSystem`: Định nghĩa toàn bộ logic va chạm trong game.
+* `InputSystem`: Quản lý toàn bộ input từ người chơi.
+* **Mẫu thiết kế Singleton:** Các lớp System và Manager (`LeaderboardManager`, `UserSettingsManager`) được triển khai theo mẫu Singleton (sử dụng inner class `Holder`) để đảm bảo chỉ có một thực thể duy nhất và có thể truy cập toàn cục.
+
+### 2. Gói `factory.entities` (Sản xuất Thực thể)
+* **Mẫu thiết kế Factory:** Logic tạo ra các đối tượng trong game (`Entity`) được tách biệt hoàn toàn khỏi logic chính.
+* Mỗi lớp (`BallFactory`, `BrickFactory`, `PaddleFactory`...) chịu trách nhiệm xây dựng một loại `Entity` cụ thể, đóng gói các components và thuộc tính cần thiết cho thực thể đó.
+
+### 3. Gói `components` (Hành vi & Thuộc tính)
+* **Trái tim của kiến trúc ECS:** Logic và dữ liệu của một `Entity` được định nghĩa bởi các `Component` mà nó chứa.
+* **`Behavior` (Hành vi):** Các lớp kế thừa từ `Behavior` định nghĩa những gì một `Entity` có thể **làm**.
+ * `Attack.java`: Hành vi gây sát thương.
+ * `HealthDeath.java`: Hành vi "chết" khi hết máu.
+ * `PaddleShooting.java`: Hành vi bắn đạn của Paddle.
+ * `Explosion.java`: Hành vi nổ của gạch.
+* **`Property` (Thuộc tính):** Các lớp kế thừa từ `Property` định nghĩa những **dữ liệu** mà một `Entity` có.
+ * `Attributes.java`: Chứa các chỉ số như phòng thủ.
+ * `Shield.java`: Chứa thông tin về các mặt được bảo vệ của gạch khiên.
+
+### 4. Gói `scenes` & `ui` (Giao diện & Trải nghiệm)
+* Chịu trách nhiệm về mọi thứ người chơi nhìn thấy.
+* `Menu.java`: Một lớp tùy chỉnh hoàn toàn, kế thừa `FXGLMenu`, để tạo ra menu chính và menu tạm dừng với phong cách riêng.
+* `ViewElement`: Lớp cơ sở cho các thành phần UI độc lập như `Hearts` (hiển thị mạng) và `HorizontalPositiveInteger` (hiển thị điểm số).
+
+---
+
+### Yêu cầu:
+* Java JDK 24 (hoặc cao hơn).
+* IDE.
+* Gradle được tích hợp sẵn trong dự án.
+
+## 🛠 Công nghệ sử dụng
+
+* **Ngôn ngữ:** Java (Eclipse Temurin 24)
+* **Framework:** **FXGL (FX Game Library)** - một game engine xây dựng trên nền tảng JavaFX.
+* **Công cụ Build:** Gradle.
+* **IDE:** IntelliJ IDEA / Visual Studio Code.
+
+---
+
+## 👨💻 Thành viên nhóm
+| Leader Mai Hải Thành | 24021627 | [@thnhmai06](https://github.com/thnhmai06) |
+| --------------------- | -------- | ------------------------------------------------ |
+| Trần Mạnh Tân | 24021619 | [@ManhTanTran](https://github.com/ManhTanTran) |
+| Nguyễn Huỳnh Anh Tuấn | 24021659 | [@huynhtuan372](https://github.com/huynhtuan372) |
+
+Phân chia công việc:
+
+| @thnhmai06 | @ManhTanTran | @huynhtuan372 |
+| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
+| Đồng đảm nhiệm Power-up, thiết lập và quản lý các lớp cơ sở, quản lý chung project và hệ thống level. | Đảm nhiệm hệ thống vật lý, va chạm, Brick, Paddle, đồng đảm nhiệm và dẫn xuất Power-up. | Thiết kế UI và assets, các menu, leaderboard, xử lý input, hoàn thiện Ball. |
+
+Ball được thiết lập và phát triển ban đầu bởi minngoc123.
diff --git a/build.gradle b/build.gradle
index d41bd96..740877d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -23,6 +23,7 @@ dependencies {
implementation 'com.github.almasb:fxgl:21.1'
implementation 'com.google.guava:guava:33.5.0-jre'
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
+ implementation 'com.google.guava:guava:33.5.0-jre'
implementation 'com.google.auto.service:auto-service-annotations:1.1.1'
annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
}
@@ -30,13 +31,16 @@ dependencies {
application {
mainClass = 'com.github.codestorm.bounceverse.Bounceverse'
applicationDefaultJvmArgs = [
- "--enable-native-access=javafx.graphics"
+ "--enable-native-access=javafx.graphics",
+ "--enable-native-access=javafx.media",
+ '-Dprism.forceGPU=true',
+ '-Dmedia.gpu.decoder=true'
]
}
defaultTasks('run')
-tasks.register('buildRelease', Jar) {
+tasks.register('release', Jar) {
group = 'build'
description = "Test this project and assemble a release JAR file."
@@ -63,16 +67,24 @@ javafx {
}
spotless {
- enforceCheck = false
- format 'misc', {
- target '*.gradle', '.gitattributes', '.gitignore'
+ enforceCheck = false
- trimTrailingWhitespace()
- leadingSpacesToTabs()
- endWithNewline()
- }
- java {
- googleJavaFormat().aosp().reflowLongStrings()
- formatAnnotations()
- }
+ format 'misc', {
+ target '*.gradle', '.gitattributes', '.gitignore'
+ trimTrailingWhitespace()
+ endWithNewline()
+ }
+
+ java {
+ target 'src/**/*.java'
+ targetExclude 'src/**/libs/**/*';
+
+ googleJavaFormat('1.17.0')
+ .aosp()
+ .reorderImports(true)
+ .reflowLongStrings(true)
+ .formatJavadoc(true)
+ formatAnnotations()
+ removeUnusedImports()
+ }
}
diff --git a/gradle.properties b/gradle.properties
index 45cac2a..8b36c8a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,4 @@
#
name=Bounceverse
group=com.github.codestorm
-version=1.0.0-dev
\ No newline at end of file
+version=1.1.0-dev
\ No newline at end of file
diff --git a/src/main/java/com/github/codestorm/bounceverse/AssetsPath.java b/src/main/java/com/github/codestorm/bounceverse/AssetsPath.java
new file mode 100644
index 0000000..d6d4e8d
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/AssetsPath.java
@@ -0,0 +1,221 @@
+package com.github.codestorm.bounceverse;
+
+import com.github.codestorm.bounceverse.typing.enums.BrickType;
+import com.google.common.collect.ImmutableList;
+
+import javafx.scene.paint.Color;
+
+import org.jspecify.annotations.NonNull;
+
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.TreeMap;
+
+/**
+ *
+ *
+ *
{@link AssetsPath}
+ *
+ * Nơi lưu trữ các đường dẫn tới assets (texture, âm thanh, video, v.v.).
+ */
+public final class AssetsPath {
+
+ private AssetsPath() {}
+
+ private static final String ROOT = "/assets";
+
+ public static final class Video {
+ private Video() {}
+
+ private static final String ROOT = AssetsPath.ROOT + "/videos";
+ public static final String INTRO = "/intro.mp4";
+ }
+
+ public static final class Sounds {
+ private Sounds() {}
+
+ private static final String ROOT = AssetsPath.ROOT + "/sounds";
+
+ public static final class Music {
+ private Music() {}
+
+ private static final String ROOT = Sounds.ROOT + "/music";
+
+ public static final String IN_GAME = ROOT + "/in_game.wav";
+ public static final String MENU = ROOT + "/menu.ogg";
+ }
+
+ public static final class SFX {
+ private SFX() {}
+
+ private static final String ROOT = Sounds.ROOT + "/sfx";
+
+ public static final String BLIP = ROOT + "/blip.wav";
+ public static final String BRICK_HIT_1 = ROOT + "/brick-hit-1.wav";
+ public static final String BRICK_HIT_2 = ROOT + "/brick-hit-2.wav";
+ public static final String BUZZ = ROOT + "/buzz.wav";
+ public static final String CLICK_TINY = ROOT + "/click_tiny.wav";
+ public static final String CONFIRM = ROOT + "/confirm.wav";
+ public static final String ELECTRIC = ROOT + "/electric.wav";
+ public static final String HIGH_SCORE = ROOT + "/high_score.wav";
+ public static final String HURT = ROOT + "/hurt.wav";
+ public static final String KEY_OPEN = ROOT + "/key_open.wav";
+ public static final String LOSE = ROOT + "/lose.wav";
+ public static final String NO_SELECT = ROOT + "/no-select.wav";
+ public static final String PADDLE_HIT = ROOT + "/paddle_hit.wav";
+ public static final String PAUSE = ROOT + "/pause.wav";
+ public static final String POWER_UP = ROOT + "/power_up.wav";
+ public static final String RECOVER = ROOT + "/recover.wav";
+ public static final String SCORE = ROOT + "/score.wav";
+ public static final String SELECT = ROOT + "/select.wav";
+ public static final String SHRINK = ROOT + "/shrink.wav";
+ public static final String SWITCH2 = ROOT + "/switch2.wav";
+ public static final String VICTORY = ROOT + "/victory.wav";
+ public static final String WALL_HIT = ROOT + "/wall_hit.wav";
+ public static final String WIN3 = ROOT + "/win3.wav";
+
+ public static final ImmutableList<@NonNull String> BRICK_HITS =
+ ImmutableList.of(BRICK_HIT_1, BRICK_HIT_2);
+ }
+ }
+
+ public static final class Textures {
+ private Textures() {}
+
+ public static final class Bricks {
+
+ private Bricks() {}
+
+ private static final String ROOT = "bricks";
+
+ public static final Map COLORS;
+
+ static {
+ COLORS =
+ Map.of(
+ "blue", new ColorAssets(Color.BLUE),
+ "green", new ColorAssets(Color.GREEN),
+ "orange", new ColorAssets(Color.ORANGE),
+ "pink", new ColorAssets(Color.PINK),
+ "red", new ColorAssets(Color.RED),
+ "yellow", new ColorAssets(Color.YELLOW));
+ }
+
+ public static final class ColorAssets {
+
+ private static final NavigableMap NORMAL = new TreeMap<>();
+ private static final NavigableMap SHIELD = new TreeMap<>();
+ private static final NavigableMap STRONG = new TreeMap<>();
+ private static final NavigableMap KEY = new TreeMap<>();
+ private static final NavigableMap EXPLODING = new TreeMap<>();
+
+ private final Color color;
+
+ public Color getColor() {
+ return color;
+ }
+
+ private ColorAssets(Color color) {
+ this.color = color;
+ }
+
+ static {
+ // Normal
+ NORMAL.put(0.0, "/normal.png");
+
+ // Shield
+ SHIELD.put(0.0, "/shield.png");
+
+ // Strong
+ STRONG.put(1.0, "/strong.png");
+ STRONG.put(2.0 / 3, "/strongFirstHit.png");
+ STRONG.put(1.0 / 3, "/strongSecondHit.png");
+ STRONG.put(0.0, "/strongThirdHit.png");
+
+ // Key Brick
+ KEY.put(0.0, "/keybrick.png");
+
+ // Exploding
+ EXPLODING.put(0.0, "/explode.png");
+ }
+
+ public String getColorName() {
+ if (color.equals(Color.BLUE)) return "blue";
+ if (color.equals(Color.GREEN)) return "green";
+ if (color.equals(Color.ORANGE)) return "orange";
+ if (color.equals(Color.PINK)) return "pink";
+ if (color.equals(Color.RED)) return "red";
+ if (color.equals(Color.YELLOW)) return "yellow";
+ throw new IllegalArgumentException("Unsupported color: " + color);
+ }
+
+ public String getRoot() {
+ return ROOT + "/" + getColorName();
+ }
+
+ /**
+ * Lấy texture path dựa trên BrickType và HP percent.
+ *
+ * @param brickType Loại brick
+ * @param hpPercent HP phần trăm (0.0 - 1.0)
+ * @return Đường dẫn đến texture
+ */
+ public String getTexture(BrickType brickType, double hpPercent) {
+ var map =
+ switch (brickType) {
+ case NORMAL -> NORMAL;
+ case SHIELD -> SHIELD;
+ case STRONG -> STRONG;
+ case KEY -> KEY;
+ case EXPLODING -> EXPLODING; // fallback dùng texture thường
+ };
+ return getRoot() + map.floorEntry(hpPercent).getValue();
+ }
+
+ public Color color() {
+ return color;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (obj == null || obj.getClass() != this.getClass()) return false;
+ var that = (ColorAssets) obj;
+ return Objects.equals(this.color, that.color);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(color);
+ }
+
+ @Override
+ public String toString() {
+ return "ColorAssets[color=" + color + ']';
+ }
+ }
+ }
+
+ public static final Map NUMBERS =
+ Map.of(
+ 0, "numbers/0.png",
+ 1, "numbers/1.png",
+ 2, "numbers/2.png",
+ 3, "numbers/3.png",
+ 4, "numbers/4.png",
+ 5, "numbers/5.png",
+ 6, "numbers/6.png",
+ 7, "numbers/7.png",
+ 8, "numbers/8.png",
+ 9, "numbers/9.png");
+
+ public static final String HEART = "/heart.png";
+ }
+
+ public static final class Other {
+ private Other() {}
+
+ public static final String CREDITS = AssetsPath.ROOT + "/credits.txt";
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/Bounceverse.java b/src/main/java/com/github/codestorm/bounceverse/Bounceverse.java
index 802f063..e8b9080 100644
--- a/src/main/java/com/github/codestorm/bounceverse/Bounceverse.java
+++ b/src/main/java/com/github/codestorm/bounceverse/Bounceverse.java
@@ -1,45 +1,68 @@
package com.github.codestorm.bounceverse;
import com.almasb.fxgl.app.GameApplication;
-import com.github.codestorm.bounceverse.core.*;
-import com.github.codestorm.bounceverse.core.systems.*;
+import com.almasb.fxgl.app.GameSettings;
+import com.almasb.fxgl.dsl.FXGL;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+import com.github.codestorm.bounceverse.systems.init.AppEventSystem;
+import com.github.codestorm.bounceverse.systems.init.GameSystem;
+import com.github.codestorm.bounceverse.systems.init.InputSystem;
+import com.github.codestorm.bounceverse.systems.init.PhysicSystem;
+import com.github.codestorm.bounceverse.systems.init.UISystem;
+import com.github.codestorm.bounceverse.systems.manager.settings.GameSettingsManager;
+import com.github.codestorm.bounceverse.systems.manager.settings.LaunchOptionsManager;
+import com.github.codestorm.bounceverse.systems.manager.settings.UserSettingsManager;
import com.github.codestorm.bounceverse.typing.exceptions.BounceverseException;
+
+import javafx.util.Duration;
+
import java.io.IOException;
+import java.util.Map;
/**
*
*
* {@link Bounceverse}
*
- * Phần Hệ thống Chương trình chính của game, nơi mà mọi thứ bắt đầu từ {@link #main(String[])}...
- *
- * Game {@link Bounceverse} được lấy cảm hứng từ game Arkanoid nổi tiếng, nơi người chơi điều
- * khiển một thanh để đỡ bóng và phá vỡ các viên gạch. Mục tiêu của game là phá vỡ tất cả các viên
- * gạch và dành được điểm số cao nhất. Nhưng liệu mọi thứ chỉ đơn giản như vậy?
+ * Game chính — quản lý vòng đời khởi tạo và vòng lặp của trò chơi.
*/
public final class Bounceverse extends GameApplication {
+
public static void main(String[] args) {
- LaunchOptions.load(args);
+ LaunchOptionsManager.getInstance().load(args);
launch(args);
}
@Override
- protected void initSettings(com.almasb.fxgl.app.GameSettings settings) {
+ protected void initSettings(GameSettings settings) {
+ UserSettingsManager.getInstance().load();
try {
- SettingsManager.load(settings);
+ GameSettingsManager.load(settings);
} catch (IOException e) {
throw new BounceverseException(e);
}
}
@Override
- protected void initGame() {
- GameSystem.getInstance().apply();
+ protected void initInput() {
+ InputSystem.getInstance().apply();
}
@Override
- protected void initInput() {
- InputSystem.getInstance().apply();
+ protected void onPreInit() {
+ AppEventSystem.getInstance().apply();
+ }
+
+ @Override
+ protected void initGameVars(Map vars) {
+ GameSystem.Variables.loadDefault(FXGL.getWorldProperties());
+ }
+
+ @Override
+ protected void initGame() {
+ GameSystem.UI.getInstance().dispose();
+ UISystem.getInstance().dispose();
+ FXGL.runOnce(() -> GameSystem.getInstance().apply(), Duration.seconds(0.1));
}
@Override
@@ -49,6 +72,14 @@ protected void initPhysics() {
@Override
protected void initUI() {
+ FXGL.getGameScene().setBackgroundColor(javafx.scene.paint.Color.web("#0d0b1a"));
UISystem.getInstance().apply();
+ FXGL.getGameScene().getRoot().getStylesheets().add("assets/ui/powerup.css");
+ }
+
+ @Override
+ protected void onUpdate(double tpf) {
+ PowerUpManager.getInstance().onUpdate(tpf);
+ GameSystem.UI.getInstance().onUpdate();
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/Utilities.java b/src/main/java/com/github/codestorm/bounceverse/Utilities.java
index c4b17fc..2cb9fda 100644
--- a/src/main/java/com/github/codestorm/bounceverse/Utilities.java
+++ b/src/main/java/com/github/codestorm/bounceverse/Utilities.java
@@ -2,17 +2,16 @@
import com.almasb.fxgl.dsl.FXGL;
import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.component.Component;
-import com.almasb.fxgl.time.TimerAction;
-import com.github.codestorm.bounceverse.typing.annotations.ForEntity;
+import com.almasb.fxgl.entity.SpawnData;
+import com.github.codestorm.bounceverse.components.Component;
+import com.github.codestorm.bounceverse.typing.annotations.OnlyForEntity;
import com.github.codestorm.bounceverse.typing.enums.DirectionUnit;
import com.github.codestorm.bounceverse.typing.enums.EntityType;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.*;
+
import javafx.geometry.Rectangle2D;
-import javafx.scene.shape.Circle;
-import javafx.util.Duration;
+
+import java.io.*;
+import java.util.*;
/** Utilities. */
public final class Utilities {
@@ -20,55 +19,35 @@ private Utilities() {}
/** Input/Output utilities. */
public static final class IO {
+ // ... (Nội dung của lớp IO giữ nguyên, không cần thay đổi)
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);
+ var fileStream = IO.class.getResourceAsStream(path);
if (fileStream == null) {
throw new IOException("Cannot open InputStream on " + path);
}
- Properties prop = new Properties();
+ var 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<>();
+ var res = new HashMap();
if (defaults != null) {
- for (Map.Entry e : defaults.entrySet()) {
+ for (var e : defaults.entrySet()) {
if (e.getValue() != null) {
res.put(e.getKey(), e.getValue());
}
}
}
- for (String s : args) {
- String[] kv = s.split("=", 2);
+ for (var s : args) {
+ var kv = s.split("=", 2);
if (whiteList != null && !whiteList.containsKey(kv[0])) {
continue;
}
@@ -77,166 +56,24 @@ public static HashMap parseArgs(
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());
+ public static List readTextFile(String path) throws IOException {
+ final var res = new ArrayList();
+ final var stream = IO.class.getResourceAsStream(path);
+ if (stream == null) {
+ throw new FileNotFoundException(path);
+ }
+ final var scanner = new Scanner(stream);
+ while (scanner.hasNextLine()) {
+ res.add(scanner.nextLine());
}
scanner.close();
+ stream.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);
- }
+ private Geometric() {}
/**
* Lọc các Entity trong phạm vi Hình tròn.
@@ -247,33 +84,49 @@ public static List getEntityInCircle(Circle circle) {
* @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);
+ final var outRect = new Rectangle2D(cx - radius, cy - radius, 2 * radius, 2 * radius);
return FXGL.getGameWorld().getEntitiesInRange(outRect).stream()
.filter(
e -> {
- double nearestX =
+ var nearestX =
Math.max(e.getX(), Math.min(cx, e.getX() + e.getWidth()));
- double nearestY =
+ var nearestY =
Math.max(e.getY(), Math.min(cy, e.getY() + e.getHeight()));
- double dx = cx - nearestX;
- double dy = cy - nearestY;
+ var dx = cx - nearestX;
+ var dy = cy - nearestY;
return (dx * dx + dy * dy) <= radius * radius;
})
.toList();
}
+
+ /**
+ * Lọc các Entity trong phạm vi Hình chữ nhật.
+ *
+ * @param centerX Tâm X của hình chữ nhật
+ * @param centerY Tâm Y của hình chữ nhật
+ * @param width Chiều rộng của hình chữ nhật
+ * @param height Chiều cao của hình chữ nhật
+ * @return Danh sách các entity nằm trong khu vực đó
+ */
+ public static List getEntitiesInRectangle(
+ double centerX, double centerY, double width, double height) {
+ var topLeftX = centerX - width / 2;
+ var topLeftY = centerY - height / 2;
+ var explosionArea = new Rectangle2D(topLeftX, topLeftY, width, height);
+ return FXGL.getGameWorld().getEntitiesInRange(explosionArea);
+ }
}
+ // ... (Các lớp Collision, Compatibility, Typing giữ nguyên, không cần thay đổi)
public static final class Collision {
+ private 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;
@@ -281,72 +134,49 @@ public static DirectionUnit getCollisionDirection(Entity source, Entity target)
}
public static final class Compatibility {
- /**
- * Throw {@link IllegalArgumentException} nếu như có component trong {@code params} không
- * phù hợp với {@code onlyFor}.
- *
- * @param onlyFor {@link EntityType} muốn kiểm tra tương thích
- * @param params Các component cần kiểm tra
- */
- public static void throwIfNotCompatible(EntityType onlyFor, Component... params) {
- for (var param : params) {
- final var annotation = param.getClass().getAnnotation(ForEntity.class);
- if (annotation != null) {
- final var paramSet = EnumSet.copyOf(Arrays.asList(annotation.value()));
- if (paramSet.isEmpty() || paramSet.contains(onlyFor)) {
- continue;
- }
+ private Compatibility() {}
+
+ public static void throwIfNotCompatible(
+ EntityType entityType, com.almasb.fxgl.entity.component.Component... components) {
+ for (var param : components) {
+ final var annotation = param.getClass().getAnnotation(OnlyForEntity.class);
+ if (annotation == null) {
+ continue;
+ }
+ final var paramEntityTypeSet = EnumSet.copyOf(Arrays.asList(annotation.value()));
+ if (!paramEntityTypeSet.contains(entityType)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Class '%s' does not compatible for entity has '%s'"
+ + " entityType.",
+ param.getClass().getSimpleName(), entityType.name()));
}
- throw new IllegalArgumentException(
- String.format(
- "Class '%s' does not compatible for entity has '%s' type.",
- param.getClass().getSimpleName(), onlyFor.name()));
}
}
- /**
- * {@link #throwIfNotCompatible(EntityType, Component...)} nhưng không throw exception.
- *
- * @param onlyFor {@link EntityType} muốn kiểm tra tương thích
- * @param params Các component cần kiểm tra
- * @return {@code true} nếu tất cả tương thích, ngược lại {@code false}.
- */
- public static boolean isCompatible(EntityType onlyFor, Component... params) {
+ public static boolean isCompatible(EntityType entityType, Component... params) {
try {
- throwIfNotCompatible(onlyFor, params);
+ throwIfNotCompatible(entityType, params);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
+ }
- /**
- * Throw {@link IllegalArgumentException} nếu như có component trong {@code params} không
- * phù hợp đồng thời tất cả với {@code onlyFor}.
- *
- * @param onlyFor Các {@link EntityType} muốn kiểm tra tương thích
- * @param params Các component cần kiểm tra
- */
- public static void throwIfNotCompatible(EntityType[] onlyFor, Component... params) {
- for (var only : onlyFor) {
- throwIfNotCompatible(only, params);
+ public static final class Typing {
+ private Typing() {}
+
+ public static T getOr(SpawnData data, String key, T ifNot) {
+ if (data.hasKey(key)) {
+ return data.get(key);
}
+ return ifNot;
}
- /**
- * {@link #throwIfNotCompatible(EntityType[], Component...)} nhưng không throw exception.
- *
- * @param onlyFor Các {@link EntityType} muốn kiểm tra tương thích
- * @param params Các component cần kiểm tra
- * @return {@code true} nếu tất cả tương thích, ngược lại {@code false}.
- */
- public static boolean isCompatible(EntityType[] onlyFor, Component... params) {
- try {
- throwIfNotCompatible(onlyFor, params);
- return true;
- } catch (IllegalArgumentException e) {
- return false;
- }
+ @SafeVarargs
+ public static T[] toArray(T... varargs) {
+ return varargs;
}
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/Behavior.java b/src/main/java/com/github/codestorm/bounceverse/components/Behavior.java
new file mode 100644
index 0000000..fe7e28a
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/Behavior.java
@@ -0,0 +1,18 @@
+package com.github.codestorm.bounceverse.components;
+
+import com.almasb.fxgl.entity.Entity;
+import com.github.codestorm.bounceverse.typing.interfaces.Executable;
+
+/**
+ *
+ *
+ * {@link Behavior}
+ *
+ *
+ * Một {@link Component} biểu diễn
+ * hành vi (behavior) của {@link Entity}.
+ *
+ * @see Property
+ */
+public abstract non-sealed class Behavior extends Component implements Executable {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/Component.java b/src/main/java/com/github/codestorm/bounceverse/components/Component.java
new file mode 100644
index 0000000..c4e5256
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/Component.java
@@ -0,0 +1,23 @@
+package com.github.codestorm.bounceverse.components;
+
+import com.github.codestorm.bounceverse.Bounceverse;
+import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+/**
+ *
+ *
+ * {@link Component}
+ *
+ * Một thành phần lưu trữ trên Entity trong game {@link Bounceverse}.
+ * Giống như phiên bản class của {@link com.almasb.fxgl.entity.component.Component}.
+ *
+ * @see com.almasb.fxgl.entity.component.Component
+ */
+public abstract sealed class Component extends com.almasb.fxgl.entity.component.Component
+ permits Behavior, Property {
+ @Override
+ public void onAdded() {
+ Utilities.Compatibility.throwIfNotCompatible((EntityType) entity.getType(), this);
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/Property.java b/src/main/java/com/github/codestorm/bounceverse/components/Property.java
similarity index 56%
rename from src/main/java/com/github/codestorm/bounceverse/components/properties/Property.java
rename to src/main/java/com/github/codestorm/bounceverse/components/Property.java
index 016058e..19fa2cc 100644
--- a/src/main/java/com/github/codestorm/bounceverse/components/properties/Property.java
+++ b/src/main/java/com/github/codestorm/bounceverse/components/Property.java
@@ -1,8 +1,6 @@
-package com.github.codestorm.bounceverse.components.properties;
+package com.github.codestorm.bounceverse.components;
import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.component.Component;
-import com.github.codestorm.bounceverse.components.behaviors.Behavior;
/**
*
@@ -16,4 +14,4 @@
*
* @see Behavior
*/
-public abstract class Property extends Component {}
+public abstract non-sealed class Property extends Component {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Attachment.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Attachment.java
new file mode 100644
index 0000000..5edd808
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Attachment.java
@@ -0,0 +1,80 @@
+package com.github.codestorm.bounceverse.components.behaviors;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.component.Component;
+import com.almasb.fxgl.physics.PhysicsComponent;
+import com.github.codestorm.bounceverse.factory.entities.BallFactory;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.geometry.Point2D;
+
+/** Gắn bóng vào paddle và điều khiển khi người chơi chưa bắn ra. */
+public class Attachment extends Component {
+
+ private Entity paddle;
+ private boolean attached = true;
+ private PhysicsComponent physics;
+
+ private boolean move = false;
+ private double direction = 1;
+ private double currentOffset = 0;
+ private double lastPaddleX;
+
+ @Override
+ public void onAdded() {
+ paddle = FXGL.getGameWorld().getSingleton(EntityType.PADDLE);
+ physics = getEntity().getComponent(PhysicsComponent.class);
+ lastPaddleX = paddle.getX();
+ }
+
+ @Override
+ public void onUpdate(double tpf) {
+ if (attached && paddle != null) {
+ var paddleCenterX = paddle.getCenter().getX();
+ var paddleTopY = paddle.getY();
+
+ var deltaX = paddle.getX() - lastPaddleX;
+ lastPaddleX = paddle.getX();
+
+ if (!move && Math.abs(deltaX) > 0.5) {
+ move = true;
+ direction = Math.signum(deltaX);
+ }
+
+ if (move) {
+ double moveSpeed = 50;
+ currentOffset += direction * moveSpeed * tpf;
+ double maxOffset = 50;
+ if (Math.abs(currentOffset) > maxOffset) {
+ direction *= -1;
+ }
+ }
+
+ var x = paddleCenterX - entity.getWidth() / 2 + currentOffset + 10;
+ var y = paddleTopY - BallFactory.DEFAULT_RADIUS * 2 + 5;
+
+ entity.setPosition(x, y);
+ physics.setLinearVelocity(Point2D.ZERO);
+ }
+ }
+
+ public void releaseBall() {
+ if (!attached) return;
+
+ attached = false;
+ physics.overwritePosition(entity.getPosition());
+ physics.getBody().setAwake(true);
+
+ double speed = 350;
+ double vx = 0;
+ var vy = -speed;
+
+ physics.setLinearVelocity(new Point2D(vx, vy));
+ move = false;
+ }
+
+ public boolean isAttached() {
+ return attached;
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Attack.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Attack.java
index 10db568..c45902d 100644
--- a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Attack.java
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Attack.java
@@ -2,8 +2,9 @@
import com.almasb.fxgl.dsl.components.HealthIntComponent;
import com.almasb.fxgl.entity.Entity;
+import com.github.codestorm.bounceverse.components.Behavior;
import com.github.codestorm.bounceverse.components.properties.Attributes;
-import com.github.codestorm.bounceverse.typing.annotations.ForEntity;
+
import java.util.List;
/**
@@ -13,14 +14,13 @@
*
* Hành động tấn công/gây sát thương của {@link Entity}.
*/
-@ForEntity({})
public class Attack extends Behavior {
public static final int DEFAULT_DAMAGE = 1;
private int damage = DEFAULT_DAMAGE;
@Override
public void execute(List data) {
- List entities =
+ var entities =
data.stream().filter(obj -> obj instanceof Entity).map(e -> (Entity) e).toList();
for (var obj : entities) {
final var theirHealth = obj.getComponentOptional(HealthIntComponent.class);
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Behavior.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Behavior.java
deleted file mode 100644
index 71153ad..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Behavior.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.github.codestorm.bounceverse.components.behaviors;
-
-import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.component.Component;
-import com.github.codestorm.bounceverse.components.properties.Property;
-import com.github.codestorm.bounceverse.typing.interfaces.CanExecute;
-
-/**
- *
- *
- * {@link Behavior}
- *
- *
- * Một {@link Component} biểu diễn
- * hành vi (behavior) của {@link Entity}.
- *
- * @see Property
- */
-public abstract class Behavior extends Component implements CanExecute {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/CooldownBehavior.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/CooldownBehavior.java
deleted file mode 100644
index 0b50a16..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/CooldownBehavior.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.github.codestorm.bounceverse.components.behaviors;
-
-import com.github.codestorm.bounceverse.Utilities.Time.Cooldown;
-import com.github.codestorm.bounceverse.typing.interfaces.CanExecute;
-import java.util.List;
-import javafx.util.Duration;
-
-/**
- *
- *
- * {@link CooldownBehavior}
- *
- * {@link Behavior} sau khi thực thi sẽ mất thời gian để có thể {@link #executeLogic(List)} lại.
- */
-public abstract class CooldownBehavior extends Behavior implements CanExecute {
- private final Cooldown cooldown = new Cooldown();
-
- public Cooldown getCooldown() {
- return cooldown;
- }
-
- /**
- * Logic bên trong {@link CanExecute#execute(List)}.
- *
- * @param data Dữ liệu truyền vào
- * @return {@code true} nếu cho phép thực thi, ngược lại {@code false}
- */
- protected abstract boolean executeLogic(List data);
-
- @Override
- public final void execute(List data) {
- if (!cooldown.getCurrent().expired()) {
- return;
- }
- if (executeLogic(data)) {
- cooldown.getCurrent().makeNew();
- }
- }
-
- protected CooldownBehavior() {}
-
- protected CooldownBehavior(Duration duration) {
- cooldown.setDuration(duration);
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Explosion.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Explosion.java
index 11c89a2..3cb7ff0 100644
--- a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Explosion.java
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Explosion.java
@@ -1,54 +1,61 @@
package com.github.codestorm.bounceverse.components.behaviors;
-import com.almasb.fxgl.entity.Entity;
import com.almasb.fxgl.entity.component.Required;
import com.github.codestorm.bounceverse.Utilities;
import com.github.codestorm.bounceverse.components.properties.Attributes;
-import com.github.codestorm.bounceverse.typing.annotations.ForEntity;
+import com.github.codestorm.bounceverse.typing.annotations.OnlyForEntity;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
import java.util.List;
-/**
- *
- *
- * {@link Explosion}
- *
- *
- * Hành vi nổ của {@link Entity}, có thể gây sát thương hoặc hồi máu cho những đối tượng xung quanh.
- *
- * Yêu cầu entity có {@link Attributes} trước.
- */
+/** Hành vi nổ của Brick – gây damage cho các đối tượng xung quanh. */
@Required(Attributes.class)
-@ForEntity({})
+@OnlyForEntity({EntityType.BRICK})
public final class Explosion extends Attack {
- public static final int DEFAULT_RADIUS = 1;
- private int radius = DEFAULT_RADIUS;
+
+ private double explosionWidth;
+ private double explosionHeight;
@Override
public void execute(List data) {
- final var attributes = entity.getComponent(Attributes.class);
- final double cx = getEntity().getCenter().getX();
- final double cy = getEntity().getCenter().getY();
+ var cx = getEntity().getCenter().getX();
+ var cy = getEntity().getCenter().getY();
+
+ var nearEntities =
+ Utilities.Geometric.getEntitiesInRectangle(cx, cy, explosionWidth, explosionHeight);
+
+ var filteredEntities =
+ nearEntities.stream()
+ .filter(e -> !e.equals(getEntity()))
+ .map(e -> (Object) e)
+ .toList();
- final var nearEntities = Utilities.Geometric.getEntityInCircle(cx, cy, radius);
- super.execute(nearEntities.stream().map(e -> (Object) e).toList());
+ super.execute(filteredEntities);
}
@Override
public void onRemoved() {
- execute(null);
+ execute(List.of());
}
- public int getRadius() {
- return radius;
+ public Explosion(double width, double height) {
+ this.explosionWidth = width;
+ this.explosionHeight = height;
}
- public void setRadius(int radius) {
- this.radius = Math.abs(radius);
+ public double getExplosionWidth() {
+ return explosionWidth;
}
- public Explosion() {}
+ public void setExplosionWidth(double explosionWidth) {
+ this.explosionWidth = explosionWidth;
+ }
+
+ public double getExplosionHeight() {
+ return explosionHeight;
+ }
- public Explosion(int radius) {
- setRadius(radius);
+ public void setExplosionHeight(double explosionHeight) {
+ this.explosionHeight = explosionHeight;
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/FallingComponent.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/FallingComponent.java
new file mode 100644
index 0000000..6412b93
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/FallingComponent.java
@@ -0,0 +1,14 @@
+package com.github.codestorm.bounceverse.components.behaviors;
+
+import com.almasb.fxgl.entity.component.Component;
+
+/** Cho Power-Up rơi thẳng xuống với tốc độ cố định. */
+public final class FallingComponent extends Component {
+
+ private static final double SPEED = 150;
+
+ @Override
+ public void onUpdate(double tpf) {
+ entity.translateY(SPEED * tpf);
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/HealthDeath.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/HealthDeath.java
index e3f1e49..ea9e136 100644
--- a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/HealthDeath.java
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/HealthDeath.java
@@ -1,26 +1,63 @@
+// C:\Users\Admin\Documents\bounceverse\src\main\java\com\github\codestorm\bounceverse\components\behaviors\HealthDeath.java
package com.github.codestorm.bounceverse.components.behaviors;
+import com.almasb.fxgl.dsl.FXGL;
import com.almasb.fxgl.dsl.components.HealthIntComponent;
import com.almasb.fxgl.entity.component.Required;
-import com.github.codestorm.bounceverse.typing.annotations.ForEntity;
+import com.github.codestorm.bounceverse.components.Behavior;
+import com.github.codestorm.bounceverse.components.properties.brick.BrickTextureManager;
+import com.github.codestorm.bounceverse.systems.init.GameSystem;
+import com.github.codestorm.bounceverse.systems.init.UISystem;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.util.Duration;
+
import java.util.List;
-/**
- *
- *
- * {@link HealthDeath}
- *
- * Hành động chết vì yêu :< vì hết máu.
- * Yêu cầu entity có {@link HealthIntComponent} trước.
- */
@Required(HealthIntComponent.class)
-@ForEntity({})
public class HealthDeath extends Behavior {
+
@Override
public void execute(List data) {
- final var health = entity.getComponent(HealthIntComponent.class);
+ var health = entity.getComponent(HealthIntComponent.class);
if (health != null && health.isZero()) {
+ if (entity.isType(EntityType.BRICK)) {
+ entity.getComponentOptional(BrickTextureManager.class)
+ .ifPresent(
+ textureManager -> {
+ var brickColor = textureManager.getColor();
+
+ UISystem.getInstance().addColorToWave(brickColor);
+
+ var scoreToAdd = 0;
+ switch (textureManager.brickType) {
+ case NORMAL, KEY:
+ scoreToAdd = 10;
+ break;
+ case SHIELD:
+ scoreToAdd = 20;
+ break;
+ case STRONG:
+ scoreToAdd = 30;
+ break;
+ case EXPLODING:
+ break;
+ }
+
+ if (scoreToAdd > 0) {
+ FXGL.inc("score", scoreToAdd);
+ }
+ });
+ }
entity.removeFromWorld();
+
+ FXGL.runOnce(
+ () -> {
+ if (FXGL.getGameWorld().getEntitiesByType(EntityType.BRICK).isEmpty()) {
+ GameSystem.nextLevel();
+ }
+ },
+ Duration.seconds(0.1));
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/ScaleChange.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/ScaleChange.java
index a54f4e3..92c61c6 100644
--- a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/ScaleChange.java
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/ScaleChange.java
@@ -1,51 +1,79 @@
package com.github.codestorm.bounceverse.components.behaviors;
-import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.components.TransformComponent;
-import com.github.codestorm.bounceverse.typing.annotations.ForEntity;
+import com.almasb.fxgl.time.TimerAction;
+import com.github.codestorm.bounceverse.components.Behavior;
+import com.github.codestorm.bounceverse.typing.interfaces.Undoable;
+
+import javafx.util.Duration;
+
import java.util.ArrayList;
import java.util.List;
-/**
- *
- *
- * {@link ScaleChange}
- *
- * Hành vi ScaleChange. Có thể {@link #execute(List)} nhiều lần nhưng sẽ không thể stack lên nhau
- * (và không làm mới).
- * ScaleChange sẽ áp dụng thông qua {@link Entity#getTransformComponent()} (tức cả view và
- * bounding).
- *
- * @see TransformComponent
- */
-@ForEntity({})
-public class ScaleChange extends UndoableBehavior {
+/** Thay đổi kích thước entity tạm thời và có thể hoàn tác (undo). */
+public class ScaleChange extends Behavior implements Undoable {
+
public static final double DONT_CHANGE = 1;
private double scaleWidth = DONT_CHANGE;
private double scaleHeight = DONT_CHANGE;
+ private final boolean removeWhenUndo;
+ private final Duration duration;
+ private List modified = null;
+ private TimerAction current;
+
+ @Override
+ public Duration getDuration() {
+ return duration;
+ }
+
@Override
- protected List executeLogic(List data) {
- if (scaleWidth <= 0 || scaleHeight <= 0) {
- return null;
- }
- final var transform = entity.getTransformComponent();
+ public boolean isRemoveWhenUndo() {
+ return removeWhenUndo;
+ }
+
+ @Override
+ public List getModified() {
+ return modified;
+ }
+
+ @Override
+ public void setModified(List modified) {
+ this.modified = modified;
+ }
+
+ @Override
+ public TimerAction getCurrent() {
+ return current;
+ }
+
+ @Override
+ public void setCurrent(TimerAction current) {
+ this.current = current;
+ }
+
+ @Override
+ public List executeLogic(List data) {
+ if (scaleWidth <= 0 || scaleHeight <= 0) return null;
+ var transform = entity.getTransformComponent();
transform.setScaleX(transform.getScaleX() * scaleWidth);
transform.setScaleY(transform.getScaleY() * scaleHeight);
return new ArrayList<>();
}
@Override
- protected boolean undoLogic(List data) {
- if (scaleWidth <= 0 || scaleHeight <= 0) {
- return false;
- }
- final var transform = entity.getTransformComponent();
+ public boolean undoLogic(List data) {
+ if (scaleWidth <= 0 || scaleHeight <= 0) return false;
+ var transform = entity.getTransformComponent();
transform.setScaleX(transform.getScaleX() / scaleWidth);
transform.setScaleY(transform.getScaleY() / scaleHeight);
return true;
}
+ @Override
+ public void onRemoved() {
+ undo();
+ }
+
public double getScaleWidth() {
return scaleWidth;
}
@@ -61,4 +89,9 @@ public double getScaleHeight() {
public void setScaleHeight(double scaleHeight) {
this.scaleHeight = scaleHeight;
}
+
+ public ScaleChange(Duration duration) {
+ this.duration = duration;
+ this.removeWhenUndo = true;
+ }
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Special.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Special.java
new file mode 100644
index 0000000..550610a
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/Special.java
@@ -0,0 +1,21 @@
+package com.github.codestorm.bounceverse.components.behaviors;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.component.Component;
+import com.github.codestorm.bounceverse.systems.init.PowerUpSpawner;
+import com.github.codestorm.bounceverse.typing.annotations.OnlyForEntity;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.util.Duration;
+
+/** Khi brick bị phá thì spawn PowerUp. */
+@OnlyForEntity({EntityType.BRICK})
+public class Special extends Component {
+
+ @Override
+ public void onRemoved() {
+ var pos = getEntity().getCenter();
+
+ FXGL.runOnce(() -> PowerUpSpawner.spawnRandom(pos.add(0, 10)), Duration.seconds(0.017));
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/UndoableBehavior.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/UndoableBehavior.java
deleted file mode 100644
index ab9341a..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/UndoableBehavior.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.github.codestorm.bounceverse.components.behaviors;
-
-import com.almasb.fxgl.dsl.FXGL;
-import com.almasb.fxgl.time.TimerAction;
-import com.github.codestorm.bounceverse.typing.interfaces.CanUndo;
-import java.util.List;
-import javafx.util.Duration;
-
-/**
- *
- *
- * {@link UndoableBehavior}
- *
- * {@link Behavior} có thể thực thi và hoàn tác được.
- *
- * @see CanUndo
- */
-public abstract class UndoableBehavior extends Behavior implements CanUndo {
- protected Duration duration = Duration.INDEFINITE;
- private List modified = null;
- private TimerAction current;
-
- /**
- * Có phải hành động đã được thực thi không.
- *
- * @return {@code true} nếu đã thực thi, {@code false} nếu chưa
- */
- public final boolean isActive() {
- return current != null && current.isExpired() && modified != null;
- }
-
- /**
- * Logic bên trong {@link #execute(List)}.
- *
- * @param data Dữ liệu truyền vào
- * @return {@code null} nếu không thực thi, {@link List} các dữ liệu cần hoàn tác lại sau này
- * nếu ngược lại
- */
- protected abstract List executeLogic(List data);
-
- /**
- * Logic bên trong {@link CanUndo#undo()}.
- *
- * @param data Dữ liệu cần hoàn tác
- * @return {@code true} nếu cho phép hoàn tác, {@code false} nếu không.
- */
- protected abstract boolean undoLogic(List data);
-
- @Override
- public final void execute(List data) {
- if (isActive()) {
- return;
- }
-
- modified = executeLogic(data);
- if (modified != null) {
- current = FXGL.getGameTimer().runOnceAfter(this::undo, duration);
- }
- }
-
- @Override
- public final void undo() {
- if (!isActive()) {
- return;
- }
-
- if (undoLogic(modified)) {
- current = null;
- modified = null;
- }
- }
-
- public Duration getDuration() {
- return duration;
- }
-
- public void setDuration(Duration duration) {
- this.duration = duration;
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/brick/BrickDropPowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/brick/BrickDropPowerUp.java
new file mode 100644
index 0000000..d8c355a
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/brick/BrickDropPowerUp.java
@@ -0,0 +1,24 @@
+package com.github.codestorm.bounceverse.components.behaviors.brick;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.github.codestorm.bounceverse.components.Behavior;
+import com.github.codestorm.bounceverse.typing.annotations.OnlyForEntity;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import java.util.List;
+
+/** Khi brick bị phá thì spawn PowerUp. */
+@OnlyForEntity({EntityType.BRICK})
+public final class BrickDropPowerUp extends Behavior {
+ // TODO: Thêm Component PowerUpContainer
+
+ @Override
+ public void execute(List data) {
+ FXGL.spawn("powerUp", getEntity().getCenter());
+ }
+
+ @Override
+ public void onRemoved() {
+ execute(List.of());
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/brick/StrongBrickTextureUpdater.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/brick/StrongBrickTextureUpdater.java
new file mode 100644
index 0000000..7b0066b
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/brick/StrongBrickTextureUpdater.java
@@ -0,0 +1,58 @@
+package com.github.codestorm.bounceverse.components.behaviors.brick;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.dsl.components.HealthIntComponent;
+import com.almasb.fxgl.entity.component.Component;
+import com.github.codestorm.bounceverse.AssetsPath;
+import com.github.codestorm.bounceverse.typing.enums.BrickType;
+
+import javafx.scene.paint.Color;
+
+/** Tự động đổi texture của Strong Brick dựa theo phần trăm HP còn lại. */
+public class StrongBrickTextureUpdater extends Component {
+
+ private final BrickType brickType = BrickType.STRONG;
+ private Color color = Color.BLUE;
+
+ @Override
+ public void onAdded() {
+ var health = getEntity().getComponent(HealthIntComponent.class);
+ health.valueProperty().addListener((obs, oldVal, newVal) -> updateTexture());
+ updateTexture();
+ }
+
+ private void updateTexture() {
+ var health = getEntity().getComponent(HealthIntComponent.class);
+ var hpPercent = Math.max(0.0, health.getValue() / (double) health.getMaxValue());
+
+ // Lấy texture tương ứng với HP còn lại
+ var colorAssets = AssetsPath.Textures.Bricks.COLORS.get(getColorName());
+ var texPath = colorAssets.getTexture(brickType, hpPercent);
+
+ var tex = FXGL.texture(texPath);
+
+ // Giữ nguyên kích thước ban đầu của brick
+ var width = getEntity().getWidth();
+ var height = getEntity().getHeight();
+ tex.setFitWidth(width);
+ tex.setFitHeight(height);
+
+ getEntity().getViewComponent().clearChildren();
+ getEntity().getViewComponent().addChild(tex);
+ }
+
+ private String getColorName() {
+ if (color.equals(Color.BLUE)) return "blue";
+ if (color.equals(Color.GREEN)) return "green";
+ if (color.equals(Color.ORANGE)) return "orange";
+ if (color.equals(Color.PINK)) return "pink";
+ if (color.equals(Color.RED)) return "red";
+ if (color.equals(Color.YELLOW)) return "yellow";
+ return "blue";
+ }
+
+ public StrongBrickTextureUpdater withColor(Color color) {
+ this.color = color;
+ return this;
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/paddle/PaddleShooting.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/paddle/PaddleShooting.java
index a310f86..efd6753 100644
--- a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/paddle/PaddleShooting.java
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/paddle/PaddleShooting.java
@@ -3,15 +3,17 @@
import com.almasb.fxgl.core.math.Vec2;
import com.almasb.fxgl.dsl.FXGL;
import com.almasb.fxgl.entity.SpawnData;
-import com.github.codestorm.bounceverse.Utilities.Time.Cooldown;
+import com.github.codestorm.bounceverse.components.Behavior;
import com.github.codestorm.bounceverse.components.behaviors.Attack;
-import com.github.codestorm.bounceverse.components.behaviors.Behavior;
-import com.github.codestorm.bounceverse.typing.annotations.ForEntity;
+import com.github.codestorm.bounceverse.typing.annotations.OnlyForEntity;
import com.github.codestorm.bounceverse.typing.enums.DirectionUnit;
import com.github.codestorm.bounceverse.typing.enums.EntityType;
-import java.util.List;
+import com.github.codestorm.bounceverse.typing.structures.Cooldown;
+
import javafx.util.Duration;
+import java.util.List;
+
/**
*
*
@@ -19,8 +21,8 @@
*
* Khả năng {@link EntityType#PADDLE} có thể bắn ra {@link EntityType#BULLET}.
*/
-@ForEntity(EntityType.PADDLE)
-public class PaddleShooting extends Behavior {
+@OnlyForEntity(EntityType.PADDLE)
+public final class PaddleShooting extends Behavior {
private static final double OFFSET_LEFT = 4;
private static final double OFFSET_RIGHT = -8;
private static final double OFFSET_HEIGHT = -10;
@@ -38,12 +40,12 @@ public PaddleShooting(Duration cooldown) {
@Override
public void execute(List data) {
- if (!cooldown.getCurrent().expired()) {
+ if (!cooldown.getCurrent().isExpired()) {
return;
}
- double leftX = entity.getX() + OFFSET_LEFT;
- double rightX = entity.getRightX() + OFFSET_RIGHT;
- double y = entity.getY() + OFFSET_HEIGHT;
+ var leftX = entity.getX() + OFFSET_LEFT;
+ var rightX = entity.getRightX() + OFFSET_RIGHT;
+ var y = entity.getY() + OFFSET_HEIGHT;
final var attack = entity.getComponentOptional(Attack.class);
if (attack.isEmpty()) {
@@ -61,7 +63,7 @@ public void execute(List data) {
FXGL.spawn("paddleBullet", leftData);
FXGL.spawn("paddleBullet", rightData);
- cooldown.getCurrent().makeNew();
+ cooldown.getCurrent().createNew();
}
public Cooldown getCooldown() {
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/behaviors/paddle/ReverseControlComponent.java b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/paddle/ReverseControlComponent.java
new file mode 100644
index 0000000..e98d612
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/behaviors/paddle/ReverseControlComponent.java
@@ -0,0 +1,11 @@
+package com.github.codestorm.bounceverse.components.behaviors.paddle;
+
+import com.almasb.fxgl.entity.component.Component;
+
+/**
+ * Component đảo ngược điều khiển của paddle trong thời gian ngắn. Chỉ đóng vai trò "flag" để
+ * PaddleMovement đọc và đảo hướng.
+ */
+public final class ReverseControlComponent extends Component {
+ // Mặc định: đảo ngược điều khiển
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/Attributes.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/Attributes.java
index 371d526..09eef03 100644
--- a/src/main/java/com/github/codestorm/bounceverse/components/properties/Attributes.java
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/Attributes.java
@@ -1,16 +1,15 @@
package com.github.codestorm.bounceverse.components.properties;
import com.almasb.fxgl.entity.Entity;
-import com.github.codestorm.bounceverse.typing.annotations.ForEntity;
+import com.github.codestorm.bounceverse.components.Property;
/**
*
*
* {@link Attributes}
*
- * Các chỉ số thuộc tính chung (chưa cụ thể thành class) của {@link Entity}.
+ * Các chỉ số thuộc tính chung (chưa cụ thể thành class) của {@link Entity} nói chung.
*/
-@ForEntity({})
public final class Attributes extends Property {
public static final int DEFAULT_DEFENSE = 0;
private int defense = DEFAULT_DEFENSE;
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/Shield.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/Shield.java
index 7a71c8e..8bb31e0 100644
--- a/src/main/java/com/github/codestorm/bounceverse/components/properties/Shield.java
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/Shield.java
@@ -1,10 +1,12 @@
package com.github.codestorm.bounceverse.components.properties;
import com.almasb.fxgl.entity.Entity;
-import com.github.codestorm.bounceverse.typing.annotations.ForEntity;
+import com.github.codestorm.bounceverse.components.Property;
+
+import javafx.geometry.Side;
+
import java.util.Arrays;
import java.util.EnumSet;
-import javafx.geometry.Side;
/**
*
@@ -13,8 +15,7 @@
*
* Khiên bảo vệ {@link Entity}. Khiên có thể bảo vệ một hoặc nhiều phía khỏi bị tấn công.
*/
-@ForEntity({})
-public class Shield extends Property {
+public final class Shield extends Property {
private EnumSet sides = EnumSet.noneOf(Side.class);
/**
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/brick/BrickTextureManager.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/brick/BrickTextureManager.java
new file mode 100644
index 0000000..57f3c1d
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/brick/BrickTextureManager.java
@@ -0,0 +1,86 @@
+package com.github.codestorm.bounceverse.components.properties.brick;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.dsl.components.HealthIntComponent;
+import com.almasb.fxgl.entity.component.CoreComponent;
+import com.almasb.fxgl.entity.component.Required;
+import com.github.codestorm.bounceverse.AssetsPath;
+import com.github.codestorm.bounceverse.components.Property;
+import com.github.codestorm.bounceverse.typing.annotations.OnlyForEntity;
+import com.github.codestorm.bounceverse.typing.enums.BrickType;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.scene.Node;
+import javafx.scene.paint.Color;
+
+/**
+ *
+ *
+ * {@link BrickTextureManager}
+ *
+ * Sự thay đổi texture của {@link EntityType#BRICK} theo {@link HealthIntComponent};
+ */
+@CoreComponent
+@Required(HealthIntComponent.class)
+@OnlyForEntity({EntityType.BRICK})
+public final class BrickTextureManager extends Property {
+ private String oldTexturePath;
+ private Node oldTexture;
+ public final BrickType brickType;
+ public final Color color;
+
+ public BrickTextureManager(BrickType brickType, Color color) {
+ this.brickType = brickType;
+ this.color = color;
+ }
+
+ private static Node makeView(String texturePath, int width, int height) {
+ var view = FXGL.getAssetLoader().loadTexture(texturePath, width, height);
+ view.setSmooth(false);
+ view.setPreserveRatio(false);
+ return view;
+ }
+
+ private String getColorName() {
+ if (color.equals(Color.BLUE)) return "blue";
+ if (color.equals(Color.GREEN)) return "green";
+ if (color.equals(Color.ORANGE)) return "orange";
+ if (color.equals(Color.PINK)) return "pink";
+ if (color.equals(Color.RED)) return "red";
+ if (color.equals(Color.YELLOW)) return "yellow";
+ return "blue";
+ }
+
+ @Override
+ public void onUpdate(double tpf) {
+ final var health = entity.getComponent(HealthIntComponent.class);
+ final var percent = health.getValuePercent() / 100;
+
+ // SỬA ĐỔI Ở ĐÂY
+ final var colorKey = getColorName(); // Chuyển Color object thành String key
+ final var colorTextures =
+ AssetsPath.Textures.Bricks.COLORS.get(colorKey); // Dùng key String để tra cứu
+
+ // Đoạn mã còn lại giữ nguyên
+ final var texturePath = colorTextures.getTexture(brickType, percent);
+
+ final var views = entity.getViewComponent();
+ if (oldTexturePath == null) {
+ oldTexturePath = texturePath;
+ oldTexture = makeView(texturePath, (int) entity.getWidth(), (int) entity.getHeight());
+ views.addChild(oldTexture);
+ } else {
+ if (!oldTexturePath.equals(texturePath)) {
+ views.removeChild(oldTexture);
+ oldTexturePath = texturePath;
+ oldTexture =
+ makeView(texturePath, (int) entity.getWidth(), (int) entity.getHeight());
+ views.addChild(oldTexture);
+ }
+ }
+ }
+
+ public Color getColor() {
+ return this.color;
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/paddle/PaddlePowerComponent.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/paddle/PaddlePowerComponent.java
new file mode 100644
index 0000000..1bf2d72
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/paddle/PaddlePowerComponent.java
@@ -0,0 +1,65 @@
+package com.github.codestorm.bounceverse.components.properties.paddle;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.component.Component;
+import com.github.codestorm.bounceverse.components.properties.powerup.ball.FastBallPowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.ball.MultipleBallPowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.misc.ExtraLifePowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.misc.ShieldPowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.paddle.DuplicatePaddlePowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.paddle.GunPowerUp;
+import com.github.codestorm.bounceverse.typing.structures.Cooldown;
+
+import javafx.util.Duration;
+
+public class PaddlePowerComponent extends Component {
+
+ private final Cooldown powerCooldown = new Cooldown(Duration.seconds(20));
+ private static final Duration POWER_VISUAL_DURATION = Duration.seconds(1.0);
+
+ /** Phương thức chính để kích hoạt sức mạnh đặc biệt của paddle. */
+ public void activatePower() {
+ if (!powerCooldown.getCurrent().isExpired()) {
+ System.out.println("Power is on cooldown!");
+ return;
+ }
+
+ var paddle = getEntity();
+ var color = FXGL.gets("paddleColor");
+ var viewManager = paddle.getComponent(PaddleViewManager.class);
+
+ System.out.println("Activating power for " + color + " paddle!");
+
+ switch (color) {
+ case "red":
+ new GunPowerUp().apply(paddle);
+ break;
+ case "green":
+ new ExtraLifePowerUp().apply(paddle);
+ break;
+ case "blue":
+ new ShieldPowerUp().apply(paddle);
+ break;
+ case "orange":
+ new MultipleBallPowerUp().apply(paddle);
+ break;
+ case "pink":
+ new FastBallPowerUp().apply(paddle);
+ break;
+ case "yellow":
+ new DuplicatePaddlePowerUp().apply(paddle);
+ break;
+ default:
+ return;
+ }
+
+ viewManager.setPowerState();
+ FXGL.getGameTimer().runOnceAfter(viewManager::setNormalState, POWER_VISUAL_DURATION);
+
+ powerCooldown.getCurrent().createNew();
+ }
+
+ public Cooldown getPowerCooldown() {
+ return powerCooldown;
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/paddle/PaddleViewManager.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/paddle/PaddleViewManager.java
new file mode 100644
index 0000000..8c42c81
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/paddle/PaddleViewManager.java
@@ -0,0 +1,133 @@
+package com.github.codestorm.bounceverse.components.properties.paddle;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.component.Component;
+import com.almasb.fxgl.entity.components.BoundingBoxComponent;
+import com.almasb.fxgl.physics.BoundingShape;
+import com.almasb.fxgl.physics.HitBox;
+import com.almasb.fxgl.texture.Texture;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Component duy nhất quản lý TOÀN BỘ hình ảnh và kích thước của Paddle. Nó thay thế cho cả
+ * PaddleSizeManager và PaddleTextureManager.
+ */
+public class PaddleViewManager extends Component {
+
+ private String color;
+ private Texture paddleTexture;
+ private BoundingBoxComponent bbox;
+
+ private double originalWidth;
+ private double originalHeight;
+
+ private String currentState = "normal";
+ private double currentSizeFactor = 1.0;
+
+ private final List clones = new ArrayList<>();
+
+ // Các hệ số kích thước cho từng trạng thái
+ private static final double EXPAND_FACTOR = 1.5;
+ private static final double SHRINK_FACTOR = 0.7;
+
+ @Override
+ public void onAdded() {
+ this.color = FXGL.gets("paddleColor");
+ this.bbox = entity.getBoundingBoxComponent();
+
+ this.originalWidth = entity.getWidth();
+ this.originalHeight = entity.getHeight();
+
+ // Khởi tạo Texture và thêm vào entity
+ this.paddleTexture = FXGL.texture(getTexturePath("normal"));
+ entity.getViewComponent().addChild(paddleTexture);
+
+ // Cập nhật trạng thái ban đầu
+ updateViewState("normal", 1.0);
+ }
+
+ // Các hàm công khai để các PowerUp gọi
+ public void setNormalState() {
+ updateViewState("normal", 1.0);
+ }
+
+ public void setExpandState() {
+ updateViewState("expand", EXPAND_FACTOR);
+ }
+
+ public void setShrinkState() {
+ updateViewState("shrink", SHRINK_FACTOR);
+ }
+
+ public void setPowerState() {
+ updateViewState("power", 1.0);
+ }
+
+ public void reset() {
+ setNormalState();
+ }
+
+ public void registerClone(Entity clone) {
+ clones.add(clone);
+ // Ngay khi đăng ký, đồng bộ hóa trạng thái hiện tại cho bản sao
+ updateViewState(currentState, currentSizeFactor);
+ }
+
+ public void clearClones() {
+ clones.clear();
+ }
+
+ /**
+ * Hàm trung tâm, chịu trách nhiệm cập nhật MỌI THỨ.
+ *
+ * @param state Tên trạng thái ("normal", "expand", "shrink")
+ * @param sizeFactor Hệ số nhân kích thước (1.0 cho normal)
+ */
+ private void updateViewState(String state, double sizeFactor) {
+ this.currentState = state;
+ this.currentSizeFactor = sizeFactor;
+
+ var newWidth = originalWidth * sizeFactor;
+
+ bbox.clearHitBoxes();
+ bbox.addHitBox(new HitBox(BoundingShape.box(newWidth, originalHeight)));
+ paddleTexture.setImage(FXGL.getAssetLoader().loadImage(getTexturePath(state)));
+ paddleTexture.setFitWidth(newWidth);
+ paddleTexture.setFitHeight(originalHeight);
+
+ for (var clone : clones) {
+ if (clone.isActive()) {
+ // Cập nhật hitbox cho bản sao
+ clone.getBoundingBoxComponent().clearHitBoxes();
+ clone.getBoundingBoxComponent()
+ .addHitBox(new HitBox(BoundingShape.box(newWidth, originalHeight)));
+
+ // Cập nhật hình ảnh cho bản sao
+ var cloneTexture = (Texture) clone.getViewComponent().getChildren().getFirst();
+ cloneTexture.setImage(FXGL.getAssetLoader().loadImage(getTexturePath(state)));
+ cloneTexture.setFitWidth(newWidth);
+ cloneTexture.setFitHeight(originalHeight);
+ }
+ }
+
+ paddleTexture.setImage(FXGL.getAssetLoader().loadImage(getTexturePath(state)));
+
+ paddleTexture.setFitWidth(newWidth);
+ paddleTexture.setFitHeight(originalHeight);
+ }
+
+ private String getTexturePath(String state) {
+ var colorCapitalized = color.substring(0, 1).toUpperCase() + color.substring(1);
+ String textureName;
+ if ("normal".equals(state)) {
+ textureName = colorCapitalized + " Paddle.png";
+ } else {
+ var stateCapitalized = state.substring(0, 1).toUpperCase() + state.substring(1);
+ textureName = colorCapitalized + " " + stateCapitalized + " Paddle.png";
+ }
+ return "paddle/" + state + "/" + color + "/" + textureName;
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUp.java
new file mode 100644
index 0000000..0ec3b1c
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUp.java
@@ -0,0 +1,17 @@
+package com.github.codestorm.bounceverse.components.properties.powerup;
+
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.component.Component;
+
+/** Base class cho mọi Power-Up có thể áp dụng hiệu ứng. */
+public abstract class PowerUp extends Component {
+
+ protected final String name;
+
+ protected PowerUp(String name) {
+ this.name = name;
+ }
+
+ /** Kích hoạt hiệu ứng lên entity nhận (paddle, ball, v.v.) */
+ public abstract void apply(Entity target);
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUpContainer.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUpContainer.java
new file mode 100644
index 0000000..4d0e777
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUpContainer.java
@@ -0,0 +1,63 @@
+package com.github.codestorm.bounceverse.components.properties.powerup;
+
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.component.Component;
+import com.almasb.fxgl.entity.component.CoreComponent;
+import com.github.codestorm.bounceverse.components.Property;
+import com.github.codestorm.bounceverse.typing.annotations.OnlyForEntity;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+import com.google.common.collect.MutableClassToInstanceMap;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+
+/**
+ *
+ *
+ * {@link PowerUpContainer}
+ *
+ * Giống như một cái túi, nơi lưu trữ {@link Component} bên trong {@link Entity} và không kích hoạt
+ * logic của các component đó.
+ */
+@OnlyForEntity({EntityType.POWER_UP})
+@CoreComponent
+public final class PowerUpContainer extends Property {
+ private MutableClassToInstanceMap container = MutableClassToInstanceMap.create();
+
+ /**
+ * Gán trực tiếp các {@link Component} lên trên {@link Entity}.
+ *
+ * @param entity Entity.
+ */
+ public void addTo(Entity entity) {
+ for (var entry : container.entrySet()) {
+ final var component = entry.getValue();
+ entity.addComponent(component);
+ }
+ }
+
+ /**
+ * Thêm các {@link Component} vào {@link #container}.
+ *
+ * @param components Các component
+ * @see MutableClassToInstanceMap#putAll(Map)
+ */
+ public void put(@NotNull Component... components) {
+ for (var component : components) {
+ container.put(component.getClass(), component);
+ }
+ }
+
+ public PowerUpContainer(Component... components) {
+ put(components);
+ }
+
+ public MutableClassToInstanceMap getContainer() {
+ return container;
+ }
+
+ public void setContainer(MutableClassToInstanceMap container) {
+ this.container = container;
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUpManager.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUpManager.java
new file mode 100644
index 0000000..fa9835a
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/PowerUpManager.java
@@ -0,0 +1,161 @@
+package com.github.codestorm.bounceverse.components.properties.powerup;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.time.TimerAction;
+
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.util.Duration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Quản lý các Power-Up đang hoạt động: thời gian, gia hạn, và hiển thị HUD đếm ngược. Dùng
+ * singleton.
+ */
+public final class PowerUpManager {
+
+ /**
+ * Lớp nội bộ để theo dõi mỗi power-up đang hoạt động. Sửa đổi: 'timer' không còn là final để có
+ * thể cập nhật khi cộng dồn thời gian.
+ */
+ private static final class ActivePowerUp {
+ TimerAction timer;
+ final Runnable onExpire;
+ final Text label;
+ double timeLeft;
+
+ ActivePowerUp(TimerAction timer, Runnable onExpire, Text label, double durationSeconds) {
+ this.timer = timer;
+ this.onExpire = onExpire;
+ this.label = label;
+ this.timeLeft = durationSeconds;
+ }
+ }
+
+ private final Map activePowerUps = new HashMap<>();
+ private final VBox hudBox = new VBox(6);
+ private boolean hudAdded = false;
+
+ private static final PowerUpManager INSTANCE = new PowerUpManager();
+
+ private PowerUpManager() {
+ hudBox.setTranslateX(FXGL.getAppWidth() - 220);
+ hudBox.setTranslateY(20);
+ }
+
+ public static PowerUpManager getInstance() {
+ return INSTANCE;
+ }
+
+ private void ensureHUDAdded() {
+ if (!hudAdded) {
+ FXGL.getGameScene().addUINode(hudBox);
+ hudAdded = true;
+ }
+ }
+
+ /**
+ * Kích hoạt hoặc gia hạn (cộng dồn) thời gian cho một Power-Up.
+ *
+ * @param name Tên power-up
+ * @param duration Thời gian hiệu lực của power-up mới
+ * @param onActivate Logic khi kích hoạt LẦN ĐẦU
+ * @param onExpire Logic khi hết hiệu lực hoàn toàn
+ */
+ public void activate(String name, Duration duration, Runnable onActivate, Runnable onExpire) {
+ ensureHUDAdded();
+ var newDurationSeconds = duration.toSeconds();
+
+ if (activePowerUps.containsKey(name)) {
+ var existing = activePowerUps.get(name);
+
+ existing.timer.expire();
+
+ var newTimeLeft = existing.timeLeft + newDurationSeconds;
+ existing.timeLeft = newTimeLeft;
+
+ existing.timer =
+ FXGL.getGameTimer()
+ .runOnceAfter(
+ () -> {
+ onExpire.run();
+ hudBox.getChildren().remove(existing.label);
+ activePowerUps.remove(name);
+ },
+ Duration.seconds(newTimeLeft));
+
+ return;
+ }
+
+ onActivate.run();
+
+ var label =
+ FXGL.getUIFactoryService()
+ .newText(name + " " + String.format("%.1fs", newDurationSeconds), 18);
+ label.getStyleClass().add("powerup-text");
+ hudBox.getChildren().add(label);
+
+ var timer =
+ FXGL.getGameTimer()
+ .runOnceAfter(
+ () -> {
+ onExpire.run();
+ hudBox.getChildren().remove(label);
+ activePowerUps.remove(name);
+ },
+ duration);
+
+ activePowerUps.put(name, new ActivePowerUp(timer, onExpire, label, newDurationSeconds));
+ }
+
+ public void onUpdate(double tpf) {
+ ensureHUDAdded();
+
+ for (var entry : activePowerUps.values()) {
+ entry.timeLeft -= tpf;
+ if (entry.timeLeft < 0) entry.timeLeft = 0;
+
+ var powerUpName = entry.label.getText().split(" ")[0];
+ entry.label.setText(String.format("%s %.1fs", powerUpName, entry.timeLeft));
+ }
+ }
+
+ public void clearAll() {
+ ensureHUDAdded();
+
+ activePowerUps
+ .values()
+ .forEach(
+ entry -> {
+ if (!entry.timer.isExpired()) {
+ entry.timer.expire();
+ }
+ entry.onExpire.run();
+ hudBox.getChildren().remove(entry.label);
+ });
+ activePowerUps.clear();
+ }
+
+ /**
+ * Hủy bỏ và dọn dẹp một Power-Up cụ thể đang hoạt động theo tên.
+ *
+ * @param name Tên của power-up cần hủy
+ */
+ public void clearPowerUp(String name) {
+ if (activePowerUps.containsKey(name)) {
+ var existing = activePowerUps.get(name);
+
+ if (!existing.timer.isExpired()) {
+ existing.timer.expire();
+ }
+
+ existing.onExpire.run();
+
+ hudBox.getChildren().remove(existing.label);
+
+ activePowerUps.remove(name);
+ }
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/FastBallPowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/FastBallPowerUp.java
new file mode 100644
index 0000000..38d460d
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/FastBallPowerUp.java
@@ -0,0 +1,50 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.ball;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.physics.PhysicsComponent;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+import com.github.codestorm.bounceverse.systems.init.GameSystem;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.util.Duration;
+
+/** Làm nhanh bóng trong một khoảng thời gian. */
+public class FastBallPowerUp extends PowerUp {
+
+ private static final Duration DURATION = Duration.seconds(6);
+ private static final double SPEED_MULTIPLIER = 1.5;
+
+ public FastBallPowerUp() {
+ super("FastBall");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ PowerUpManager.getInstance().activate(name, DURATION, this::speedUpBalls, this::resetBalls);
+ }
+
+ private void speedUpBalls() {
+ var currentSpeed = FXGL.getd("ballSpeed");
+ FXGL.set("ballSpeed", currentSpeed * SPEED_MULTIPLIER);
+ updateAllBallSpeeds();
+ }
+
+ private void resetBalls() {
+ FXGL.set("ballSpeed", GameSystem.Variables.DEFAULT_BALL_SPEED);
+ updateAllBallSpeeds();
+ }
+
+ private void updateAllBallSpeeds() {
+ var newSpeed = FXGL.getd("ballSpeed");
+ FXGL.getGameWorld()
+ .getEntitiesByType(EntityType.BALL)
+ .forEach(
+ ball -> {
+ var physics = ball.getComponent(PhysicsComponent.class);
+ physics.setLinearVelocity(
+ physics.getLinearVelocity().normalize().multiply(newSpeed));
+ });
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/MultipleBallPowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/MultipleBallPowerUp.java
new file mode 100644
index 0000000..c11fefb
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/MultipleBallPowerUp.java
@@ -0,0 +1,71 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.ball;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.SpawnData;
+import com.almasb.fxgl.physics.PhysicsComponent;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.geometry.Point2D;
+
+import java.util.List;
+
+/**
+ * PowerUp nhân đôi toàn bộ bóng hiện có. Mỗi bóng sinh thêm 1 bóng mới tách từ vị trí của nó và bay
+ * lệch nhẹ sang 2 hướng.
+ */
+public final class MultipleBallPowerUp extends PowerUp {
+
+ public MultipleBallPowerUp() {
+ super("MultipleBall");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ var balls = FXGL.getGameWorld().getEntitiesByType(EntityType.BALL);
+ var currentBalls = List.copyOf(balls);
+
+ for (var ball : currentBalls) {
+ ball.getComponentOptional(PhysicsComponent.class)
+ .ifPresent(
+ phys -> {
+ var pos = ball.getCenter();
+ var velocity = phys.getLinearVelocity();
+
+ if (velocity.magnitude() < 1e-3) return;
+
+ // chỉ nhân đôi: mỗi bóng cũ sinh thêm 1 bản sao duy nhất
+ spawnDuplicateBall(pos, velocity);
+ });
+ }
+ }
+
+ private void spawnDuplicateBall(Point2D pos, Point2D velocity) {
+ // lệch nhẹ góc bay để tránh trùng hướng
+ var newVelocity = rotateVector(velocity);
+
+ // spawn tại vị trí bóng gốc (dịch nhẹ 1 chút)
+ var spawnPos = pos.add(newVelocity.normalize().multiply(8));
+
+ var newBall = FXGL.spawn("ball", new SpawnData(spawnPos.getX(), spawnPos.getY()));
+
+ newBall.getComponentOptional(PhysicsComponent.class)
+ .ifPresent(
+ newPhys -> {
+ newPhys.setLinearVelocity(newVelocity);
+
+ // giữ velocity ổn định sau 1 frame
+ FXGL.runOnce(
+ () -> newPhys.setLinearVelocity(newVelocity),
+ javafx.util.Duration.millis(20));
+ });
+ }
+
+ private static Point2D rotateVector(Point2D v) {
+ var rad = Math.toRadians(15);
+ var cos = Math.cos(rad);
+ var sin = Math.sin(rad);
+ return new Point2D(v.getX() * cos - v.getY() * sin, v.getX() * sin + v.getY() * cos);
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/SlowBallPowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/SlowBallPowerUp.java
new file mode 100644
index 0000000..665bbaf
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/ball/SlowBallPowerUp.java
@@ -0,0 +1,50 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.ball;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.physics.PhysicsComponent;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+import com.github.codestorm.bounceverse.systems.init.GameSystem;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.util.Duration;
+
+/** Làm chậm tốc độ tất cả bóng tạm thời. */
+public class SlowBallPowerUp extends PowerUp {
+
+ private static final Duration DURATION = Duration.seconds(6);
+ private static final double SPEED_MULTIPLIER = 0.7;
+
+ public SlowBallPowerUp() {
+ super("SlowBall");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ PowerUpManager.getInstance().activate(name, DURATION, this::slowBalls, this::resetBalls);
+ }
+
+ private void slowBalls() {
+ var currentSpeed = FXGL.getd("ballSpeed");
+ FXGL.set("ballSpeed", currentSpeed * SPEED_MULTIPLIER);
+ updateAllBallSpeeds();
+ }
+
+ private void resetBalls() {
+ FXGL.set("ballSpeed", GameSystem.Variables.DEFAULT_BALL_SPEED);
+ updateAllBallSpeeds();
+ }
+
+ private void updateAllBallSpeeds() {
+ var newSpeed = FXGL.getd("ballSpeed");
+ FXGL.getGameWorld()
+ .getEntitiesByType(EntityType.BALL)
+ .forEach(
+ ball -> {
+ var physics = ball.getComponent(PhysicsComponent.class);
+ physics.setLinearVelocity(
+ physics.getLinearVelocity().normalize().multiply(newSpeed));
+ });
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/misc/ExtraLifePowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/misc/ExtraLifePowerUp.java
new file mode 100644
index 0000000..e2ed909
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/misc/ExtraLifePowerUp.java
@@ -0,0 +1,21 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.misc;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.typing.structures.HealthIntValue;
+
+/** Power-up cộng thêm một mạng cho người chơi. Hiệu ứng xảy ra ngay lập tức. */
+public final class ExtraLifePowerUp extends PowerUp {
+
+ public ExtraLifePowerUp() {
+ super("ExtraLife");
+ }
+
+ @Override
+ public void apply(Entity target) {
+ HealthIntValue lives = FXGL.getWorldProperties().getObject("lives");
+
+ lives.restore(1);
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/misc/ShieldPowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/misc/ShieldPowerUp.java
new file mode 100644
index 0000000..86e26e4
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/misc/ShieldPowerUp.java
@@ -0,0 +1,52 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.misc;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.physics.BoundingShape;
+import com.almasb.fxgl.physics.HitBox;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+import com.github.codestorm.bounceverse.typing.enums.PowerUpType;
+
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+/** Tạo một tấm chắn ở dưới đáy ngăn bóng rơi trong vài giây. */
+public class ShieldPowerUp extends PowerUp {
+
+ private static final Duration DURATION = Duration.seconds(8);
+ private Entity shieldEntity;
+
+ public ShieldPowerUp() {
+ super("Shield");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ PowerUpManager.getInstance()
+ .activate(name, DURATION, this::spawnShield, this::removeShield);
+ }
+
+ private void spawnShield() {
+ double appWidth = FXGL.getAppWidth();
+ double appHeight = FXGL.getAppHeight();
+
+ var shieldHeight = 20.0;
+
+ var view = new Rectangle(appWidth, 8, Color.AQUA);
+
+ shieldEntity =
+ FXGL.entityBuilder()
+ .at(0, appHeight - 10)
+ .type(PowerUpType.SHIELD)
+ .view(view)
+ .bbox(new HitBox(BoundingShape.box(appWidth, shieldHeight)))
+ .collidable()
+ .buildAndAttach();
+ }
+
+ private void removeShield() {
+ if (shieldEntity != null) shieldEntity.removeFromWorld();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/DuplicatePaddlePowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/DuplicatePaddlePowerUp.java
new file mode 100644
index 0000000..07cff82
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/DuplicatePaddlePowerUp.java
@@ -0,0 +1,98 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.paddle;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.physics.BoundingShape;
+import com.almasb.fxgl.physics.HitBox;
+import com.almasb.fxgl.physics.PhysicsComponent;
+import com.almasb.fxgl.physics.box2d.dynamics.BodyType;
+import com.github.codestorm.bounceverse.components.properties.paddle.PaddleViewManager;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class DuplicatePaddlePowerUp extends PowerUp {
+
+ private static final Duration DURATION = Duration.seconds(10);
+ private static final double PADDLE_GAP = 25.0;
+
+ public static final String CLONES_LIST_KEY = "clonedPaddles";
+
+ public DuplicatePaddlePowerUp() {
+ super("DuplicatePaddle");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ PowerUpManager.getInstance().clearPowerUp(this.name);
+ PowerUpManager.getInstance()
+ .activate(name, DURATION, () -> spawnClones(paddle), () -> removeClones(paddle));
+ }
+
+ private void spawnClones(Entity mainPaddle) {
+ var mainViewManager = mainPaddle.getComponent(PaddleViewManager.class);
+ var width = mainPaddle.getWidth();
+ var mainPaddleX = mainPaddle.getX();
+ var mainPaddleY = mainPaddle.getY();
+
+ // Tạo bản sao bên trái
+ var leftClone = createClone(mainPaddle);
+ // THAY ĐỔI QUAN TRỌNG: KHÔNG DÙNG .bind(), chỉ đặt vị trí ban đầu
+ leftClone.setPosition(mainPaddleX - width - PADDLE_GAP, mainPaddleY);
+ FXGL.getGameWorld().addEntity(leftClone);
+ mainViewManager.registerClone(leftClone);
+
+ // Tạo bản sao bên phải
+ var rightClone = createClone(mainPaddle);
+ // THAY ĐỔI QUAN TRỌNG: KHÔNG DÙNG .bind(), chỉ đặt vị trí ban đầu
+ rightClone.setPosition(mainPaddleX + width + PADDLE_GAP, mainPaddleY);
+ FXGL.getGameWorld().addEntity(rightClone);
+ mainViewManager.registerClone(rightClone);
+
+ // Lưu danh sách vào biến chung để InputSystem điều khiển
+ List clones = new ArrayList<>();
+ clones.add(leftClone);
+ clones.add(rightClone);
+ FXGL.set(CLONES_LIST_KEY, clones);
+ }
+
+ private void removeClones(Entity mainPaddle) {
+ if (mainPaddle != null && mainPaddle.isActive()) {
+ mainPaddle.getComponent(PaddleViewManager.class).clearClones();
+ }
+
+ if (FXGL.getWorldProperties().exists(CLONES_LIST_KEY)) {
+ List clones = FXGL.geto(CLONES_LIST_KEY);
+ for (var clone : clones) {
+ if (clone.isActive()) {
+ // Không cần unbind vì chúng ta không bind ngay từ đầu
+ clone.removeFromWorld();
+ }
+ }
+ FXGL.getWorldProperties().remove(CLONES_LIST_KEY);
+ }
+ }
+
+ private Entity createClone(Entity mainPaddle) {
+ var color = FXGL.gets("paddleColor");
+ var colorCapitalized = color.substring(0, 1).toUpperCase() + color.substring(1);
+ var texturePath = "paddle/power/" + color + "/" + colorCapitalized + " Power Paddle.png";
+ var texture = FXGL.texture(texturePath, mainPaddle.getWidth(), mainPaddle.getHeight());
+
+ var physics = new PhysicsComponent();
+ physics.setBodyType(BodyType.KINEMATIC);
+
+ return FXGL.entityBuilder()
+ .type(EntityType.PADDLE_CLONE)
+ .view(texture)
+ .bbox(new HitBox(BoundingShape.box(mainPaddle.getWidth(), mainPaddle.getHeight())))
+ .collidable()
+ .with(physics)
+ .build();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ExpandPaddlePowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ExpandPaddlePowerUp.java
new file mode 100644
index 0000000..82e2c7e
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ExpandPaddlePowerUp.java
@@ -0,0 +1,26 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.paddle;
+
+import com.almasb.fxgl.entity.Entity;
+import com.github.codestorm.bounceverse.components.properties.paddle.PaddleViewManager;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+
+import javafx.util.Duration;
+
+/** Power-Up mở rộng Paddle tạm thời. */
+public final class ExpandPaddlePowerUp extends PowerUp {
+
+ private static final Duration DURATION = Duration.seconds(8);
+
+ public ExpandPaddlePowerUp() {
+ super("ExpandPaddle");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ var viewManager = paddle.getComponent(PaddleViewManager.class);
+
+ PowerUpManager.getInstance()
+ .activate(name, DURATION, viewManager::setExpandState, viewManager::setNormalState);
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/GunPowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/GunPowerUp.java
new file mode 100644
index 0000000..6852afa
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/GunPowerUp.java
@@ -0,0 +1,107 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.paddle;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.time.TimerAction;
+import com.github.codestorm.bounceverse.components.behaviors.paddle.PaddleShooting;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.util.Duration;
+
+// Power up tạo ra gun.
+public final class GunPowerUp extends PowerUp {
+
+ private static final Duration DURATION = Duration.seconds(10);
+ private static final double GUN_WIDTH = 6;
+ private static final double GUN_HEIGHT = 10;
+ private static final double OFFSET_Y = -10;
+ private static final double OFFSET_X_LEFT = 4;
+ private static final double OFFSET_X_RIGHT = 4;
+
+ public GunPowerUp() {
+ super("Gun");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ PowerUpManager.getInstance().clearPowerUp(this.name);
+
+ PowerUpManager.getInstance()
+ .activate(
+ name,
+ DURATION,
+ () -> {
+ var shooting =
+ paddle.getComponentOptional(PaddleShooting.class)
+ .orElseGet(
+ () -> {
+ var newShooting = new PaddleShooting(0.25);
+ paddle.addComponent(newShooting);
+ return newShooting;
+ });
+
+ var leftGun = createGunEntity(paddle);
+ var rightGun = createGunEntity(paddle);
+
+ var pT = paddle.getTransformComponent();
+ var lT = leftGun.getTransformComponent();
+ var rT = rightGun.getTransformComponent();
+
+ lT.xProperty().bind(pT.xProperty().add(OFFSET_X_LEFT));
+ lT.yProperty().bind(pT.yProperty().add(OFFSET_Y));
+ rT.xProperty()
+ .bind(
+ pT.xProperty()
+ .add(
+ paddle.getBoundingBoxComponent()
+ .widthProperty())
+ .subtract(GUN_WIDTH)
+ .subtract(OFFSET_X_RIGHT));
+ rT.yProperty().bind(pT.yProperty().add(OFFSET_Y));
+
+ var shootingTask =
+ FXGL.getGameTimer()
+ .runAtInterval(
+ () -> shooting.execute(null),
+ Duration.seconds(0.25));
+
+ // Lưu lại các đối tượng cần dọn dẹp vào paddle
+ paddle.setProperty("gun.left", leftGun);
+ paddle.setProperty("gun.right", rightGun);
+ paddle.setProperty("gun.task", shootingTask);
+ },
+ () -> {
+ Entity leftGun = paddle.getObject("gun.left");
+ Entity rightGun = paddle.getObject("gun.right");
+ TimerAction shootingTask = paddle.getObject("gun.task");
+
+ if (shootingTask != null) {
+ shootingTask.expire();
+ }
+
+ if (leftGun != null && leftGun.isActive()) {
+ leftGun.getTransformComponent().xProperty().unbind();
+ leftGun.getTransformComponent().yProperty().unbind();
+ leftGun.removeFromWorld();
+ }
+ if (rightGun != null && rightGun.isActive()) {
+ rightGun.getTransformComponent().xProperty().unbind();
+ rightGun.getTransformComponent().yProperty().unbind();
+ rightGun.removeFromWorld();
+ }
+ if (paddle.hasComponent(PaddleShooting.class)) {
+ paddle.removeComponent(PaddleShooting.class);
+ }
+ });
+ }
+
+ private Entity createGunEntity(Entity owner) {
+ return FXGL.entityBuilder()
+ .view(new Rectangle(GUN_WIDTH, GUN_HEIGHT, Color.DARKRED))
+ .zIndex(owner.getZIndex() + 1)
+ .buildAndAttach();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ReversePaddlePowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ReversePaddlePowerUp.java
new file mode 100644
index 0000000..8396896
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ReversePaddlePowerUp.java
@@ -0,0 +1,38 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.paddle;
+
+import com.almasb.fxgl.entity.Entity;
+import com.github.codestorm.bounceverse.components.behaviors.paddle.ReverseControlComponent;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+
+import javafx.util.Duration;
+
+/** Power-Up: Đảo ngược điều khiển của paddle trong thời gian ngắn. */
+public final class ReversePaddlePowerUp extends PowerUp {
+
+ private static final Duration DURATION = Duration.seconds(10);
+
+ public ReversePaddlePowerUp() {
+ super("ReversePaddle");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ PowerUpManager.getInstance()
+ .activate(
+ name,
+ DURATION,
+ () -> {
+ // Nếu paddle chưa có component thì thêm
+ if (!paddle.hasComponent(ReverseControlComponent.class)) {
+ paddle.addComponent(new ReverseControlComponent());
+ }
+ },
+ () -> {
+ // Khi hết hạn thì gỡ component nếu còn
+ if (paddle.hasComponent(ReverseControlComponent.class)) {
+ paddle.removeComponent(ReverseControlComponent.class);
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ShrinkPaddlePowerUp.java b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ShrinkPaddlePowerUp.java
new file mode 100644
index 0000000..7087c2d
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/components/properties/powerup/paddle/ShrinkPaddlePowerUp.java
@@ -0,0 +1,26 @@
+package com.github.codestorm.bounceverse.components.properties.powerup.paddle;
+
+import com.almasb.fxgl.entity.Entity;
+import com.github.codestorm.bounceverse.components.properties.paddle.PaddleViewManager;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+
+import javafx.util.Duration;
+
+/** Power-Up thu nhỏ Paddle tạm thời. */
+public final class ShrinkPaddlePowerUp extends PowerUp {
+
+ private static final Duration DURATION = Duration.seconds(8);
+
+ public ShrinkPaddlePowerUp() {
+ super("ShrinkPaddle");
+ }
+
+ @Override
+ public void apply(Entity paddle) {
+ var viewManager = paddle.getComponent(PaddleViewManager.class);
+
+ PowerUpManager.getInstance()
+ .activate(name, DURATION, viewManager::setShrinkState, viewManager::setNormalState);
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/core/LaunchOptions.java b/src/main/java/com/github/codestorm/bounceverse/core/LaunchOptions.java
deleted file mode 100644
index 688a754..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/core/LaunchOptions.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.github.codestorm.bounceverse.core;
-
-import com.github.codestorm.bounceverse.Utilities;
-
-/**
- *
- *
- * {@link LaunchOptions}
- *
- * Các tùy chọn khi khởi động được áp dụng trong game.
- * Sử dụng {@link #load(String...)} để tải các options.
- */
-public final class LaunchOptions {
- private static boolean debug = false;
-
- private LaunchOptions() {}
-
- /**
- * Tải các launch options từ Command-line Arguments.
- *
- * @param args Command-Line Arguments - được truyền vào từ hàm {@code main()}
- */
- public static void load(String... args) {
- final var map = Utilities.IO.parseArgs(args, null, null);
-
- debug = Boolean.parseBoolean(map.getOrDefault("debug", "false"));
- }
-
- public static boolean isDebug() {
- return debug;
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/core/SettingsManager.java b/src/main/java/com/github/codestorm/bounceverse/core/SettingsManager.java
deleted file mode 100644
index f557990..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/core/SettingsManager.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.github.codestorm.bounceverse.core;
-
-import com.almasb.fxgl.app.ApplicationMode;
-import com.almasb.fxgl.app.GameSettings;
-import com.github.codestorm.bounceverse.Utilities;
-import com.github.codestorm.bounceverse.factory.SceneFactory;
-import java.io.IOException;
-
-/**
- *
- *
- * {@link SettingsManager}
- *
- * Nơi quản lý các thiết lập trong game.
- *
- * @see GameSettings
- */
-public final class SettingsManager {
- private SettingsManager() {}
-
- /**
- * Tải các settings từ file đã thiết lập vào CTDL.
- *
- * Chú ý:
- *
- *
- * Cần load {@link LaunchOptions#load(String...)} trước khi tải.
- *
- *
- * @param target Nơi tải vào
- * @throws IOException if an error occurred when reading from the input stream.
- */
- public static void load(GameSettings target) throws IOException {
- final var gameSettings = Utilities.IO.loadProperties("/settings.properties");
- final var userSettings = UserSetting.getSetting();
-
- // ? General
- target.setTitle(gameSettings.getProperty("general.name"));
- target.setVersion(gameSettings.getProperty("general.version"));
- target.setCredits(Utilities.IO.readTextFile("credits.txt"));
- target.setApplicationMode(
- Boolean.parseBoolean(gameSettings.getProperty("general.devMode"))
- ? ApplicationMode.DEVELOPER
- : (LaunchOptions.isDebug())
- ? ApplicationMode.DEBUG
- : ApplicationMode.RELEASE);
-
- // ? Display
- target.setWidth(userSettings.getVideo().getWidth());
- target.setHeight(userSettings.getVideo().getHeight());
- target.setFullScreenAllowed(true);
-
- // ? In-game
- target.setSceneFactory(new SceneFactory());
- target.setMainMenuEnabled(true);
- target.setIntroEnabled(true);
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/core/UserSetting.java b/src/main/java/com/github/codestorm/bounceverse/core/UserSetting.java
deleted file mode 100644
index 097d63f..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/core/UserSetting.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package com.github.codestorm.bounceverse.core;
-
-import com.almasb.fxgl.logging.Logger;
-import com.moandjiezana.toml.Toml;
-import com.moandjiezana.toml.TomlWriter;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-/**
- *
- *
- * {@link UserSetting}
- *
- * Thiết lập game của người chơi.
- */
-public final class UserSetting {
- private static final String FORMAT = "toml";
- private Video video = new Video();
-
- private UserSetting() {}
-
- /**
- * Lấy tên file setting tương ứng của người chơi.
- *
- * @return Tên file
- */
- public static String getFilename() {
- final String username = System.getProperty("user.name");
- return String.format("config.%s.%s", username, FORMAT);
- }
-
- /**
- * Lấy địa chỉ file setting tương ứng của người chơi.
- *
- * @return Địa chỉ file setting tuyệt đối
- */
- public static Path getFilepath() {
- return Paths.get(getFilename()).toAbsolutePath();
- }
-
- /**
- * Lấy thiết lập game của người chơi.
- *
- * @return Thiết lập game của người chơi
- */
- public static UserSetting getSetting() {
- final var filepath = getFilepath();
- try {
- final var file = new Toml().read(filepath.toString());
- return file.to(UserSetting.class);
- } catch (IllegalStateException e) {
- Logger.get(UserSetting.class)
- .warning(
- "Using default config because the user config file cannot be opened: "
- + filepath,
- e);
- return new UserSetting();
- }
- }
-
- /**
- * Lưu thiết lập game thành file.
- *
- * @throws IOException if any file operations fail
- */
- public void save() throws IOException {
- final var filepath = getFilepath();
- TomlWriter writer = new TomlWriter();
- writer.write(this, new File(filepath.toUri()));
- Logger.get(UserSetting.class).infof("Saved user config to: %s", filepath);
- }
-
- public Video getVideo() {
- return video;
- }
-
- public void setVideo(Video video) {
- this.video = video;
- }
-
- /** Setting về Hình ảnh. */
- public static final class Video {
- private int width = 1024;
- private int height = 768;
-
- private Video() {}
-
- public int getWidth() {
- return width;
- }
-
- public void setWidth(int width) {
- this.width = width;
- }
-
- public int getHeight() {
- return height;
- }
-
- public void setHeight(int height) {
- this.height = height;
- }
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/core/systems/GameSystem.java b/src/main/java/com/github/codestorm/bounceverse/core/systems/GameSystem.java
deleted file mode 100644
index 9ed5117..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/core/systems/GameSystem.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.github.codestorm.bounceverse.core.systems;
-
-import com.almasb.fxgl.dsl.FXGL;
-import com.almasb.fxgl.entity.EntityFactory;
-import com.github.codestorm.bounceverse.factory.entities.*;
-
-public final class GameSystem extends System {
- private GameSystem() {}
-
- public static GameSystem getInstance() {
- return GameSystem.Holder.INSTANCE;
- }
-
- private static void addFactory(EntityFactory... factories) {
- for (var factory : factories) {
- FXGL.getGameWorld().addEntityFactory(factory);
- }
- }
-
- private static void spawnWalls() {
- FXGL.spawn("wallTop");
- FXGL.spawn("wallBottom");
- FXGL.spawn("wallLeft");
- FXGL.spawn("wallRight");
- }
-
- @Override
- public void apply() {
- addFactory(
- new WallFactory(),
- new BrickFactory(),
- new BulletFactory(),
- new PaddleFactory(),
- new BallFactory());
-
- // Wall
- spawnWalls();
-
- // Brick
- for (int y = 1; y <= 6; y++) {
- for (int x = 1; x <= 10; x++) {
- FXGL.spawn("normalBrick", 85 * x, 35 * y);
- }
- }
-
- // Paddle
- double px = FXGL.getAppWidth() / 2.0 - 60;
- double py = FXGL.getAppHeight() - 40;
- FXGL.spawn("paddle", px, py);
-
- // Ball
- FXGL.spawn("ball");
- }
-
- /**
- * Lazy-loaded singleton holder.
- * Follow
- * Initialization-on-demand holder idiom .
- */
- private static final class Holder {
- static final GameSystem INSTANCE = new GameSystem();
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/core/systems/InputSystem.java b/src/main/java/com/github/codestorm/bounceverse/core/systems/InputSystem.java
deleted file mode 100644
index b7f0f5c..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/core/systems/InputSystem.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.github.codestorm.bounceverse.core.systems;
-
-import com.almasb.fxgl.dsl.FXGL;
-import com.almasb.fxgl.input.UserAction;
-import com.github.codestorm.bounceverse.typing.enums.DirectionUnit;
-import com.github.codestorm.bounceverse.typing.enums.EntityType;
-import javafx.scene.input.KeyCode;
-
-/**
- *
- *
- * {@link InputSystem}
- *
- * {@link System} quản lý Input trong game.
- * Đây là một Singleton, cần lấy instance thông qua {@link #getInstance()} .
- */
-public class InputSystem extends System {
- public static InputSystem getInstance() {
- return InputSystem.Holder.INSTANCE;
- }
-
- @Override
- public void apply() {
- FXGL.getInput()
- .addAction(
- new UserAction("Move Left") {
- @Override
- protected void onAction() {
- FXGL.getGameWorld()
- .getEntitiesByType(EntityType.PADDLE)
- .forEach(
- e ->
- e.translate(
- DirectionUnit.LEFT
- .getVector()
- .mul(10)));
- }
- },
- KeyCode.LEFT);
-
- FXGL.getInput()
- .addAction(
- new UserAction("Move Right") {
- @Override
- protected void onAction() {
- FXGL.getGameWorld()
- .getEntitiesByType(EntityType.PADDLE)
- .forEach(
- e ->
- e.translate(
- DirectionUnit.RIGHT
- .getVector()
- .mul(10)));
- }
- },
- KeyCode.RIGHT);
- }
-
- /**
- * Lazy-loaded singleton holder.
- * Follow
- * Initialization-on-demand holder idiom .
- */
- private static final class Holder {
- static final InputSystem INSTANCE = new InputSystem();
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/core/systems/PhysicSystem.java b/src/main/java/com/github/codestorm/bounceverse/core/systems/PhysicSystem.java
deleted file mode 100644
index f51b326..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/core/systems/PhysicSystem.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package com.github.codestorm.bounceverse.core.systems;
-
-import com.almasb.fxgl.dsl.FXGL;
-import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.physics.CollisionHandler;
-import com.almasb.fxgl.physics.PhysicsComponent;
-import com.github.codestorm.bounceverse.Utilities;
-import com.github.codestorm.bounceverse.components.behaviors.Attack;
-import com.github.codestorm.bounceverse.typing.enums.EntityType;
-import java.util.List;
-
-/**
- *
- *
- * {@link PhysicSystem}
- *
- * {@link System} quản lý Vật lý trong game.
- * Đây là một Singleton, cần lấy instance thông qua {@link #getInstance()} .
- *
- * @see System
- */
-public final class PhysicSystem extends System {
- private PhysicSystem() {}
-
- public static PhysicSystem getInstance() {
- return Holder.INSTANCE;
- }
-
- @Override
- public void apply() {
- final var physicWorld = FXGL.getPhysicsWorld();
- physicWorld.setGravity(0, 0);
-
- // Bullet vs Brick
- physicWorld.addCollisionHandler(
- new CollisionHandler(EntityType.BULLET, EntityType.BRICK) {
- @Override
- protected void onCollisionBegin(Entity bullet, Entity brick) {
- final var atk = bullet.getComponentOptional(Attack.class);
- if (atk.isEmpty()) {
- return;
- }
- atk.get().execute(List.of(brick));
- }
- });
-
- // Ball vs Brick
- physicWorld.addCollisionHandler(
- new CollisionHandler(EntityType.BALL, EntityType.BRICK) {
- @Override
- protected void onCollisionBegin(Entity ball, Entity brick) {
- // collision
- final var collisionDirection =
- Utilities.Collision.getCollisionDirection(ball, brick);
-
- final var physics = ball.getComponent(PhysicsComponent.class);
- switch (collisionDirection) {
- case UP, DOWN ->
- physics.setLinearVelocity(
- physics.getVelocityX(), -physics.getVelocityY());
- case LEFT, RIGHT ->
- physics.setLinearVelocity(
- -physics.getVelocityX(), physics.getVelocityY());
- }
-
- // damage
- final var atk = ball.getComponent(Attack.class);
- atk.execute(List.of(brick));
- }
- });
-
- // Ball vs Paddle
- physicWorld.addCollisionHandler(
- new CollisionHandler(EntityType.BALL, EntityType.PADDLE) {
- @Override
- protected void onCollisionBegin(Entity ball, Entity paddle) {
- final var collisionDirection =
- Utilities.Collision.getCollisionDirection(ball, paddle);
-
- final var physics = ball.getComponent(PhysicsComponent.class);
- switch (collisionDirection) {
- case UP, DOWN ->
- physics.setLinearVelocity(
- physics.getVelocityX(), -physics.getVelocityY());
- case LEFT, RIGHT ->
- physics.setLinearVelocity(
- -physics.getVelocityX(), physics.getVelocityY());
- }
- }
- });
-
- // Ball vs Wall
- physicWorld.addCollisionHandler(
- new CollisionHandler(EntityType.BALL, EntityType.WALL) {
- @Override
- protected void onCollisionBegin(Entity ball, Entity wall) {
- final var collisionDirection =
- Utilities.Collision.getCollisionDirection(ball, wall);
-
- final var physics = ball.getComponent(PhysicsComponent.class);
- switch (collisionDirection) {
- case UP, DOWN ->
- physics.setLinearVelocity(
- physics.getVelocityX(), -physics.getVelocityY());
- case LEFT, RIGHT ->
- physics.setLinearVelocity(
- -physics.getVelocityX(), physics.getVelocityY());
- }
- }
- });
- }
-
- /**
- * Lazy-loaded singleton holder.
- * Follow
- * Initialization-on-demand holder idiom .
- */
- private static final class Holder {
- static final PhysicSystem INSTANCE = new PhysicSystem();
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/core/systems/UISystem.java b/src/main/java/com/github/codestorm/bounceverse/core/systems/UISystem.java
deleted file mode 100644
index ca92148..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/core/systems/UISystem.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.github.codestorm.bounceverse.core.systems;
-
-import com.almasb.fxgl.dsl.FXGL;
-import javafx.scene.paint.Color;
-
-/**
- *
- *
- * {@link UISystem}
- *
- * {@link System} quản lý Giao diện trong game.
- * Đây là một Singleton, cần lấy instance thông qua {@link #getInstance()} .
- */
-public final class UISystem extends System {
- public static UISystem getInstance() {
- return UISystem.Holder.INSTANCE;
- }
-
- @Override
- public void apply() {
- FXGL.getGameScene().setBackgroundColor(Color.web("#2B2B2B"));
- }
-
- /**
- * Lazy-loaded singleton holder.
- * Follow
- * Initialization-on-demand holder idiom .
- */
- private static final class Holder {
- static final UISystem INSTANCE = new UISystem();
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/factory/SceneFactory.java b/src/main/java/com/github/codestorm/bounceverse/factory/SceneFactory.java
index 3cb01bf..f5afb11 100644
--- a/src/main/java/com/github/codestorm/bounceverse/factory/SceneFactory.java
+++ b/src/main/java/com/github/codestorm/bounceverse/factory/SceneFactory.java
@@ -1,8 +1,12 @@
package com.github.codestorm.bounceverse.factory;
import com.almasb.fxgl.app.scene.FXGLMenu;
+import com.almasb.fxgl.app.scene.IntroScene;
+import com.almasb.fxgl.app.scene.MenuType;
import com.almasb.fxgl.scene.Scene;
-import com.github.codestorm.bounceverse.scenes.MainMenu;
+import com.github.codestorm.bounceverse.scenes.Intro;
+import com.github.codestorm.bounceverse.scenes.Menu;
+
import org.jetbrains.annotations.NotNull;
/**
@@ -17,6 +21,11 @@
public final class SceneFactory extends com.almasb.fxgl.app.scene.SceneFactory {
@NotNull @Override
public FXGLMenu newMainMenu() {
- return new MainMenu();
+ return new Menu(MenuType.MAIN_MENU);
+ }
+
+ @NotNull @Override
+ public IntroScene newIntro() {
+ return new Intro();
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/factory/entities/BallFactory.java b/src/main/java/com/github/codestorm/bounceverse/factory/entities/BallFactory.java
index 6a152ce..48dfef9 100644
--- a/src/main/java/com/github/codestorm/bounceverse/factory/entities/BallFactory.java
+++ b/src/main/java/com/github/codestorm/bounceverse/factory/entities/BallFactory.java
@@ -1,70 +1,100 @@
package com.github.codestorm.bounceverse.factory.entities;
+import com.almasb.fxgl.dsl.EntityBuilder;
import com.almasb.fxgl.dsl.FXGL;
import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.EntityFactory;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.entity.Spawns;
import com.almasb.fxgl.physics.PhysicsComponent;
import com.almasb.fxgl.physics.box2d.dynamics.BodyType;
import com.almasb.fxgl.physics.box2d.dynamics.FixtureDef;
+import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.components.behaviors.Attachment;
import com.github.codestorm.bounceverse.components.behaviors.Attack;
import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
import javafx.geometry.Point2D;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
-/**
- *
- *
- * {@link BallFactory}
- *
- * This class defines and spawns a new {@link EntityType#BALL} entity in the game world.
- *
- *
By default, the spawned ball has:
- *
- *
- * Radius = {@link #DEFAULT_RADIUS}
- * Position: {@link #DEFAULT_POS}
- * Color: {@link #DEFAULT_COLOR}
- *
- *
- * @author minngoc1213
- */
-public final class BallFactory implements EntityFactory {
- public static final int DEFAULT_RADIUS = 10;
- public static final Point2D DEFAULT_POS = new Point2D(400, 500);
+/** Factory tạo bóng trong trò chơi. Hỗ trợ spawn bóng gắn (attached) hoặc tự do (free). */
+public final class BallFactory extends EntityFactory {
+
+ public static final double DEFAULT_RADIUS = 10;
public static final Color DEFAULT_COLOR = Color.RED;
- @Spawns("ball")
- public Entity spawnBall(SpawnData data) {
- PhysicsComponent physics = new PhysicsComponent();
+ @Override
+ protected EntityBuilder getBuilder(SpawnData data) {
+ final boolean attached = Utilities.Typing.getOr(data, "attached", false);
+ var physics = new PhysicsComponent();
var fixture = new FixtureDef();
fixture.setDensity(1.0f);
- fixture.setFriction(0f);
- fixture.setRestitution(1f);
-
+ fixture.setFriction(0.0f);
+ fixture.setRestitution(1.0f);
physics.setFixtureDef(fixture);
physics.setBodyType(BodyType.DYNAMIC);
- // set ball doesn't rotate
physics.setOnPhysicsInitialized(
() -> {
- physics.setLinearVelocity(200, 200);
- physics.setAngularVelocity(0);
+ physics.getBody().setGravityScale(0f);
physics.getBody().setFixedRotation(true);
physics.getBody().setLinearDamping(0f);
physics.getBody().setAngularDamping(0f);
+ physics.getBody().setBullet(true);
+
+ if (attached) {
+ physics.setLinearVelocity(Point2D.ZERO);
+ } else {
+ var initialSpeed = FXGL.getd("ballSpeed");
+ var initialVelocity = new Point2D(1, -1).normalize().multiply(initialSpeed);
+ physics.setLinearVelocity(initialVelocity);
+ }
});
- return FXGL.entityBuilder(data)
- .type(EntityType.BALL)
- .at(DEFAULT_POS)
- .viewWithBBox(new Circle(DEFAULT_RADIUS, DEFAULT_COLOR))
- .collidable()
- .with(physics, new Attack())
- .anchorFromCenter()
- .buildAndAttach();
+ var builder =
+ FXGL.entityBuilder(data)
+ .type(EntityType.BALL)
+ .viewWithBBox(new Circle(DEFAULT_RADIUS, DEFAULT_COLOR))
+ .collidable()
+ .with(physics, new Attack());
+
+ if (attached) {
+ builder.with(new Attachment());
+ }
+
+ return builder;
+ }
+
+ @Spawns("ball")
+ public Entity spawnBall(SpawnData data) {
+ final var attached = data.hasKey("attached") && Boolean.TRUE.equals(data.get("attached"));
+
+ Point2D pos = null;
+ if (data.hasKey("x") && data.hasKey("y")) {
+ pos = new Point2D(data.getX(), data.getY());
+ } else if (data.hasKey("position")) {
+ pos = data.get("position");
+ }
+
+ if (pos == null) {
+ var paddleOpt =
+ FXGL.getGameWorld().getEntitiesByType(EntityType.PADDLE).stream().findFirst();
+ if (paddleOpt.isPresent()) {
+ var paddle = paddleOpt.get();
+ pos =
+ new Point2D(
+ paddle.getCenter().getX() - DEFAULT_RADIUS,
+ paddle.getY() - DEFAULT_RADIUS * 2);
+ } else {
+ // Fallback an toàn nếu không tìm thấy paddle
+ pos = new Point2D(FXGL.getAppWidth() / 2.0, FXGL.getAppHeight() / 2.0);
+ }
+ }
+
+ data.put("attached", attached);
+ data.put("x", pos.getX());
+ data.put("y", pos.getY());
+ return getBuilder(data).buildAndAttach();
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/factory/entities/BrickFactory.java b/src/main/java/com/github/codestorm/bounceverse/factory/entities/BrickFactory.java
index 0c7cf19..e3aad1a 100644
--- a/src/main/java/com/github/codestorm/bounceverse/factory/entities/BrickFactory.java
+++ b/src/main/java/com/github/codestorm/bounceverse/factory/entities/BrickFactory.java
@@ -1,93 +1,200 @@
package com.github.codestorm.bounceverse.factory.entities;
+import com.almasb.fxgl.dsl.EntityBuilder;
import com.almasb.fxgl.dsl.FXGL;
import com.almasb.fxgl.dsl.components.HealthIntComponent;
import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.EntityFactory;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.entity.Spawns;
-import com.almasb.fxgl.entity.component.Component;
+import com.almasb.fxgl.physics.BoundingShape;
import com.almasb.fxgl.physics.PhysicsComponent;
import com.almasb.fxgl.physics.box2d.dynamics.BodyType;
import com.almasb.fxgl.physics.box2d.dynamics.FixtureDef;
+import com.github.codestorm.bounceverse.AssetsPath;
import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.components.behaviors.Explosion;
import com.github.codestorm.bounceverse.components.behaviors.HealthDeath;
-import com.github.codestorm.bounceverse.components.behaviors.paddle.PaddleShooting;
+import com.github.codestorm.bounceverse.components.behaviors.Special;
+import com.github.codestorm.bounceverse.components.behaviors.brick.StrongBrickTextureUpdater;
+import com.github.codestorm.bounceverse.components.properties.Attributes;
+import com.github.codestorm.bounceverse.components.properties.Shield;
+import com.github.codestorm.bounceverse.components.properties.brick.BrickTextureManager;
+import com.github.codestorm.bounceverse.typing.enums.BrickType;
+import com.github.codestorm.bounceverse.typing.enums.CollisionGroup;
import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
import javafx.geometry.Point2D;
+import javafx.geometry.Side;
import javafx.scene.paint.Color;
-import javafx.scene.shape.Rectangle;
-import javafx.util.Duration;
-import org.jetbrains.annotations.NotNull;
-
-/**
- *
- *
- * {@link BrickFactory}
- *
- *
- * Factory để tạo các entity loại {@link EntityType#BRICK} trong trò chơi.
- *
- * @see EntityFactory
- */
-public final class BrickFactory implements EntityFactory {
+
+import java.util.List;
+import java.util.Random;
+
+public final class BrickFactory extends EntityFactory {
+
private static final int DEFAULT_WIDTH = 80;
private static final int DEFAULT_HEIGHT = 30;
- private static final Color DEFAULT_COLOR = Color.LIGHTBLUE;
private static final int DEFAULT_HP = 1;
+ private static final Random RANDOM = new Random();
+ private static final List COLORS =
+ List.of("blue", "green", "orange", "pink", "red", "yellow");
+
+ private String getColorName(Color color) {
+ if (color.equals(Color.BLUE)) {
+ return "blue";
+ }
+ if (color.equals(Color.GREEN)) {
+ return "green";
+ }
+ if (color.equals(Color.ORANGE)) {
+ return "orange";
+ }
+ if (color.equals(Color.PINK)) {
+ return "pink";
+ }
+ if (color.equals(Color.RED)) {
+ return "red";
+ }
+ if (color.equals(Color.YELLOW)) {
+ return "yellow";
+ }
+ return "blue";
+ }
+
+ // Phương thức trợ giúp để lấy Color object từ SpawnData
+ private Color getSpawnColor(SpawnData data) {
+ var colorValue = data.get("color");
+ if (colorValue instanceof Color) {
+ return (Color) colorValue;
+ } else if (colorValue instanceof String) {
+ return switch ((String) colorValue) {
+ case "green" -> Color.GREEN;
+ case "orange" -> Color.ORANGE;
+ case "pink" -> Color.PINK;
+ case "red" -> Color.RED;
+ case "yellow" -> Color.YELLOW;
+ default -> Color.BLUE;
+ };
+ }
+ return Color.BLUE; // Mặc định
+ }
+
+ @Override
+ protected EntityBuilder getBuilder(SpawnData data) {
+ var hp = ((Number) Utilities.Typing.getOr(data, "hp", DEFAULT_HP)).intValue();
+ var width = ((Number) Utilities.Typing.getOr(data, "width", DEFAULT_WIDTH)).intValue();
+ var height = ((Number) Utilities.Typing.getOr(data, "height", DEFAULT_HEIGHT)).intValue();
+
+ Point2D pos = data.hasKey("pos") ? data.get("pos") : new Point2D(data.getX(), data.getY());
- /**
- * Tạo mới một entity brick.
- *
- * @param pos Vị trí
- * @param hp HP
- * @param view Khung nhìn
- * @param components Các components thêm vào
- * @return Entity Brick mới tạo
- */
- @NotNull private static Entity newBrick(Point2D pos, int hp, Rectangle view, Component... components) {
- Utilities.Compatibility.throwIfNotCompatible(EntityType.BRICK, components);
+ // CHỈ LẤY STRING KEY: Đảm bảo chỉ làm việc với String ở đây
+ String colorKey;
+ var colorValue = data.get("color");
+
+ if (colorValue instanceof Color) {
+ colorKey = getColorName((Color) colorValue); // Chuyển Color object thành String key
+ } else if (colorValue instanceof String) {
+ colorKey = (String) colorValue;
+ } else {
+ colorKey = COLORS.get(RANDOM.nextInt(COLORS.size()));
+ }
+
+ var colorAsset = AssetsPath.Textures.Bricks.COLORS.get(colorKey);
+ if (colorAsset == null) {
+ colorAsset =
+ AssetsPath.Textures.Bricks.COLORS.values().stream()
+ .findFirst()
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ "No ColorAssets available for bricks"));
+ }
+
+ var type = Utilities.Typing.getOr(data, "type", BrickType.NORMAL);
+ var texturePath = colorAsset.getTexture(type, 1.0);
+ var texture = FXGL.texture(texturePath);
+ texture.setFitWidth(width);
+ texture.setFitHeight(height);
var physics = new PhysicsComponent();
var fixture = new FixtureDef();
fixture.setFriction(0f);
fixture.setRestitution(1f);
-
+ fixture.getFilter().categoryBits = CollisionGroup.BRICK.bits;
+ fixture.getFilter().maskBits = CollisionGroup.BALL.bits | CollisionGroup.BULLET.bits;
physics.setFixtureDef(fixture);
physics.setBodyType(BodyType.STATIC);
- return FXGL.entityBuilder()
+ return FXGL.entityBuilder(data)
.type(EntityType.BRICK)
+ .bbox(BoundingShape.box(width, height))
+ .viewWithBBox(texture)
.at(pos)
- .viewWithBBox(view)
.collidable()
- .with(physics, new HealthIntComponent(hp), new HealthDeath())
- .with(components)
- .build();
+ .with(physics, new Attributes(), new HealthIntComponent(hp), new HealthDeath());
+ }
+
+ @Spawns("normalBrick")
+ public Entity newNormalBrick(SpawnData data) {
+ data.put("type", BrickType.NORMAL);
+ data.put("hp", (double) DEFAULT_HP);
+ var color = getSpawnColor(data);
+ data.put("color", getColorName(color));
+ return getBuilder(data)
+ .with(new BrickTextureManager(BrickType.NORMAL, color))
+ .buildAndAttach();
+ }
+
+ @Spawns("strongBrick")
+ public Entity newStrongBrick(SpawnData data) {
+ data.put("type", BrickType.STRONG);
+ data.put("hp", DEFAULT_HP + 2);
+ var color = getSpawnColor(data);
+ data.put("color", getColorName(color));
+ return getBuilder(data)
+ .with(new StrongBrickTextureUpdater().withColor(color))
+ .with(new BrickTextureManager(BrickType.STRONG, color))
+ .buildAndAttach();
+ }
+
+ @Spawns("shieldBrick")
+ public Entity newShieldBrick(SpawnData data) {
+ data.put("type", BrickType.SHIELD);
+ data.put("hp", (double) (DEFAULT_HP));
+ var shield = new Shield(Side.LEFT, Side.RIGHT, Side.BOTTOM);
+ var color = getSpawnColor(data);
+ data.put("color", getColorName(color));
+ return getBuilder(data)
+ .with(shield)
+ .with(new BrickTextureManager(BrickType.SHIELD, color))
+ .buildAndAttach();
}
- /**
- * Tạo mới một entity brick với khung nhìn mặc định.
- *
- * @param pos Vị trí
- * @param hp HP
- * @param components Các components thêm vào
- * @return Entity Brick mới tạo
- */
- @NotNull private static Entity newBrick(Point2D pos, int hp, Component... components) {
- return newBrick(
- pos, hp, new Rectangle(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR), components);
+ @Spawns("explodingBrick")
+ public Entity newExplodingBrick(SpawnData data) {
+ data.put("type", BrickType.EXPLODING);
+ data.put("hp", (double) DEFAULT_HP);
+ var explosionWidth = DEFAULT_WIDTH * 1.5;
+ var explosionHeight = DEFAULT_HEIGHT * 2.5;
+ var explosion = new Explosion(explosionWidth, explosionHeight);
+ var color = getSpawnColor(data);
+ data.put("color", getColorName(color));
+ return getBuilder(data)
+ .with(explosion)
+ .with(new BrickTextureManager(BrickType.EXPLODING, color))
+ .buildAndAttach();
}
- /**
- * Tạo entity Brick bình thường.
- *
- * @param pos Vị trí
- * @return Entity Brick mới tạo
- */
- @NotNull @Spawns("normalBrick")
- public static Entity newNormalBrick(SpawnData pos) {
- return newBrick(
- new Point2D(pos.getX(), pos.getY()), DEFAULT_HP, new PaddleShooting(Duration.ONE));
+ @Spawns("keyBrick")
+ public Entity newKeyBrick(SpawnData data) {
+ data.put("type", BrickType.KEY);
+ data.put("hp", (double) DEFAULT_HP);
+ var special = new Special();
+ var color = getSpawnColor(data);
+ data.put("color", getColorName(color));
+ return getBuilder(data)
+ .with(special)
+ .with(new BrickTextureManager(BrickType.KEY, color))
+ .buildAndAttach();
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/factory/entities/BulletFactory.java b/src/main/java/com/github/codestorm/bounceverse/factory/entities/BulletFactory.java
index 60aa51e..cd4d656 100644
--- a/src/main/java/com/github/codestorm/bounceverse/factory/entities/BulletFactory.java
+++ b/src/main/java/com/github/codestorm/bounceverse/factory/entities/BulletFactory.java
@@ -3,35 +3,60 @@
import static com.almasb.fxgl.dsl.FXGL.entityBuilder;
import com.almasb.fxgl.core.math.Vec2;
+import com.almasb.fxgl.dsl.EntityBuilder;
import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.EntityFactory;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.entity.Spawns;
import com.almasb.fxgl.physics.PhysicsComponent;
+import com.almasb.fxgl.physics.box2d.dynamics.BodyType; // Thêm import này
+import com.almasb.fxgl.physics.box2d.dynamics.FixtureDef;
+import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.components.behaviors.Attack;
+import com.github.codestorm.bounceverse.typing.enums.CollisionGroup;
import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.geometry.Point2D;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
-/**
- *
- *
- * {@link BulletFactory}
- *
- * Factory để tạo các entity loại {@link EntityType#BULLET} trong trò chơi.
- *
- * @see EntityFactory
- */
-public final class BulletFactory implements EntityFactory {
+/** Factory để tạo các entity loại BULLET trong trò chơi. */
+public final class BulletFactory extends EntityFactory {
+ public static final double DEFAULT_RADIUS = 4;
+ public static final Color DEFAULT_COLOR = Color.YELLOW;
+
+ @Override
+ protected EntityBuilder getBuilder(SpawnData data) {
+ var physics = new PhysicsComponent();
+ var fixture = new FixtureDef();
+
+ // Lọc va chạm: Đạn thuộc nhóm BULLET và chỉ va chạm với BRICK, WALL
+ fixture.getFilter().categoryBits = CollisionGroup.BULLET.bits;
+ fixture.getFilter().maskBits = CollisionGroup.BRICK.bits | CollisionGroup.WALL.bits;
+
+ physics.setFixtureDef(fixture);
+ physics.setBodyType(BodyType.DYNAMIC);
+
+ physics.setOnPhysicsInitialized(
+ () -> {
+ Vec2 velocity = data.get("velocity");
+ physics.setLinearVelocity(velocity.toPoint2D());
+ physics.getBody().setGravityScale(0f);
+ physics.getBody().setBullet(true);
+ });
+
+ return entityBuilder(data).type(EntityType.BULLET).collidable().with(physics);
+ }
+
@Spawns("paddleBullet")
public Entity newBullet(SpawnData data) {
- final var physics = new PhysicsComponent();
- physics.setLinearVelocity(((Vec2) data.get("velocity")).toPoint2D());
-
- return entityBuilder(data)
- .type(EntityType.BULLET)
- .viewWithBBox(new Circle(4, Color.YELLOW))
- .collidable()
- .with(physics)
- .build();
+ final var pos = new Point2D(data.getX(), data.getY());
+ final double radius = Utilities.Typing.getOr(data, "radius", DEFAULT_RADIUS);
+ final var color = Utilities.Typing.getOr(data, "color", DEFAULT_COLOR);
+ final int damage = Utilities.Typing.getOr(data, "damage", Attack.DEFAULT_DAMAGE);
+ final var attackComponent = new Attack(damage);
+
+ final var bbox = new Circle(radius, color);
+
+ return getBuilder(data).at(pos).viewWithBBox(bbox).with(attackComponent).buildAndAttach();
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/factory/entities/EntityFactory.java b/src/main/java/com/github/codestorm/bounceverse/factory/entities/EntityFactory.java
new file mode 100644
index 0000000..369bbb0
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/factory/entities/EntityFactory.java
@@ -0,0 +1,23 @@
+package com.github.codestorm.bounceverse.factory.entities;
+
+import com.almasb.fxgl.dsl.EntityBuilder;
+import com.almasb.fxgl.entity.SpawnData;
+
+/**
+ *
+ *
+ * {@link EntityFactory}
+ *
+ * Một Factory chỉ chuyên sản xuất một loại Entity nào đó.
+ *
+ * @see com.almasb.fxgl.entity.EntityFactory
+ */
+abstract class EntityFactory implements com.almasb.fxgl.entity.EntityFactory {
+ /**
+ * Lấy {@link EntityBuilder} chung của entity tương ứng.
+ *
+ * @param data Dữ liệu (chung nhất) phục vụ spawn entity
+ * @return Entity builder
+ */
+ protected abstract EntityBuilder getBuilder(SpawnData data);
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/factory/entities/PaddleFactory.java b/src/main/java/com/github/codestorm/bounceverse/factory/entities/PaddleFactory.java
index 67c1f2f..6616ffa 100644
--- a/src/main/java/com/github/codestorm/bounceverse/factory/entities/PaddleFactory.java
+++ b/src/main/java/com/github/codestorm/bounceverse/factory/entities/PaddleFactory.java
@@ -1,45 +1,57 @@
package com.github.codestorm.bounceverse.factory.entities;
+import com.almasb.fxgl.dsl.EntityBuilder;
import com.almasb.fxgl.dsl.FXGL;
import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.EntityFactory;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.entity.Spawns;
-import com.github.codestorm.bounceverse.components.behaviors.ScaleChange;
+import com.almasb.fxgl.physics.BoundingShape;
+import com.almasb.fxgl.physics.HitBox;
+import com.almasb.fxgl.physics.PhysicsComponent;
+import com.almasb.fxgl.physics.box2d.dynamics.BodyType;
+import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.components.behaviors.Attack;
import com.github.codestorm.bounceverse.components.behaviors.paddle.PaddleShooting;
+import com.github.codestorm.bounceverse.components.properties.paddle.PaddlePowerComponent;
+import com.github.codestorm.bounceverse.components.properties.paddle.PaddleViewManager;
import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
import javafx.scene.paint.Color;
-import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
-/**
- *
- *
- * {@link PaddleFactory}
- *
- * Factory để tạo các entity loại {@link EntityType#PADDLE} trong trò chơi.
- *
- * @see EntityFactory
- */
-public final class PaddleFactory implements EntityFactory {
+/** Factory để tạo các entity loại PADDLE trong trò chơi. */
+public final class PaddleFactory extends EntityFactory {
+
private static final double DEFAULT_WIDTH = 150;
- private static final double DEFAULT_HEIGHT = 20;
+ private static final double DEFAULT_HEIGHT = 40;
private static final double DEFAULT_ARC_WIDTH = 14;
private static final double DEFAULT_ARC_HEIGHT = 14;
+ public static final Color DEFAULT_COLOR = Color.LIGHTBLUE;
private static final Duration DEFAULT_SHOOT_COOLDOWN = Duration.ZERO;
+ @Override
+ protected EntityBuilder getBuilder(SpawnData data) {
+ return FXGL.entityBuilder(data).type(EntityType.PADDLE).collidable();
+ }
+
@Spawns("paddle")
public Entity newPaddle(SpawnData data) {
- Rectangle view = new Rectangle(DEFAULT_WIDTH, DEFAULT_HEIGHT, Color.LIGHTBLUE);
- view.setArcWidth(DEFAULT_ARC_WIDTH);
- view.setArcHeight(DEFAULT_ARC_HEIGHT);
-
- return FXGL.entityBuilder(data)
- .type(EntityType.PADDLE)
- .viewWithBBox(view)
- .collidable()
- .with(new ScaleChange(), new PaddleShooting(DEFAULT_SHOOT_COOLDOWN))
+ final double width = Utilities.Typing.getOr(data, "width", DEFAULT_WIDTH);
+ final double height = Utilities.Typing.getOr(data, "height", DEFAULT_HEIGHT);
+
+ var physics = new PhysicsComponent();
+ physics.setBodyType(BodyType.KINEMATIC);
+ var px = FXGL.getAppWidth() / 2.0 - width / 2.0;
+ var py = FXGL.getAppHeight() - height - 40;
+
+ return getBuilder(data)
+ .at(px, py)
+ .bbox(new HitBox(BoundingShape.box(width, height)))
+ .with(physics)
+ .with(new PaddleShooting(DEFAULT_SHOOT_COOLDOWN))
+ .with(new Attack())
+ .with(new PaddleViewManager())
+ .with(new PaddlePowerComponent())
.build();
- // TODO: Thêm điều chỉnh tốc độ
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/factory/entities/PowerUpFactory.java b/src/main/java/com/github/codestorm/bounceverse/factory/entities/PowerUpFactory.java
new file mode 100644
index 0000000..4628083
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/factory/entities/PowerUpFactory.java
@@ -0,0 +1,72 @@
+package com.github.codestorm.bounceverse.factory.entities;
+
+import com.almasb.fxgl.dsl.EntityBuilder;
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.SpawnData;
+import com.almasb.fxgl.entity.Spawns;
+import com.almasb.fxgl.physics.BoundingShape;
+import com.almasb.fxgl.physics.HitBox;
+import com.almasb.fxgl.texture.Texture;
+import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.components.behaviors.FallingComponent;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpContainer;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.geometry.Point2D;
+
+/** Factory tạo Power-Up entity (rơi thẳng xuống, không dùng physics). */
+public final class PowerUpFactory extends EntityFactory {
+
+ public static final double DEFAULT_RADIUS = 10;
+
+ @Override
+ protected EntityBuilder getBuilder(SpawnData data) {
+ return FXGL.entityBuilder().type(EntityType.POWER_UP).collidable();
+ }
+
+ /**
+ * Tạo mới một PowerUp entity.
+ *
+ * @param data dữ liệu spawn
+ * @return entity PowerUp
+ */
+ @Spawns("powerUp")
+ public Entity newPowerUp(SpawnData data) {
+ final double radius = Utilities.Typing.getOr(data, "radius", DEFAULT_RADIUS);
+
+ // 🔹 Lấy danh sách power-up chứa bên trong (1 hoặc nhiều)
+ final var containsData = data.hasKey("contains") ? data.get("contains") : new PowerUp[0];
+ final var contains =
+ containsData instanceof PowerUp p ? new PowerUp[] {p} : (PowerUp[]) containsData;
+
+ // 🔹 Lấy vị trí spawn (có thể từ pos hoặc x,y)
+ final Point2D pos =
+ data.hasKey("pos") ? data.get("pos") : new Point2D(data.getX(), data.getY());
+
+ // 🔹 Lấy texture (hoặc mặc định)
+ final Texture texture =
+ data.hasKey("texture")
+ ? data.get("texture")
+ : FXGL.texture("power/paddle/Expand Paddle.png");
+
+ // 🔹 Giới hạn kích thước hiển thị
+ texture.setFitWidth(42);
+ texture.setFitHeight(42);
+ texture.setPreserveRatio(true);
+
+ // 🔹 Căn tâm ảnh để khi xoay hoặc va chạm không bị lệch
+ texture.setTranslateX(-texture.getFitWidth() / 2);
+ texture.setTranslateY(-texture.getFitHeight() / 2);
+
+ final var hitbox = new HitBox(BoundingShape.circle(radius));
+
+ return getBuilder(data)
+ .bbox(hitbox)
+ .at(pos)
+ .view(texture)
+ .with(new FallingComponent(), new PowerUpContainer(contains))
+ .buildAndAttach();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/factory/entities/WallFactory.java b/src/main/java/com/github/codestorm/bounceverse/factory/entities/WallFactory.java
index d637c9f..64832c9 100644
--- a/src/main/java/com/github/codestorm/bounceverse/factory/entities/WallFactory.java
+++ b/src/main/java/com/github/codestorm/bounceverse/factory/entities/WallFactory.java
@@ -1,50 +1,75 @@
package com.github.codestorm.bounceverse.factory.entities;
+import com.almasb.fxgl.dsl.EntityBuilder;
import com.almasb.fxgl.dsl.FXGL;
import com.almasb.fxgl.entity.Entity;
-import com.almasb.fxgl.entity.EntityFactory;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.entity.Spawns;
import com.almasb.fxgl.physics.PhysicsComponent;
import com.almasb.fxgl.physics.box2d.dynamics.BodyType;
import com.almasb.fxgl.physics.box2d.dynamics.FixtureDef;
+import com.github.codestorm.bounceverse.Utilities;
import com.github.codestorm.bounceverse.typing.enums.AnchorPoint;
+import com.github.codestorm.bounceverse.typing.enums.CollisionGroup;
import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
import javafx.geometry.Point2D;
+import javafx.geometry.Side;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
-/**
- *
- *
- * {@link WallFactory}
- *
- * This class spawns {@link EntityType#WALL} entity for the game world.
- *
- *
By default, 4 walls top, left, bottom, right will be spawn at the beginning.
- *
- * @author minngoc1213
- */
-public final class WallFactory implements EntityFactory {
+/** Factory tạo 4 tường vật lý bao quanh màn chơi. */
+public final class WallFactory extends EntityFactory {
+
public static final double DEFAULT_THICKNESS = 5;
+ public static final Color DEFAULT_COLOR = Color.GRAY;
+
+ @Override
+ protected EntityBuilder getBuilder(SpawnData data) {
+ var appShape = new Rectangle(FXGL.getAppWidth(), FXGL.getAppHeight());
+ Side side = data.get("side");
+
+ double thickness = Utilities.Typing.getOr(data, "thickness", DEFAULT_THICKNESS);
+ var color = Utilities.Typing.getOr(data, "color", DEFAULT_COLOR);
- /**
- * Create new Entity Wall with ing-game physic.
- *
- * @param pos position
- * @param width width
- * @param height height
- * @return Wall entity at pos
- */
- public static Entity createWall(Point2D pos, double width, double height) {
- Rectangle rect = new Rectangle(width, height, Color.GRAY);
+ Point2D pos;
+ double width, height;
- PhysicsComponent physics = new PhysicsComponent();
+ switch (side) {
+ case TOP -> {
+ pos = AnchorPoint.TOP_LEFT.of(appShape);
+ width = appShape.getWidth();
+ height = thickness;
+ }
+ case BOTTOM -> {
+ pos = AnchorPoint.BOTTOM_LEFT.of(appShape).subtract(0, thickness);
+ width = appShape.getWidth();
+ height = thickness;
+ }
+ case LEFT -> {
+ pos = AnchorPoint.TOP_LEFT.of(appShape);
+ width = thickness;
+ height = appShape.getHeight();
+ }
+ case RIGHT -> {
+ pos = AnchorPoint.TOP_RIGHT.of(appShape).subtract(thickness, 0);
+ width = thickness;
+ height = appShape.getHeight();
+ }
+ default -> throw new IllegalArgumentException("Invalid side for wall: " + side);
+ }
- FixtureDef fixture = new FixtureDef();
+ var rect = new Rectangle(width, height, color);
+ var physics = new PhysicsComponent();
+ var fixture = new FixtureDef();
fixture.setFriction(0f);
fixture.setRestitution(1f);
+ // Lọc va chạm: Tường thuộc nhóm WALL và va chạm với BALL, BULLET, PADDLE
+ fixture.getFilter().categoryBits = CollisionGroup.WALL.bits;
+ fixture.getFilter().maskBits =
+ CollisionGroup.BALL.bits | CollisionGroup.BULLET.bits | CollisionGroup.PADDLE.bits;
+
physics.setFixtureDef(fixture);
physics.setBodyType(BodyType.STATIC);
@@ -54,73 +79,34 @@ public static Entity createWall(Point2D pos, double width, double height) {
.viewWithBBox(rect)
.collidable()
.with(physics)
- .anchorFromCenter()
- .build();
+ .with("side", side);
}
- /**
- * Create Top {@link EntityType#WALL}. `
- *
- * @param data Spawn data
- * @return Top wall
- */
+ /** Tạo tường trên ({@link EntityType#WALL}). */
@Spawns("wallTop")
public Entity newWallTop(SpawnData data) {
- final var appShape = new Rectangle(FXGL.getAppWidth(), FXGL.getAppHeight());
- final double thickness =
- data.hasKey("thickness") ? data.get("thickness") : DEFAULT_THICKNESS;
-
- return createWall(AnchorPoint.TOP_LEFT.of(appShape), appShape.getWidth(), thickness);
+ data.put("side", Side.TOP);
+ return getBuilder(data).buildAndAttach();
}
- /**
- * Create Bottom {@link EntityType#WALL}. `
- *
- * @param data Spawn data
- * @return Botton wall
- */
+ /** Tạo tường dưới ({@link EntityType#WALL}). */
@Spawns("wallBottom")
public Entity newWallBottom(SpawnData data) {
- final var appShape = new Rectangle(FXGL.getAppWidth(), FXGL.getAppHeight());
- final double thickness =
- data.hasKey("thickness") ? data.get("thickness") : DEFAULT_THICKNESS;
-
- return createWall(
- AnchorPoint.BOTTOM_LEFT.of(appShape).subtract(0, thickness),
- appShape.getWidth(),
- thickness);
+ data.put("side", Side.BOTTOM);
+ return getBuilder(data).buildAndAttach();
}
- /**
- * Create Left {@link EntityType#WALL}. `
- *
- * @param data Spawn data
- * @return Left wall
- */
+ /** Tạo tường trái ({@link EntityType#WALL}). */
@Spawns("wallLeft")
public Entity newWallLeft(SpawnData data) {
- final var appShape = new Rectangle(FXGL.getAppWidth(), FXGL.getAppHeight());
- final double thickness =
- data.hasKey("thickness") ? data.get("thickness") : DEFAULT_THICKNESS;
-
- return createWall(AnchorPoint.TOP_LEFT.of(appShape), thickness, appShape.getHeight());
+ data.put("side", Side.LEFT);
+ return getBuilder(data).buildAndAttach();
}
- /**
- * Create Right {@link EntityType#WALL}. `
- *
- * @param data Spawn data
- * @return Left wall
- */
+ /** Tạo tường phải ({@link EntityType#WALL}). */
@Spawns("wallRight")
public Entity newWallRight(SpawnData data) {
- final var appShape = new Rectangle(FXGL.getAppWidth(), FXGL.getAppHeight());
- final double thickness =
- data.hasKey("thickness") ? data.get("thickness") : DEFAULT_THICKNESS;
-
- return createWall(
- AnchorPoint.TOP_RIGHT.of(appShape).subtract(thickness, 0),
- thickness,
- appShape.getHeight());
+ data.put("side", Side.RIGHT);
+ return getBuilder(data).buildAndAttach();
}
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/scenes/Intro.java b/src/main/java/com/github/codestorm/bounceverse/scenes/Intro.java
new file mode 100644
index 0000000..dd19ade
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/scenes/Intro.java
@@ -0,0 +1,101 @@
+package com.github.codestorm.bounceverse.scenes;
+
+import com.almasb.fxgl.app.scene.IntroScene;
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.logging.Logger;
+import com.github.codestorm.bounceverse.AssetsPath;
+
+import javafx.geometry.Pos;
+import javafx.scene.input.MouseButton;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.util.Duration;
+
+/**
+ *
+ *
+ *
{@link Intro}
+ *
+ * Intro của game.
+ *
+ * @see IntroScene
+ */
+public final class Intro extends IntroScene {
+ @Override
+ public void startIntro() {
+ // Background
+ setBackgroundColor(Color.BLACK);
+ setCursorInvisible();
+
+ try {
+ // Tải video
+ final var video = FXGL.getAssetLoader().loadVideo(AssetsPath.Video.INTRO);
+ video.setFitWidth(FXGL.getAppWidth());
+ video.setFitHeight(FXGL.getAppHeight());
+ addChild(video);
+
+ final var player = video.getMediaPlayer();
+
+ // Xử lý khi video kết thúc thành công
+ player.setOnEndOfMedia(
+ () -> {
+ player.dispose();
+ finishIntro();
+ });
+
+ // SỬA ĐỔI: Xử lý khi có lỗi xảy ra
+ player.setOnError(
+ () -> {
+ Logger.get(Intro.class).fatal("Cannot play intro video", player.getError());
+
+ // Hiển thị thông báo lỗi thay vì bỏ qua ngay lập tức
+ showErrorAndExit();
+
+ player.dispose(); // Dọn dẹp media player
+ });
+
+ // Bỏ qua intro khi người dùng click chuột
+ getContentRoot()
+ .setOnMouseClicked(
+ event -> {
+ if (event.getButton() == MouseButton.PRIMARY) {
+ player.getOnEndOfMedia().run();
+ }
+ });
+
+ // Bắt đầu phát khi video đã sẵn sàng
+ player.setOnReady(player::play);
+
+ } catch (Exception e) {
+ // Bắt lỗi nếu ngay cả việc tải video ban đầu cũng thất bại (ví dụ file không tồn tại)
+ Logger.get(Intro.class).fatal("Failed to load intro video asset.", e);
+ }
+ }
+
+ /** Hiển thị một thông báo lỗi trên màn hình trong 3 giây rồi kết thúc intro. */
+ private void showErrorAndExit() {
+ // Xóa các thành phần cũ (nếu có)
+ getContentRoot().getChildren().clear();
+
+ // Tạo văn bản thông báo lỗi
+ var errorText =
+ FXGL.getUIFactoryService()
+ .newText(
+ "Lỗi: Không thể tải video intro.\n"
+ + "Vui lòng kiểm tra file tại\n"
+ + "src/main/resources/assets/videos/intro.mp4",
+ Color.WHITE,
+ 22.0);
+ errorText.setTextAlignment(javafx.scene.text.TextAlignment.CENTER);
+
+ // Đặt văn bản vào giữa màn hình
+ var layout = new StackPane(errorText);
+ layout.setPrefSize(FXGL.getAppWidth(), FXGL.getAppHeight());
+ StackPane.setAlignment(errorText, Pos.CENTER);
+
+ addChild(layout);
+
+ // Tự động kết thúc intro sau 3.5 giây để người dùng kịp đọc
+ FXGL.getGameTimer().runOnceAfter(this::finishIntro, Duration.seconds(3.5));
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/scenes/MainMenu.java b/src/main/java/com/github/codestorm/bounceverse/scenes/MainMenu.java
deleted file mode 100644
index 4e1f726..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/scenes/MainMenu.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.github.codestorm.bounceverse.scenes;
-
-import com.almasb.fxgl.app.scene.FXGLDefaultMenu;
-import com.almasb.fxgl.app.scene.FXGLMenu;
-import com.almasb.fxgl.app.scene.MenuType;
-
-/**
- *
- *
- * {@link MainMenu}
- *
- * {@link FXGLMenu} loại {@link MenuType#MAIN_MENU} được sử dụng trong trò chơi.
- */
-public class MainMenu extends FXGLDefaultMenu {
- public MainMenu() {
- super(MenuType.MAIN_MENU);
- // TODO: Customize Main Menu
- }
-}
diff --git a/src/main/java/com/github/codestorm/bounceverse/scenes/Menu.java b/src/main/java/com/github/codestorm/bounceverse/scenes/Menu.java
new file mode 100644
index 0000000..bf62d1b
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/scenes/Menu.java
@@ -0,0 +1,1095 @@
+/*
+ * FXGL - JavaFX Game Library. The MIT License (MIT).
+ * Copyright (c) AlmasB (almaslvl@gmail.com).
+ * See LICENSE for details.
+ *
+ * Modified by Mai Thành (@thnhmai06), 2025.
+ */
+package com.github.codestorm.bounceverse.scenes;
+
+// ... (Các import giữ nguyên)
+import com.almasb.fxgl.animation.Animation;
+import com.almasb.fxgl.animation.Interpolators;
+import com.almasb.fxgl.app.ApplicationMode;
+import com.almasb.fxgl.app.MenuItem;
+import com.almasb.fxgl.app.scene.FXGLMenu;
+import com.almasb.fxgl.app.scene.MenuType;
+import com.almasb.fxgl.core.math.FXGLMath;
+import com.almasb.fxgl.core.util.InputPredicates;
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.input.Input;
+import com.almasb.fxgl.input.InputModifier;
+import com.almasb.fxgl.input.Trigger;
+import com.almasb.fxgl.input.UserAction;
+import com.almasb.fxgl.input.view.TriggerView;
+import com.almasb.fxgl.localization.Language;
+import com.almasb.fxgl.logging.Logger;
+import com.almasb.fxgl.particle.ParticleEmitters;
+import com.almasb.fxgl.particle.ParticleSystem;
+import com.almasb.fxgl.profile.SaveFile;
+import com.almasb.fxgl.scene.SubScene;
+import com.almasb.fxgl.ui.FXGLScrollPane;
+import com.almasb.fxgl.ui.FontType;
+import com.github.codestorm.bounceverse.systems.manager.metrics.LeaderboardManager;
+
+import javafx.animation.FadeTransition;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.Point2D;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.Tooltip;
+import javafx.scene.effect.BlendMode;
+import javafx.scene.effect.GaussianBlur;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.ColumnConstraints;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.RowConstraints;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.CycleMethod;
+import javafx.scene.paint.LinearGradient;
+import javafx.scene.paint.Paint;
+import javafx.scene.paint.Stop;
+import javafx.scene.shape.Polygon;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Text;
+import javafx.util.Duration;
+import javafx.util.StringConverter;
+
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+
+public class Menu extends FXGLMenu {
+
+ // ... (Các biến thành viên giữ nguyên)
+ private static final Logger log = Logger.get(Menu.class);
+
+ private final ParticleSystem particleSystem = new ParticleSystem();
+
+ private final SimpleObjectProperty titleColor = new SimpleObjectProperty<>(Color.WHITE);
+ private final Pane menuRoot = new Pane();
+ private final Pane menuContentRoot = new Pane();
+ private final MenuContent defaultContent = new MenuContent();
+ private final PressAnyKeyState pressAnyKeyState = new PressAnyKeyState();
+ private final MenuBox menu;
+ private final List> animations = new ArrayList<>();
+ private double t = 0.0;
+
+ public Menu(MenuType type) {
+ super(type);
+
+ if (getAppWidth() < 800 || getAppHeight() < 600) {
+ log.warning("Menu is not designed for resolutions < 800x600");
+ }
+
+ if (type == MenuType.MAIN_MENU) {
+ menu = createMenuBodyMainMenu();
+ } else {
+ menu = createMenuBodyGameMenu();
+ }
+
+ var menuX = 50.0;
+ var menuY = getAppHeight() / 2.0 - menu.getLayoutHeight() / 2.0;
+
+ menuRoot.setTranslateX(menuX);
+ menuRoot.setTranslateY(menuY);
+
+ menuContentRoot.setTranslateX(getAppWidth() - 500.0);
+ menuContentRoot.setTranslateY(menuY);
+
+ initParticles();
+
+ menuRoot.getChildren().add(menu);
+ menuContentRoot.getChildren().add(defaultContent);
+
+ getContentRoot()
+ .getChildren()
+ .addAll(
+ createBackground(getAppWidth(), getAppHeight()),
+ createTitleView(FXGL.getSettings().getTitle()),
+ createVersionView(makeVersionString()),
+ particleSystem.getPane(),
+ menuRoot,
+ menuContentRoot);
+ }
+
+ // ... (Các phương thức khác giữ nguyên cho đến createMenuBodyMainMenu)
+ private void initParticles() {
+ // particle smoke
+ var t = FXGL.texture("particles/smoke.png", 128.0, 128.0).brighter().brighter();
+
+ var emitter = ParticleEmitters.newFireEmitter();
+ emitter.setBlendMode(BlendMode.SRC_OVER);
+ emitter.setSourceImage(t.getImage());
+ emitter.setSize(150.0, 220.0);
+ emitter.setNumParticles(10);
+ emitter.setEmissionRate(0.01);
+ emitter.setVelocityFunction(
+ (v) -> new Point2D(FXGL.random() * 2.5, -FXGL.random() * FXGL.random(80, 120)));
+ emitter.setExpireFunction((v) -> Duration.seconds(FXGL.random(4, 7)));
+ emitter.setScaleFunction((v) -> new Point2D(0.15, 0.10));
+ emitter.setSpawnPointFunction(
+ (v) -> new Point2D(FXGL.random(0.0, getAppWidth() - 200.0), 120.0));
+
+ particleSystem.addParticleEmitter(emitter, 0.0, FXGL.getAppHeight());
+ }
+
+ @Override
+ public void onCreate() {
+ animations.clear();
+
+ var menuBox = (MenuBox) menuRoot.getChildren().getFirst();
+
+ for (var i = 0; i < menuBox.getChildren().size(); i++) {
+ var node = menuBox.getChildren().get(i);
+ node.setTranslateX(-250.0);
+
+ var animation =
+ FXGL.animationBuilder()
+ .delay(Duration.seconds(i * 0.07))
+ .interpolator(Interpolators.EXPONENTIAL.EASE_OUT())
+ .duration(Duration.seconds(0.66))
+ .translate(node)
+ .from(new Point2D(-250.0, 0.0))
+ .to(new Point2D(0.0, 0.0))
+ .build();
+
+ animations.add(animation);
+
+ animation.stop();
+ animation.start();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ // the scene is no longer active so reset everything
+ // so that next time scene is active everything is loaded properly
+ switchMenuTo(menu);
+ switchMenuContentTo(defaultContent);
+ }
+
+ @Override
+ public void onUpdate(double tpf) {
+ if (getType() == MenuType.MAIN_MENU
+ && FXGL.getSettings().isUserProfileEnabled()
+ && "DEFAULT".equals(FXGL.getSettings().getProfileName().get())) {
+ showProfileDialog();
+ }
+
+ animations.forEach((a) -> a.onUpdate(tpf));
+
+ var frequency = 1.7;
+
+ t += tpf * frequency;
+
+ particleSystem.onUpdate(tpf);
+
+ var color = Color.color(1.0, 1.0, 1.0, FXGLMath.noise1D(t));
+ titleColor.set(color);
+ }
+
+ private Rectangle createBackground(double width, double height) {
+ var bg = new Rectangle(width, height);
+ bg.setFill(Color.rgb(10, 1, 1, getType() == MenuType.GAME_MENU ? 0.5 : 1.0));
+ return bg;
+ }
+
+ private StackPane createTitleView(String title) {
+ var text = FXGL.getUIFactoryService().newText(title.substring(0, 1), 50.0);
+ text.setFill(null);
+ text.strokeProperty().bind(titleColor);
+ text.setStrokeWidth(1.5);
+
+ var text2 = FXGL.getUIFactoryService().newText(title.substring(1), 50.0);
+ text2.setFill(null);
+ text2.setStroke(titleColor.get());
+ text2.setStrokeWidth(1.5);
+
+ var textWidth = text.getLayoutBounds().getWidth() + text2.getLayoutBounds().getWidth();
+
+ var border = new Rectangle(textWidth + 30, 65.0, null);
+ border.setStroke(Color.WHITE);
+ border.setStrokeWidth(4.0);
+ border.setArcWidth(25.0);
+ border.setArcHeight(25.0);
+
+ var emitter = ParticleEmitters.newExplosionEmitter(50);
+ emitter.setBlendMode(BlendMode.ADD);
+ emitter.setSourceImage(FXGL.image("particles/trace_horizontal.png", 64.0, 64.0));
+ emitter.setMaxEmissions(Integer.MAX_VALUE);
+ emitter.setSize(18.0, 22.0);
+ emitter.setNumParticles(2);
+ emitter.setEmissionRate(0.2);
+ emitter.setVelocityFunction(
+ i -> {
+ if (i % 2 == 0) {
+ return new Point2D(FXGL.random(-10.0, 0.0), 0.0);
+ } else {
+ return new Point2D(FXGL.random(0.0, 10.0), 0.0);
+ }
+ });
+ emitter.setExpireFunction((v) -> Duration.seconds(FXGL.random(4.0, 6.0)));
+ emitter.setScaleFunction((v) -> new Point2D(-0.03, -0.03));
+ emitter.setSpawnPointFunction((v) -> new Point2D(0, 0));
+
+ var box = new HBox(text, text2);
+ box.setAlignment(Pos.CENTER);
+
+ var titleRoot = new StackPane(border, box);
+ titleRoot.setTranslateX(getAppWidth() / 2.0 - (textWidth + 30) / 2.0);
+ titleRoot.setTranslateY(50.0);
+
+ if (!FXGL.getSettings().isNative()) {
+ particleSystem.addParticleEmitter(
+ emitter,
+ getAppWidth() / 2.0 - 30,
+ titleRoot.getTranslateY() + border.getHeight() - 16);
+ }
+
+ return titleRoot;
+ }
+
+ private Text createVersionView(String version) {
+ var view = FXGL.getUIFactoryService().newText(version);
+ view.setTranslateY(getAppHeight() - 2.0);
+ return view;
+ }
+
+ private MenuBox createMenuBodyMainMenu() {
+ log.debug("createMenuBodyMainMenu()");
+
+ var box = new MenuBox();
+
+ Set enabledItems = FXGL.getSettings().getEnabledMenuItems();
+
+ var itemNewGame = new MenuButton("menu.newGame");
+ itemNewGame.setOnAction(e -> fireNewGame());
+ box.add(itemNewGame);
+
+ // << --- THÊM NÚT LOAD GAME --- >>
+ if (enabledItems.contains(MenuItem.SAVE_LOAD)) {
+ var itemLoad = new MenuButton("menu.load");
+ itemLoad.setMenuContent(this::createContentLoad, false);
+ box.add(itemLoad);
+ }
+
+ var itemOptions = new MenuButton("menu.options");
+ itemOptions.setChild(createOptionsMenu());
+ box.add(itemOptions);
+
+ if (enabledItems.contains(MenuItem.EXTRA)) {
+ var itemExtra = new MenuButton("menu.extra");
+ itemExtra.setChild(createExtraMenu());
+ box.add(itemExtra);
+ }
+
+ var itemExit = new MenuButton("menu.exit");
+ itemExit.setOnAction(e -> fireExit());
+ box.add(itemExit);
+
+ return box;
+ }
+
+ private MenuBox createMenuBodyGameMenu() {
+ log.debug("createMenuBodyGameMenu()");
+
+ var box = new MenuBox();
+
+ var enabledItems = FXGL.getSettings().getEnabledMenuItems();
+
+ var itemResume = new MenuButton("menu.resume");
+ itemResume.setOnAction(e -> fireResume());
+ box.add(itemResume);
+
+ if (enabledItems.contains(MenuItem.SAVE_LOAD)) {
+ var itemSave = new MenuButton("menu.save");
+ itemSave.setOnAction(e -> fireSave());
+
+ var itemLoad = new MenuButton("menu.load");
+ itemLoad.setMenuContent(this::createContentLoad, false);
+
+ box.add(itemSave);
+ box.add(itemLoad);
+ }
+
+ var itemOptions = new MenuButton("menu.options");
+ itemOptions.setChild(createOptionsMenu());
+ box.add(itemOptions);
+
+ if (enabledItems.contains(MenuItem.EXTRA)) {
+ var itemExtra = new MenuButton("menu.extra");
+ itemExtra.setChild(createExtraMenu());
+ box.add(itemExtra);
+ }
+
+ if (FXGL.getSettings().isMainMenuEnabled()) {
+ var itemExit = new MenuButton("menu.mainMenu");
+ itemExit.setOnAction(e -> fireExitToMainMenu());
+ box.add(itemExit);
+ } else {
+ var itemExit = new MenuButton("menu.exit");
+ itemExit.setOnAction(e -> fireExit());
+ box.add(itemExit);
+ }
+
+ return box;
+ }
+
+ private MenuBox createOptionsMenu() {
+ log.debug("createOptionsMenu()");
+
+ // var itemGameplay = new MenuButton("menu.gameplay");
+ // itemGameplay.setMenuContent(this::createContentGameplay);
+ var itemControls = new MenuButton("menu.controls");
+ itemControls.setMenuContent(this::createContentControls);
+
+ var itemVideo = new MenuButton("menu.video");
+ itemVideo.setMenuContent(this::createContentVideo);
+ var itemAudio = new MenuButton("menu.audio");
+ itemAudio.setMenuContent(this::createContentAudio);
+
+ // var btnRestore = createRestoreButton();
+ return new MenuBox(itemControls, itemVideo, itemAudio);
+ }
+
+ private MenuButton createRestoreButton() {
+ var btnRestore = new MenuButton("menu.restore");
+ btnRestore.setOnAction(
+ e ->
+ FXGL.getDialogService()
+ .showConfirmationBox(
+ FXGL.localize("menu.settingsRestore"),
+ yes -> {
+ if (yes) {
+ switchMenuContentTo(defaultContent);
+ // TODO: Restore Default Settings
+ restoreDefaultSettings();
+ }
+ }));
+ return btnRestore;
+ }
+
+ private MenuBox createExtraMenu() {
+ log.debug("createExtraMenu()");
+
+ // var itemAchievements = new MenuButton("menu.trophies");
+ // itemAchievements.setMenuContent(this::createContentAchievements);
+ var itemLeaderboard = new MenuButton("temp.key");
+ itemLeaderboard.btn.textProperty().unbind();
+ itemLeaderboard.btn.setText("LEADERBOARD");
+ itemLeaderboard.setMenuContent(this::createContentLeaderboard);
+
+ var itemCredits = new MenuButton("menu.credits");
+ itemCredits.setMenuContent(this::createContentCredits);
+
+ return new MenuBox(itemLeaderboard, itemCredits);
+ }
+
+ private void switchMenuTo(Node menuNode) {
+ var oldMenu = menuRoot.getChildren().getFirst();
+
+ var ft = new FadeTransition(Duration.seconds(0.33), oldMenu);
+ ft.setToValue(0.0);
+ ft.setOnFinished(
+ e -> {
+ menuNode.setOpacity(0.0);
+ menuRoot.getChildren().set(0, menuNode);
+ oldMenu.setOpacity(1.0);
+
+ var ft2 = new FadeTransition(Duration.seconds(0.33), menuNode);
+ ft2.setToValue(1.0);
+ ft2.play();
+ });
+ ft.play();
+ }
+
+ private void switchMenuContentTo(Node content) {
+ menuContentRoot.getChildren().set(0, content);
+ }
+
+ /**
+ * @return full version string
+ */
+ private String makeVersionString() {
+ return "v"
+ + FXGL.getSettings().getVersion()
+ + (FXGL.getSettings().getApplicationMode() == ApplicationMode.RELEASE
+ ? ""
+ : "-" + FXGL.getSettings().getApplicationMode());
+ }
+
+ protected MenuContent createContentLoad() {
+ log.debug("createContentLoad()");
+
+ ListView list = FXGL.getUIFactoryService().newListView();
+
+ final var FONT_SIZE = 16.0;
+
+ list.setCellFactory(
+ e ->
+ new ListCell<>() {
+ @Override
+ protected void updateItem(SaveFile item, boolean empty) {
+ super.updateItem(item, empty);
+
+ if (empty || item == null) {
+ setText(null);
+ setGraphic(null);
+ } else {
+ var nameDate =
+ String.format(
+ "%-25.25s %s",
+ item.getName(),
+ item.getDateTime()
+ .format(
+ DateTimeFormatter.ofPattern(
+ "dd-MM-yyyy HH-mm")));
+
+ var text =
+ FXGL.getUIFactoryService()
+ .newText(
+ nameDate,
+ Color.WHITE,
+ FontType.MONO,
+ FONT_SIZE);
+
+ setGraphic(text);
+ }
+ }
+ });
+
+ var task =
+ getSaveLoadService()
+ .readSaveFilesTask("./", FXGL.getSettings().getSaveFileExt())
+ .onSuccess(files -> list.getItems().addAll(files));
+
+ FXGL.getTaskService().runAsyncFXWithDialog(task, FXGL.localize("menu.load"));
+
+ list.prefHeightProperty().bind(Bindings.size(list.getItems()).multiply(FONT_SIZE).add(16));
+
+ var btnLoad =
+ FXGL.getUIFactoryService().newButton(FXGL.localizedStringProperty("menu.load"));
+ btnLoad.disableProperty().bind(list.getSelectionModel().selectedItemProperty().isNull());
+
+ btnLoad.setOnAction(
+ e -> {
+ var saveFile = list.getSelectionModel().getSelectedItem();
+ fireLoad(saveFile);
+ });
+
+ var btnDelete =
+ FXGL.getUIFactoryService().newButton(FXGL.localizedStringProperty("menu.delete"));
+ btnDelete.disableProperty().bind(list.getSelectionModel().selectedItemProperty().isNull());
+
+ btnDelete.setOnAction(
+ e -> {
+ var saveFile = list.getSelectionModel().getSelectedItem();
+ fireDelete(saveFile);
+ });
+
+ var hbox = new HBox(50.0, btnLoad, btnDelete);
+ hbox.setAlignment(Pos.CENTER);
+
+ return new MenuContent(list, hbox);
+ }
+
+ protected MenuContent createContentGameplay() {
+ log.debug("createContentGameplay()");
+
+ return new MenuContent();
+ }
+
+ protected MenuContent createContentControls() {
+ log.debug("createContentControls()");
+
+ var grid = new GridPane();
+ grid.setAlignment(Pos.CENTER);
+ grid.setHgap(10.0);
+ grid.setVgap(10.0);
+ grid.setPadding(new Insets(10.0, 10.0, 10.0, 10.0));
+ grid.getColumnConstraints()
+ .add(new ColumnConstraints(200.0, 200.0, 200.0, Priority.ALWAYS, HPos.LEFT, true));
+ grid.getRowConstraints()
+ .add(new RowConstraints(40.0, 40.0, 40.0, Priority.ALWAYS, VPos.CENTER, true));
+
+ grid.setUserData(0);
+
+ for (var e : FXGL.getInput().getAllBindings().entrySet()) {
+ addNewInputBinding(e.getKey(), e.getValue(), grid);
+ }
+
+ var scroll = new FXGLScrollPane(grid);
+ scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
+ scroll.setMaxHeight(getAppHeight() / 2.5);
+
+ var hbox = new HBox(scroll);
+ hbox.setAlignment(Pos.CENTER);
+
+ return new MenuContent(hbox);
+ }
+
+ private void addNewInputBinding(UserAction action, Trigger trigger, GridPane grid) {
+ var actionName = FXGL.getUIFactoryService().newText(action.getName(), Color.WHITE, 18.0);
+
+ var triggerView = new TriggerView(trigger);
+ triggerView.triggerProperty().bind(FXGL.getInput().triggerProperty(action));
+
+ triggerView.setOnMouseClicked(
+ e -> {
+ if (pressAnyKeyState.isActive) {
+ return;
+ }
+
+ pressAnyKeyState.isActive = true;
+ pressAnyKeyState.actionContext = action;
+ FXGL.getSceneService().pushSubScene(pressAnyKeyState);
+ });
+
+ var hBox = new HBox();
+ hBox.setPrefWidth(100.0);
+ hBox.setAlignment(Pos.CENTER);
+ hBox.getChildren().add(triggerView);
+
+ var controlsRow = (int) grid.getUserData();
+ grid.addRow(controlsRow++, actionName, hBox);
+ grid.setUserData(controlsRow);
+ }
+
+ protected MenuContent createContentVideo() {
+ log.debug("createContentVideo()");
+
+ var languageBox =
+ FXGL.getUIFactoryService()
+ .newChoiceBox(
+ FXCollections.observableArrayList(
+ FXGL.getSettings().getSupportedLanguages()));
+ languageBox.setValue(FXGL.getSettings().getLanguage().get());
+ languageBox.setConverter(
+ new StringConverter<>() {
+ @Override
+ public String toString(Language object) {
+ return object.getNativeName();
+ }
+
+ @Override
+ public Language fromString(String string) {
+ return FXGL.getSettings().getSupportedLanguages().stream()
+ .filter(l -> l.getNativeName().equals(string))
+ .findFirst()
+ .orElse(Language.NONE);
+ }
+ });
+
+ FXGL.getSettings().getLanguage().bindBidirectional(languageBox.valueProperty());
+
+ var vbox = new VBox();
+
+ if (FXGL.getSettings().isFullScreenAllowed()) {
+ var cbFullScreen = FXGL.getUIFactoryService().newCheckBox();
+ cbFullScreen.selectedProperty().bindBidirectional(FXGL.getSettings().getFullScreen());
+
+ vbox.getChildren()
+ .add(
+ new HBox(
+ 25.0,
+ FXGL.getUIFactoryService()
+ .newText(FXGL.localize("menu.fullscreen") + ": "),
+ cbFullScreen));
+ }
+
+ return new MenuContent(
+ new HBox(
+ 25.0,
+ FXGL.getUIFactoryService()
+ .newText(FXGL.localizedStringProperty("menu.language").concat(":")),
+ languageBox),
+ vbox);
+ }
+
+ protected MenuContent createContentAudio() {
+ log.debug("createContentAudio()");
+
+ var sliderMusic = FXGL.getUIFactoryService().newSlider();
+ sliderMusic.setMin(0.0);
+ sliderMusic.setMax(1.0);
+ sliderMusic
+ .valueProperty()
+ .bindBidirectional(FXGL.getSettings().globalMusicVolumeProperty());
+
+ var textMusic =
+ FXGL.getUIFactoryService()
+ .newText(FXGL.localizedStringProperty("menu.music.volume").concat(": "));
+ var percentMusic = FXGL.getUIFactoryService().newText("");
+ percentMusic
+ .textProperty()
+ .bind(sliderMusic.valueProperty().multiply(100).asString("%.0f"));
+
+ var sliderSound = FXGL.getUIFactoryService().newSlider();
+ sliderSound.setMin(0.0);
+ sliderSound.setMax(1.0);
+ sliderSound
+ .valueProperty()
+ .bindBidirectional(FXGL.getSettings().globalSoundVolumeProperty());
+
+ var textSound =
+ FXGL.getUIFactoryService()
+ .newText(FXGL.localizedStringProperty("menu.sound.volume").concat(": "));
+ var percentSound = FXGL.getUIFactoryService().newText("");
+ percentSound
+ .textProperty()
+ .bind(sliderSound.valueProperty().multiply(100).asString("%.0f"));
+
+ var gridPane = new GridPane();
+ gridPane.setHgap(15.0);
+ gridPane.addRow(0, textMusic, sliderMusic, percentMusic);
+ gridPane.addRow(1, textSound, sliderSound, percentSound);
+
+ return new MenuContent(gridPane);
+ }
+
+ protected MenuContent createContentCredits() {
+ log.debug("createContentCredits()");
+
+ var pane = new FXGLScrollPane();
+ pane.setPrefWidth(500.0);
+ pane.setPrefHeight(getAppHeight() / 2.0);
+ pane.setStyle("-fx-background:black;");
+
+ var vbox = new VBox();
+ vbox.setAlignment(Pos.CENTER_LEFT);
+ vbox.setPrefWidth(pane.getPrefWidth() - 15);
+
+ var credits = new ArrayList<>(FXGL.getSettings().getCredits());
+ credits.add("");
+ credits.add("Powered by FXGL " + FXGL.getVersion());
+ credits.add("Author: Almas Baimagambetov");
+ credits.add("https://github.com/AlmasB/FXGL");
+ credits.add("");
+
+ for (var credit : credits) {
+ if (credit.length() > 45) {
+ log.warning("Credit color length > 45: " + credit);
+ }
+
+ vbox.getChildren().add(FXGL.getUIFactoryService().newText(credit));
+ }
+
+ pane.setContent(vbox);
+
+ return new MenuContent(pane);
+ }
+
+ protected MenuContent createContentAchievements() {
+ log.debug("createContentAchievements()");
+
+ var content = new MenuContent();
+
+ FXGL.getAchievementService()
+ .getAchievementsCopy()
+ .forEach(
+ a -> {
+ var checkBox = new CheckBox();
+ checkBox.setDisable(true);
+ checkBox.selectedProperty().bind(a.achievedProperty());
+
+ var text = FXGL.getUIFactoryService().newText(a.getName());
+ var tooltip = new Tooltip(a.getDescription());
+ tooltip.setShowDelay(Duration.seconds(0.1));
+
+ Tooltip.install(text, tooltip);
+
+ var box = new HBox(25.0, text, checkBox);
+ box.setAlignment(Pos.CENTER_RIGHT);
+
+ content.getChildren().add(box);
+ });
+
+ return content;
+ }
+
+ protected MenuContent createContentLeaderboard() {
+ log.debug("createContentLeaderboard()");
+
+ var leaderboardManager = LeaderboardManager.getInstance();
+
+ var endlessScores = leaderboardManager.getViewLeaderboard();
+
+ var contentBox = new VBox(20);
+ contentBox.setAlignment(Pos.TOP_CENTER);
+
+ var title =
+ FXGL.getUIFactoryService()
+ .newText("LEADERBOARD", Color.ORANGE, FontType.MONO, 27.0);
+
+ var grid = new GridPane();
+ grid.setAlignment(Pos.TOP_LEFT);
+ grid.setHgap(10);
+ grid.setVgap(10);
+ grid.setPadding(new Insets(10, 10, 10, 10));
+
+ grid.getColumnConstraints()
+ .add(new ColumnConstraints(40, 40, 40, Priority.NEVER, HPos.CENTER, false)); // Rank
+ grid.getColumnConstraints()
+ .add(
+ new ColumnConstraints(
+ 120, 120, 120, Priority.NEVER, HPos.LEFT, false)); // Name
+ grid.getColumnConstraints()
+ .add(new ColumnConstraints(70, 70, 70, Priority.NEVER, HPos.RIGHT, false)); // Score
+ grid.getColumnConstraints()
+ .add(
+ new ColumnConstraints(
+ 50, 50, 50, Priority.NEVER, HPos.CENTER, false)); // Level
+ grid.getColumnConstraints()
+ .add(
+ new ColumnConstraints(
+ 130, 130, 130, Priority.NEVER, HPos.CENTER, false)); // Date
+
+ grid.addRow(
+ 0,
+ FXGL.getUIFactoryService().newText("Rank", Color.NAVAJOWHITE, FontType.MONO, 18.0),
+ FXGL.getUIFactoryService()
+ .newText("Player", Color.NAVAJOWHITE, FontType.MONO, 18.0),
+ FXGL.getUIFactoryService().newText("Score", Color.NAVAJOWHITE, FontType.MONO, 18.0),
+ FXGL.getUIFactoryService().newText("Level", Color.NAVAJOWHITE, FontType.MONO, 18.0),
+ FXGL.getUIFactoryService().newText("Date", Color.NAVAJOWHITE, FontType.MONO, 18.0));
+
+ var rank = 1;
+ if (endlessScores.isEmpty()) {
+ var noDataText =
+ FXGL.getUIFactoryService()
+ .newText("No data available.", Color.GRAY, FontType.UI, 16.0);
+ grid.add(noDataText, 0, 1);
+ GridPane.setColumnSpan(noDataText, 5);
+ } else {
+ for (var score : endlessScores) {
+ var dateStr =
+ score.timestamp()
+ .atZone(java.time.ZoneId.systemDefault())
+ .format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"));
+
+ grid.addRow(
+ rank,
+ FXGL.getUIFactoryService()
+ .newText("#" + rank, Color.YELLOW, FontType.MONO, 16.0),
+ FXGL.getUIFactoryService()
+ .newText(score.name(), Color.CYAN, FontType.MONO, 16.0),
+ FXGL.getUIFactoryService()
+ .newText(
+ String.valueOf(score.score()),
+ Color.LIGHTGREEN,
+ FontType.MONO,
+ 16.0),
+ FXGL.getUIFactoryService()
+ .newText(
+ String.valueOf(score.level()),
+ Color.ORANGE,
+ FontType.MONO,
+ 16.0),
+ FXGL.getUIFactoryService()
+ .newText(dateStr, Color.LIGHTGRAY, FontType.MONO, 16.0));
+ rank++;
+ }
+ }
+
+ var scrollPane = new FXGLScrollPane(grid);
+ scrollPane.setPrefHeight(getAppHeight() / 2.0);
+ scrollPane.setPrefWidth(480);
+ scrollPane.setStyle("-fx-background: black;");
+
+ contentBox.getChildren().addAll(title, scrollPane);
+
+ return new MenuContent(contentBox);
+ }
+
+ private void showProfileDialog() {
+ ChoiceBox profilesBox =
+ FXGL.getUIFactoryService().newChoiceBox(FXCollections.observableArrayList());
+
+ var btnNew =
+ FXGL.getUIFactoryService()
+ .newButton(FXGL.localizedStringProperty("multiplayer.new"));
+ var btnSelect =
+ FXGL.getUIFactoryService()
+ .newButton(FXGL.localizedStringProperty("multiplayer.select"));
+ btnSelect.disableProperty().bind(profilesBox.valueProperty().isNull());
+ var btnDelete =
+ FXGL.getUIFactoryService().newButton(FXGL.localizedStringProperty("menu.delete"));
+ btnDelete.disableProperty().bind(profilesBox.valueProperty().isNull());
+
+ btnNew.setOnAction(
+ e ->
+ FXGL.getDialogService()
+ .showInputBox(
+ FXGL.localize("profile.new"),
+ InputPredicates.ALPHANUM,
+ s -> {
+ // TODO: implement profile creation tasks if needed
+ }));
+
+ btnSelect.setOnAction(
+ e -> {
+ var name = profilesBox.getValue();
+ FXGL.getSettings().getProfileName().set(name);
+ // TODO: load profile details
+ });
+
+ // The rest of profile loading is commented out in original Kotlin
+ }
+
+ private static class MenuBox extends VBox {
+
+ public MenuBox(MenuButton... items) {
+ super();
+ for (var item : items) {
+ add(item);
+ }
+ }
+
+ public double getLayoutHeight() {
+ return 10 * getChildren().size();
+ }
+
+ public void add(MenuButton item) {
+ item.setParent(this);
+ getChildren().addAll(item);
+ }
+ }
+
+ public static class MenuContent extends VBox {
+
+ public int maxW = 0;
+ private Runnable onOpen;
+ private Runnable onClose;
+
+ public MenuContent(Node... items) {
+ super();
+
+ if (items != null && items.length > 0) {
+ maxW = (int) items[0].getLayoutBounds().getWidth();
+
+ for (var n : items) {
+ var w = (int) n.getLayoutBounds().getWidth();
+ if (w > maxW) {
+ maxW = w;
+ }
+ }
+
+ for (var item : items) {
+ getChildren().addAll(item);
+ }
+ }
+
+ sceneProperty()
+ .addListener(
+ (v, oldS, newS) -> {
+ if (newS != null) {
+ onOpen();
+ } else {
+ onClose();
+ }
+ });
+ }
+
+ public void setOnOpen(Runnable onOpenAction) {
+ this.onOpen = onOpenAction;
+ }
+
+ public void setOnClose(Runnable onCloseAction) {
+ this.onClose = onCloseAction;
+ }
+
+ private void onOpen() {
+ if (onOpen != null) {
+ onOpen.run();
+ }
+ }
+
+ private void onClose() {
+ if (onClose != null) {
+ onClose.run();
+ }
+ }
+ }
+
+ private class MenuButton extends Pane {
+
+ public final Button btn;
+ private MenuBox parent;
+ private MenuContent cachedContent;
+ private boolean isAnimating = false;
+
+ public MenuButton(String stringKey) {
+ btn = FXGL.getUIFactoryService().newButton(FXGL.localizedStringProperty(stringKey));
+ btn.setAlignment(Pos.CENTER_LEFT);
+ btn.setStyle("-fx-background-color: transparent");
+
+ btn.setPrefWidth(280);
+
+ final var p = new Polygon(0.0, 0.0, 270.0, 0.0, 300.0, 35.0, 0.0, 35.0);
+ p.setMouseTransparent(true);
+
+ var g =
+ new LinearGradient(
+ 0.0,
+ 1.0,
+ 1.0,
+ 0.2,
+ true,
+ CycleMethod.NO_CYCLE,
+ new Stop(0.6, Color.color(1.0, 0.8, 0.0, 0.34)),
+ new Stop(0.85, Color.color(1.0, 0.8, 0.0, 0.74)),
+ new Stop(1.0, Color.WHITE));
+
+ p.fillProperty()
+ .bind(
+ Bindings.when(btn.pressedProperty())
+ .then((Paint) Color.color(1.0, 0.8, 0.0, 0.75))
+ .otherwise(g));
+
+ p.setStroke(Color.color(0.1, 0.1, 0.1, 0.15));
+
+ if (!FXGL.getSettings().isNative()) {
+ p.setEffect(new GaussianBlur());
+ }
+
+ p.visibleProperty().bind(btn.hoverProperty());
+
+ getChildren().addAll(btn, p);
+
+ btn.focusedProperty()
+ .addListener(
+ (v, e, isFocused) -> {
+ if (isFocused) {
+ var isOK =
+ animations.stream().noneMatch(Animation::isAnimating)
+ && !isAnimating;
+ if (isOK) {
+ isAnimating = true;
+
+ FXGL.animationBuilder()
+ .onFinished(() -> isAnimating = false)
+ .bobbleDown(this)
+ .buildAndPlay(Menu.this);
+ }
+ }
+ });
+ }
+
+ public void setOnAction(EventHandler e) {
+ btn.setOnAction(e);
+ }
+
+ public void setParent(MenuBox menu) {
+ parent = menu;
+ }
+
+ public void setMenuContent(Supplier contentSupplier) {
+ setMenuContent(contentSupplier, true);
+ }
+
+ public void setMenuContent(Supplier contentSupplier, boolean isCached) {
+ btn.addEventHandler(
+ ActionEvent.ACTION,
+ e -> {
+ if (cachedContent == null || !isCached) {
+ cachedContent = contentSupplier.get();
+ }
+
+ switchMenuContentTo(cachedContent);
+ });
+ }
+
+ public void setChild(MenuBox menu) {
+ var back = new MenuButton("menu.back");
+ menu.getChildren().addFirst(back);
+
+ back.addEventHandler(ActionEvent.ACTION, e -> switchMenuTo(this.parent));
+
+ btn.addEventHandler(ActionEvent.ACTION, e -> switchMenuTo(menu));
+ }
+ }
+
+ private class PressAnyKeyState extends SubScene {
+
+ private UserAction actionContext;
+
+ private boolean isActive = false;
+
+ public PressAnyKeyState() {
+ getInput()
+ .addEventFilter(
+ KeyEvent.KEY_PRESSED,
+ e -> {
+ if (Input.isIllegal(e.getCode())) {
+ return;
+ }
+
+ var rebound =
+ FXGL.getInput()
+ .rebind(
+ actionContext,
+ e.getCode(),
+ InputModifier.from(e));
+
+ if (rebound) {
+ FXGL.getSceneService().popSubScene();
+ isActive = false;
+ }
+ });
+
+ getInput()
+ .addEventFilter(
+ MouseEvent.MOUSE_PRESSED,
+ e -> {
+ var rebound =
+ FXGL.getInput()
+ .rebind(
+ actionContext,
+ e.getButton(),
+ InputModifier.from(e));
+
+ if (rebound) {
+ FXGL.getSceneService().popSubScene();
+ isActive = false;
+ }
+ });
+
+ var rect = new Rectangle(250.0, 100.0);
+ rect.setStroke(Color.color(0.85, 0.9, 0.9, 0.95));
+ rect.setStrokeWidth(10.0);
+ rect.setArcWidth(15.0);
+ rect.setArcHeight(15.0);
+
+ var text = FXGL.getUIFactoryService().newText("", 24.0);
+ text.textProperty().bind(FXGL.localizedStringProperty("menu.pressAnyKey"));
+
+ var pane = new StackPane(rect, text);
+ pane.setTranslateX(getAppWidth() / 2.0 - 125);
+ pane.setTranslateY(getAppHeight() / 2.0 - 50);
+
+ getContentRoot().getChildren().add(pane);
+ }
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/scenes/subscenes/ingame/DeathSubscene.java b/src/main/java/com/github/codestorm/bounceverse/scenes/subscenes/ingame/DeathSubscene.java
new file mode 100644
index 0000000..80fac60
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/scenes/subscenes/ingame/DeathSubscene.java
@@ -0,0 +1,151 @@
+package com.github.codestorm.bounceverse.scenes.subscenes.ingame;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.scene.SubScene;
+import com.almasb.fxgl.ui.FontType;
+import com.github.codestorm.bounceverse.systems.manager.metrics.LeaderboardManager;
+import com.github.codestorm.bounceverse.typing.records.EndlessScore;
+
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ *
+ * {@link DeathSubscene}
+ *
+ * {@link SubScene} hiển thị khi người chơi thua (Game Over).
+ * Hiển thị điểm số và cho phép lưu vào leaderboard nếu đạt top.
+ */
+public class DeathSubscene extends SubScene {
+ private Rectangle overlay;
+ private VBox mainContainer;
+ private final int score;
+ private final int level;
+
+ public final List onGotoMainMenuListeners = new ArrayList<>();
+
+ public DeathSubscene(int score, int level) {
+ this.score = score;
+ this.level = level;
+ final var canSavedToLeaderboard = LeaderboardManager.getInstance().isTopScore(score);
+
+ final var root = new StackPane();
+
+ // Lớp phủ màu đỏ trong suốt
+ final var overlay = new Rectangle(FXGL.getAppWidth(), FXGL.getAppHeight());
+ overlay.setFill(Color.rgb(255, 0, 0, 0.3));
+
+ // Container chính
+ final var mainContainer = new VBox(20);
+ mainContainer.setAlignment(Pos.CENTER);
+ mainContainer.setMaxWidth(500);
+
+ // Tiêu đề "GAME OVER"
+ final var gameOverText =
+ FXGL.getUIFactoryService().newText("GAME OVER", Color.RED, FontType.MONO, 48.0);
+ final var scoreText =
+ FXGL.getUIFactoryService()
+ .newText("Score: " + score, Color.WHITE, FontType.MONO, 24.0);
+ final var levelText =
+ FXGL.getUIFactoryService()
+ .newText("Level: " + level, Color.WHITE, FontType.MONO, 24.0);
+
+ mainContainer.getChildren().addAll(gameOverText, scoreText, levelText);
+
+ if (canSavedToLeaderboard) {
+ final var congratsText =
+ FXGL.getUIFactoryService()
+ .newText(
+ "Congratulations! You made it to the Top "
+ + LeaderboardManager.MAX_SIZE
+ + "!",
+ Color.YELLOW,
+ FontType.MONO,
+ 18.0);
+ congratsText.setWrappingWidth(450);
+
+ final var enterNameText =
+ FXGL.getUIFactoryService()
+ .newText("Enter your name:", Color.WHITE, FontType.UI, 16.0);
+ final var nameField = new TextField();
+ nameField.setPromptText("Enter name (alphanumeric only)");
+ nameField.setMaxWidth(300);
+ nameField.setStyle(
+ "-fx-background-color: rgba(255, 255, 255, 0.9);"
+ + "-fx-text-fill: black;"
+ + "-fx-font-size: 16px;"
+ + "-fx-padding: 10px;");
+ nameField
+ .textProperty()
+ .addListener(
+ (obs, oldVal, newVal) -> {
+ if (!newVal.matches("[a-zA-Z0-9]*")) {
+ nameField.setText(oldVal);
+ }
+ // Giới hạn độ dài tên
+ if (newVal.length() > 20) {
+ nameField.setText(oldVal);
+ }
+ });
+ final var saveButton = new Button("Save Score");
+ saveButton.setStyle(
+ "-fx-background-color: #4CAF50;"
+ + "-fx-text-fill: white;"
+ + "-fx-font-size: 16px;"
+ + "-fx-padding: 10px 30px;"
+ + "-fx-cursor: hand;");
+ saveButton.setOnAction(
+ e -> {
+ var name = nameField.getText().trim();
+ if (name.isEmpty()) {
+ name = "Anonymous";
+ }
+ saveToLeaderboard(name);
+ returnToMainMenu();
+ });
+
+ saveButton.disableProperty().bind(nameField.textProperty().isEmpty());
+ mainContainer.getChildren().addAll(congratsText, enterNameText, nameField, saveButton);
+ }
+ final var mainMenuButton = new Button("Return to Main Menu");
+ mainMenuButton.setStyle(
+ "-fx-background-color: #f44336;"
+ + "-fx-text-fill: white;"
+ + "-fx-font-size: 16px;"
+ + "-fx-padding: 10px 30px;"
+ + "-fx-cursor: hand;");
+ mainMenuButton.setOnAction(e -> returnToMainMenu());
+ mainContainer.getChildren().add(mainMenuButton);
+
+ root.getChildren().addAll(overlay, mainContainer);
+ root.setAlignment(Pos.CENTER);
+ getContentRoot().getChildren().add(root);
+ }
+
+ /**
+ * Lưu điểm số vào leaderboard.
+ *
+ * @param playerName Tên người chơi
+ */
+ private void saveToLeaderboard(String playerName) {
+ final var newScore = new EndlessScore(playerName, score, level, Instant.now());
+ LeaderboardManager.getInstance().addScore(newScore);
+ FXGL.getNotificationService().pushNotification("Score saved to leaderboard!");
+ }
+
+ /** Trở về màn hình chính. */
+ private void returnToMainMenu() {
+ onGotoMainMenuListeners.forEach(Runnable::run);
+ FXGL.getGameController().gotoMainMenu();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/System.java b/src/main/java/com/github/codestorm/bounceverse/systems/System.java
new file mode 100644
index 0000000..3a186ca
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/System.java
@@ -0,0 +1,10 @@
+package com.github.codestorm.bounceverse.systems;
+
+/**
+ *
+ *
+ * {@link System}
+ *
+ * Hệ thống game đảm nhiệm chức năng nào đó.
+ */
+public abstract class System {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/init/AppEventSystem.java b/src/main/java/com/github/codestorm/bounceverse/systems/init/AppEventSystem.java
new file mode 100644
index 0000000..d464e92
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/init/AppEventSystem.java
@@ -0,0 +1,58 @@
+package com.github.codestorm.bounceverse.systems.init;
+
+import com.almasb.fxgl.app.GameApplication;
+import com.almasb.fxgl.dsl.FXGL;
+import com.github.codestorm.bounceverse.systems.manager.metrics.LeaderboardManager;
+import com.github.codestorm.bounceverse.systems.manager.settings.UserSettingsManager;
+
+/**
+ *
+ *
+ * {@link AppEventSystem}
+ *
+ * {@link InitialSystem} quản lý các sự kiện trên {@link GameApplication}.
+ * Đừng nhầm lẫn với {@link GameSystem}.
+ *
+ * @apiNote Đây là một Singleton, cần lấy instance thông qua {@link #getInstance()}.
+ */
+public final class AppEventSystem extends InitialSystem {
+
+ private AppEventSystem() {}
+
+ public static AppEventSystem getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ /** Sự kiện khởi động game. */
+ private void onStart() {
+ // Áp dụng cài đặt người dùng
+ UserSettingsManager.getInstance().apply();
+
+ // Đăng ký Save/Load handler
+ FXGL.getSaveLoadService().addHandler(GameSystem.Variables.SAVE_LOAD_HANDLER);
+ }
+
+ /** Sự kiện thoát game. */
+ private void onExit() {
+ // Lưu cài đặt
+ UserSettingsManager.getInstance().save();
+
+ // Lưu bảng xếp hạng
+ LeaderboardManager.getInstance().save();
+ }
+
+ @Override
+ public void apply() {
+ onStart();
+ Runtime.getRuntime().addShutdownHook(new Thread(this::onExit));
+ }
+
+ /**
+ * Lazy-loaded singleton holder.
+ * Follow
+ * Initialization-on-demand holder idiom .
+ */
+ private static final class Holder {
+ static final AppEventSystem INSTANCE = new AppEventSystem();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/init/GameSystem.java b/src/main/java/com/github/codestorm/bounceverse/systems/init/GameSystem.java
new file mode 100644
index 0000000..14a478b
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/init/GameSystem.java
@@ -0,0 +1,467 @@
+package com.github.codestorm.bounceverse.systems.init;
+
+import com.almasb.fxgl.core.math.FXGLMath;
+import com.almasb.fxgl.core.serialization.Bundle;
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.SpawnData;
+import com.almasb.fxgl.profile.DataFile;
+import com.almasb.fxgl.profile.SaveLoadHandler;
+import com.github.codestorm.bounceverse.AssetsPath;
+import com.github.codestorm.bounceverse.AssetsPath.Textures.Bricks.ColorAssets;
+import com.github.codestorm.bounceverse.components.properties.paddle.PaddlePowerComponent;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+import com.github.codestorm.bounceverse.factory.entities.*;
+import com.github.codestorm.bounceverse.scenes.subscenes.ingame.DeathSubscene;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+import com.github.codestorm.bounceverse.typing.structures.HealthIntValue;
+import com.github.codestorm.bounceverse.ui.elements.HorizontalPositiveInteger;
+import com.github.codestorm.bounceverse.ui.elements.ingame.Hearts;
+
+import javafx.geometry.Point2D;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Text;
+import javafx.util.Duration;
+
+import libs.FastNoiseLite;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Random;
+
+public final class GameSystem extends InitialSystem {
+
+ private GameSystem() {}
+
+ public static GameSystem getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ public static void nextLevel() {
+ FXGL.getGameTimer().clear();
+ PowerUpManager.getInstance().clearAll();
+
+ FXGL.inc("level", 1);
+ FXGL.set("seed", FXGL.geti("seed") + 1);
+
+ FXGL.getNotificationService().pushNotification("Level " + FXGL.geti("level"));
+
+ // Dọn dẹp tất cả thực thể động
+ FXGL.getGameWorld()
+ .getEntitiesByType(
+ EntityType.BALL, EntityType.BRICK, EntityType.POWER_UP, EntityType.BULLET)
+ .forEach(Entity::removeFromWorld);
+
+ // Tạo lại màn chơi sau một khoảng trễ nhỏ để đảm bảo mọi thứ đã được dọn dẹp
+ FXGL.runOnce(
+ () -> {
+ EntitySpawn.paddle();
+ EntitySpawn.ball();
+ EntitySpawn.bricks();
+ },
+ Duration.seconds(0.1));
+ }
+
+ /** Reset lại toàn bộ game về trạng thái ban đầu. */
+ public static void resetGame() {
+ // Cách làm đúng và an toàn nhất là yêu cầu engine bắt đầu lại từ đầu
+ FXGL.getGameController().startNewGame();
+ }
+
+ public static final class Variables {
+
+ private Variables() {}
+
+ public static final int MAX_LIVES = 3;
+ public static final int DEFAULT_LIVES = 3;
+ public static final int DEFAULT_SCORE = 0;
+ public static final double DEFAULT_BALL_SPEED = 300;
+ private static final List PADDLE_COLORS =
+ List.of("blue", "green", "orange", "pink", "red", "yellow");
+
+ public static final SaveLoadHandler SAVE_LOAD_HANDLER =
+ new SaveLoadHandler() {
+ @Override
+ public void onSave(@NotNull DataFile dataFile) {
+ final var properties = FXGL.getWorldProperties();
+ final HealthIntValue lives = properties.getObject("lives");
+ final int score = properties.getInt("score");
+ final int level = properties.getInt("level");
+ final int seed = properties.getInt("seed");
+ final var bundle = new Bundle("game");
+ final var paddleColor = properties.getString("paddleColor");
+ final var ballSpeed = properties.getDouble("ballSpeed");
+
+ bundle.put("lives", lives.getValue());
+ bundle.put("score", score);
+ bundle.put("level", level);
+ bundle.put("seed", seed);
+ bundle.put("paddleColor", paddleColor);
+ bundle.put("ballSpeed", ballSpeed);
+ dataFile.putBundle(bundle);
+ }
+
+ @Override
+ public void onLoad(@NotNull DataFile dataFile) {
+ final var bundle = dataFile.getBundle("game");
+ final int intLives = bundle.get("lives");
+ final var lives =
+ new HealthIntValue(GameSystem.Variables.MAX_LIVES, intLives);
+ final int score = bundle.get("score");
+ final int level = bundle.get("level");
+ final int seed = bundle.get("seed");
+ final var paddleColor = bundle.get("paddleColor");
+ final var ballSpeed = bundle.get("ballSpeed");
+
+ final var vars = FXGL.getWorldProperties();
+ vars.clear();
+ vars.setValue("lives", lives);
+ vars.setValue("score", score);
+ vars.setValue("level", level);
+ vars.setValue("seed", seed);
+ vars.setValue("paddleColor", paddleColor);
+ vars.setValue("ballSpeed", ballSpeed);
+ }
+ };
+
+ public static void loadDefault(com.almasb.fxgl.core.collection.PropertyMap vars) {
+ final var lives = new HealthIntValue(MAX_LIVES, DEFAULT_LIVES);
+ vars.clear();
+ vars.setValue("lives", lives);
+ vars.setValue("score", DEFAULT_SCORE);
+ vars.setValue("level", 1);
+ vars.setValue("seed", (int) (System.currentTimeMillis() & 0x7FFFFFFF));
+ vars.setValue("ballSpeed", DEFAULT_BALL_SPEED);
+ var randomColor = PADDLE_COLORS.get(FXGLMath.random(0, PADDLE_COLORS.size() - 1));
+ vars.setValue("paddleColor", randomColor);
+ }
+
+ public static void hookDeathSubscene() {
+ final HealthIntValue livesProperty = FXGL.getWorldProperties().getObject("lives");
+ livesProperty.onZeroListeners.add(
+ () -> {
+ final var score = FXGL.getWorldProperties().getInt("score");
+ final var level = FXGL.getWorldProperties().getInt("level");
+ final var deathSubscene = new DeathSubscene(score, level);
+ deathSubscene.onGotoMainMenuListeners.add(
+ () -> {
+ FXGL.getSceneService().popSubScene();
+ FXGL.getGameController().resumeEngine();
+ });
+ FXGL.getSceneService().pushSubScene(deathSubscene);
+ FXGL.getGameController().pauseEngine();
+ });
+ }
+ }
+
+ private static final class EntitySpawn {
+
+ private EntitySpawn() {}
+
+ public static void addFactory() {
+ FXGL.getGameWorld().addEntityFactory(new WallFactory());
+ FXGL.getGameWorld().addEntityFactory(new BrickFactory());
+ FXGL.getGameWorld().addEntityFactory(new BulletFactory());
+ FXGL.getGameWorld().addEntityFactory(new PaddleFactory());
+ FXGL.getGameWorld().addEntityFactory(new BallFactory());
+ FXGL.getGameWorld().addEntityFactory(new PowerUpFactory());
+ }
+
+ public static void walls() {
+ FXGL.spawn("wallTop");
+ FXGL.spawn("wallBottom");
+ FXGL.spawn("wallLeft");
+ FXGL.spawn("wallRight");
+ }
+
+ public static void bricks() {
+ final var seed = FXGL.getWorldProperties().getInt("seed");
+ var noise = new FastNoiseLite(seed);
+
+ var colors =
+ AssetsPath.Textures.Bricks.COLORS.values().stream()
+ .map(ColorAssets::color)
+ .toArray(Color[]::new);
+
+ noise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
+ noise.SetFrequency(0.2f);
+ noise.SetFractalType(FastNoiseLite.FractalType.FBm);
+ noise.SetFractalOctaves(4);
+ noise.SetFractalLacunarity(2.1f);
+ noise.SetFractalGain(0.6f);
+ noise.SetDomainWarpType(FastNoiseLite.DomainWarpType.OpenSimplex2);
+ noise.SetDomainWarpAmp(6.0f);
+
+ final var rows = 5;
+ final var cols = 10;
+ final var amount = 36;
+ final var appWidth = FXGL.getAppWidth();
+ final var appHeight = FXGL.getAppHeight();
+ final double marginX = 40;
+ final double startY = 60;
+ final var verticalCoverageRatio = 0.30;
+ final double spacingX = 5;
+ final double spacingY = 5;
+ final var aspect = 30.0 / 80.0;
+ var availableWidth = appWidth - 2 * marginX;
+ var wFromWidth = (availableWidth - (cols - 1) * spacingX) / cols;
+ var hFromWidth = wFromWidth * aspect;
+ var targetBrickAreaHeight = appHeight * verticalCoverageRatio;
+ var hMaxByHeight = (targetBrickAreaHeight - (rows - 1) * spacingY) / rows;
+ var brickW = wFromWidth;
+ var brickH = hFromWidth;
+ if (brickH > hMaxByHeight) {
+ brickH = hMaxByHeight;
+ brickW = brickH / aspect;
+ }
+ double minW = 40, maxW = 160;
+ if (brickW < minW) {
+ brickW = minW;
+ }
+ if (brickW > maxW) {
+ brickW = maxW;
+ }
+ brickH = brickW * aspect;
+ var totalH = rows * brickH + (rows - 1) * spacingY;
+ if (totalH > targetBrickAreaHeight) {
+ brickH = hMaxByHeight;
+ brickW = brickH / aspect;
+ }
+ var brickWidth = (int) Math.round(brickW);
+ var brickHeight = (int) Math.round(brickH);
+ var totalW = cols * brickWidth + (cols - 1) * spacingX;
+ var startX = Math.max(marginX, (appWidth - totalW) / 2.0);
+
+ record Cell(int gx, int gy, double x, double y, float n) {}
+
+ var cells = new ArrayList(rows * cols);
+ for (var gy = 0; gy < rows; gy++) {
+ for (var gx = 0; gx < cols; gx++) {
+ var posX = startX + gx * (brickWidth + spacingX);
+ var posY = startY + gy * (brickHeight + spacingY);
+ var n = noise.GetNoise(gx, gy);
+ cells.add(new Cell(gx, gy, posX, posY, n));
+ }
+ }
+ cells.sort(Comparator.comparing(Cell::n).reversed());
+
+ // *** BẮT ĐẦU THAY ĐỔI LOGIC TẠI ĐÂY ***
+ // 1. Danh sách các loại gạch BẮT BUỘC. Bạn có thể thêm/bớt ở đây.
+ final List requiredTypes =
+ new ArrayList<>(
+ List.of(
+ "explodingBrick", // Thêm gạch nổ
+ "explodingBrick",
+ "explodingBrick",
+ "keyBrick", // Thêm gạch chìa khóa
+ "keyBrick",
+ "shieldBrick",
+ "strongBrick",
+ "normalBrick"));
+ // Xáo trộn danh sách để vị trí của chúng ngẫu nhiên
+ Collections.shuffle(requiredTypes, new Random(seed));
+
+ var target = Math.min(amount, cells.size());
+ var colorAssignments = new Color[target];
+ System.arraycopy(colors, 0, colorAssignments, 0, Math.min(colors.length, target));
+ for (var i = colors.length; i < target; i++) {
+ var n = cells.get(i).n();
+ var normalizedN = (n + 1.0f) / 2.0f;
+ var colorIndex = (int) (normalizedN * colors.length);
+ if (colorIndex >= colors.length) {
+ colorIndex = colors.length - 1;
+ }
+ colorAssignments[i] = colors[colorIndex];
+ }
+ for (var i = 0; i < target; i++) {
+ var c = cells.get(i);
+ var n = c.n();
+ final String type;
+
+ // 2. Ưu tiên spawn các loại gạch bắt buộc
+ if (i < requiredTypes.size()) {
+ type = requiredTypes.get(i);
+ } else {
+ // 3. Logic ngẫu nhiên cho các viên gạch còn lại, có thêm gạch nổ
+ if (n > 0.6f) {
+ type = "shieldBrick";
+ } else if (n > 0.3f) {
+ type = "strongBrick";
+ } else if (n > 0.0f) {
+ type = "explodingBrick";
+ } else {
+ type = "normalBrick";
+ }
+ }
+
+ var data = new SpawnData();
+ data.put("pos", new Point2D(c.x(), c.y()));
+ data.put("width", brickWidth);
+ data.put("height", brickHeight);
+ data.put("color", colorAssignments[i]);
+ FXGL.spawn(type, data);
+ }
+ // *** KẾT THÚC THAY ĐỔI ***
+ }
+
+ public static void paddle() {
+ FXGL.getGameWorld()
+ .getEntitiesByType(EntityType.PADDLE)
+ .forEach(Entity::removeFromWorld);
+ FXGL.spawn("paddle");
+ }
+
+ public static void ball() {
+ if (FXGL.getGameWorld().getEntitiesByType(EntityType.BALL).isEmpty()) {
+ var paddle = FXGL.getGameWorld().getSingleton(EntityType.PADDLE);
+ var x = paddle.getCenter().getX() - BallFactory.DEFAULT_RADIUS;
+ var y = paddle.getY() - BallFactory.DEFAULT_RADIUS + 1;
+ FXGL.spawn("ball", new SpawnData(x, y).put("attached", true));
+ FXGL.set("ballAttached", true);
+ }
+ }
+ }
+
+ public static final class UI {
+
+ private UI() {}
+
+ private boolean isInitialized = false;
+ private Hearts heartsDisplay;
+ private HorizontalPositiveInteger scoreDisplay;
+ private Text levelDisplay;
+ private Text cooldownText;
+
+ public static UI getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ public void addScoreDisplay() {
+ if (scoreDisplay == null) {
+ final var scoreProperty = FXGL.getWorldProperties().intProperty("score");
+ scoreDisplay = new HorizontalPositiveInteger(scoreProperty);
+ scoreDisplay.setMinDigits(6);
+ }
+ FXGL.getGameScene().addUINode(scoreDisplay.getView());
+ scoreDisplay.getView().setTranslateX(20);
+ scoreDisplay.getView().setTranslateY(20);
+ }
+
+ public void addHeartsDisplay() {
+ if (heartsDisplay == null) {
+ heartsDisplay = new Hearts();
+ }
+ FXGL.getGameScene().addUINode(heartsDisplay.getView());
+ heartsDisplay.getView().setTranslateX(20);
+ heartsDisplay.getView().setTranslateY(FXGL.getAppHeight() - 52);
+ }
+
+ public void addLevelDisplay() {
+ if (levelDisplay == null) {
+ final var levelProperty = FXGL.getWorldProperties().intProperty("level");
+ levelDisplay = FXGL.getUIFactoryService().newText("", Color.WHITE, 24.0);
+ levelDisplay.textProperty().bind(levelProperty.asString("LEVEL %d"));
+ }
+ FXGL.getGameScene().addUINode(levelDisplay);
+ levelDisplay.setTranslateX(FXGL.getAppWidth() - 120);
+ levelDisplay.setTranslateY(FXGL.getAppHeight() - 30);
+ }
+
+ public void addCooldownDisplay() {
+ if (cooldownText == null) {
+ cooldownText = FXGL.getUIFactoryService().newText("", 18);
+ cooldownText.getStyleClass().add("powerup-text");
+ }
+ FXGL.getGameScene().addUINode(cooldownText);
+ cooldownText.setTranslateX(FXGL.getAppWidth() - 220);
+ cooldownText.setTranslateY((double) FXGL.getAppHeight() / 2);
+ }
+
+ public void onUpdate() {
+ if (!isInitialized || cooldownText == null) {
+ return;
+ }
+ var paddleOpt = FXGL.getGameWorld().getSingletonOptional(EntityType.PADDLE);
+ if (paddleOpt.isEmpty()) {
+ cooldownText.setText("");
+ return;
+ }
+ var powerCompOpt = paddleOpt.get().getComponentOptional(PaddlePowerComponent.class);
+ if (powerCompOpt.isEmpty()) {
+ cooldownText.setText("");
+ return;
+ }
+ var powerComp = powerCompOpt.get();
+ var powerCooldown = powerComp.getPowerCooldown();
+ var timeLeft = powerCooldown.getTimeLeft();
+ if (timeLeft == null || timeLeft.toMillis() <= 0) {
+ cooldownText.setText("Power: Start");
+ } else {
+ cooldownText.setText("Power: Cooldown");
+ }
+ }
+
+ public void addAll() {
+ if (isInitialized) {
+ return;
+ }
+ addScoreDisplay();
+ addHeartsDisplay();
+ addLevelDisplay();
+ addCooldownDisplay();
+ isInitialized = true;
+ }
+
+ public void removeAll() {
+ if (!isInitialized) {
+ return;
+ }
+ if (scoreDisplay != null) {
+ FXGL.getGameScene().removeUINode(scoreDisplay.getView());
+ }
+ if (heartsDisplay != null) {
+ FXGL.getGameScene().removeUINode(heartsDisplay.getView());
+ }
+ if (levelDisplay != null) {
+ FXGL.getGameScene().removeUINode(levelDisplay);
+ }
+ if (cooldownText != null) {
+ FXGL.getGameScene().removeUINode(cooldownText);
+ }
+ isInitialized = false;
+ }
+
+ public void dispose() {
+ removeAll();
+ scoreDisplay = null;
+ heartsDisplay = null;
+ levelDisplay = null;
+ cooldownText = null;
+ }
+
+ private static final class Holder {
+
+ static final UI INSTANCE = new UI();
+ }
+ }
+
+ @Override
+ public void apply() {
+ Variables.hookDeathSubscene();
+ EntitySpawn.addFactory();
+ EntitySpawn.walls();
+ EntitySpawn.bricks();
+ EntitySpawn.paddle();
+ EntitySpawn.ball();
+ UI.getInstance().addAll();
+ }
+
+ private static final class Holder {
+
+ static final GameSystem INSTANCE = new GameSystem();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/core/systems/System.java b/src/main/java/com/github/codestorm/bounceverse/systems/init/InitialSystem.java
similarity index 67%
rename from src/main/java/com/github/codestorm/bounceverse/core/systems/System.java
rename to src/main/java/com/github/codestorm/bounceverse/systems/init/InitialSystem.java
index 61bb3ed..0fe6acc 100644
--- a/src/main/java/com/github/codestorm/bounceverse/core/systems/System.java
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/init/InitialSystem.java
@@ -1,13 +1,14 @@
-package com.github.codestorm.bounceverse.core.systems;
+package com.github.codestorm.bounceverse.systems.init;
import com.github.codestorm.bounceverse.Bounceverse;
+import com.github.codestorm.bounceverse.systems.System;
/**
*
*
- * {@link System}
+ * {@link InitialSystem}
*
- * Hệ thống của game, đảm nhiệm cho một thành phần nào đó của game.
+ * Hệ thống khởi tạo của game, đảm nhiệm cho việc khởi tạo chức năng nào đó của game
*
* Các lớp kế thừa nên thiết kế dựa trên (lazy-loaded) Singleton.
* Tất cả logic của hệ thống được áp dụng thông qua {@link #apply()}.
@@ -20,8 +21,8 @@
* A : Tất cả là tại abstract/interface không cho ghi đè static đó!!! 😭😭😭
*
*/
-abstract class System {
- protected System() {}
+abstract class InitialSystem extends System {
+ protected InitialSystem() {}
/**
* Áp dụng logic của hệ thống vào game.
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/init/InputSystem.java b/src/main/java/com/github/codestorm/bounceverse/systems/init/InputSystem.java
new file mode 100644
index 0000000..3fe5ed9
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/init/InputSystem.java
@@ -0,0 +1,159 @@
+package com.github.codestorm.bounceverse.systems.init;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.input.UserAction;
+import com.almasb.fxgl.physics.PhysicsComponent;
+import com.github.codestorm.bounceverse.components.behaviors.Attachment;
+import com.github.codestorm.bounceverse.components.behaviors.paddle.ReverseControlComponent;
+import com.github.codestorm.bounceverse.components.properties.paddle.PaddlePowerComponent;
+import com.github.codestorm.bounceverse.components.properties.powerup.paddle.DuplicatePaddlePowerUp;
+import com.github.codestorm.bounceverse.factory.entities.WallFactory;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
+import javafx.scene.input.KeyCode;
+
+import java.util.List;
+
+/** Quản lý Input cho game. */
+public final class InputSystem extends InitialSystem {
+
+ private static final double MOVE_SPEED = 400.0;
+ private static final double WALL_THICKNESS = WallFactory.DEFAULT_THICKNESS;
+
+ private InputSystem() {}
+
+ public static InputSystem getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ private void movePaddle(double inputDirection) {
+ // TODO: Sửa lỗi chọn reset game khi đang giữ di chuyển di chuyển
+ var mainPaddle = FXGL.getGameWorld().getSingleton(EntityType.PADDLE);
+ var leftmostPaddle = mainPaddle;
+ var rightmostPaddle = mainPaddle;
+
+ if (FXGL.getWorldProperties().exists(DuplicatePaddlePowerUp.CLONES_LIST_KEY)) {
+ List clones = FXGL.geto(DuplicatePaddlePowerUp.CLONES_LIST_KEY);
+ if (!clones.isEmpty() && clones.get(0).isActive()) {
+ leftmostPaddle = clones.get(0);
+ rightmostPaddle = clones.get(1);
+ }
+ }
+
+ var isReversed = mainPaddle.hasComponent(ReverseControlComponent.class);
+ var actualMoveDirection = isReversed ? -inputDirection : inputDirection;
+
+ if (actualMoveDirection < 0) {
+ if (leftmostPaddle.getX() <= WALL_THICKNESS) {
+ setAllPaddlesVelocity(0);
+ return;
+ }
+ } else {
+ if (rightmostPaddle.getRightX() >= FXGL.getAppWidth() - WALL_THICKNESS) {
+ setAllPaddlesVelocity(0);
+ return;
+ }
+ }
+ setAllPaddlesVelocity(actualMoveDirection * MOVE_SPEED);
+ }
+
+ private void setAllPaddlesVelocity(double velocity) {
+ var mainPaddle = FXGL.getGameWorld().getSingleton(EntityType.PADDLE);
+ mainPaddle.getComponent(PhysicsComponent.class).setVelocityX(velocity);
+
+ if (FXGL.getWorldProperties().exists(DuplicatePaddlePowerUp.CLONES_LIST_KEY)) {
+ List clones = FXGL.geto(DuplicatePaddlePowerUp.CLONES_LIST_KEY);
+ for (var clone : clones) {
+ clone.getComponent(PhysicsComponent.class).setVelocityX(velocity);
+ }
+ }
+ }
+
+ @Override
+ public void apply() {
+ FXGL.getInput()
+ .addAction(
+ new UserAction("Move Left") {
+ @Override
+ protected void onAction() {
+ movePaddle(-1.0);
+ }
+
+ @Override
+ protected void onActionEnd() {
+ setAllPaddlesVelocity(0);
+ }
+ },
+ KeyCode.LEFT);
+
+ FXGL.getInput()
+ .addAction(
+ new UserAction("Move Right") {
+ @Override
+ protected void onAction() {
+ movePaddle(1.0);
+ }
+
+ @Override
+ protected void onActionEnd() {
+ setAllPaddlesVelocity(0);
+ }
+ },
+ KeyCode.RIGHT);
+
+ FXGL.getInput()
+ .addAction(
+ new UserAction("Launch Ball") {
+ @Override
+ protected void onActionBegin() {
+ FXGL.getGameWorld()
+ .getEntitiesByType(EntityType.BALL)
+ .forEach(
+ ball ->
+ ball.getComponentOptional(Attachment.class)
+ .ifPresent(
+ a -> {
+ if (a.isAttached()) {
+ a.releaseBall();
+ FXGL.set(
+ "ballAttached",
+ false);
+ }
+ }));
+ }
+ },
+ KeyCode.SPACE);
+
+ FXGL.getInput()
+ .addAction(
+ new UserAction("Activate Power") {
+ @Override
+ protected void onActionBegin() {
+ FXGL.getGameWorld()
+ .getSingletonOptional(EntityType.PADDLE)
+ .flatMap(
+ paddle ->
+ paddle.getComponentOptional(
+ PaddlePowerComponent.class))
+ .ifPresent(PaddlePowerComponent::activatePower);
+ }
+ },
+ KeyCode.S);
+
+ FXGL.getInput()
+ .addAction(
+ new UserAction("Reset Game") {
+ @Override
+ protected void onActionBegin() {
+ GameSystem.resetGame();
+ }
+ },
+ KeyCode.R);
+ }
+
+ private static final class Holder {
+
+ static final InputSystem INSTANCE = new InputSystem();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/init/PhysicSystem.java b/src/main/java/com/github/codestorm/bounceverse/systems/init/PhysicSystem.java
new file mode 100644
index 0000000..a82ac5e
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/init/PhysicSystem.java
@@ -0,0 +1,321 @@
+package com.github.codestorm.bounceverse.systems.init;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.SpawnData;
+import com.almasb.fxgl.physics.CollisionHandler;
+import com.almasb.fxgl.physics.PhysicsComponent;
+import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.components.behaviors.Attack;
+import com.github.codestorm.bounceverse.components.properties.Shield;
+import com.github.codestorm.bounceverse.components.properties.paddle.PaddleViewManager;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpContainer;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUpManager;
+import com.github.codestorm.bounceverse.factory.entities.BallFactory;
+import com.github.codestorm.bounceverse.typing.enums.DirectionUnit;
+import com.github.codestorm.bounceverse.typing.enums.EntityType;
+import com.github.codestorm.bounceverse.typing.enums.PowerUpType;
+import com.github.codestorm.bounceverse.typing.structures.HealthIntValue;
+
+import javafx.geometry.Point2D;
+import javafx.geometry.Side;
+
+import java.util.List;
+
+/**
+ *
+ *
+ * PhysicSystem
+ *
+ * Quản lý toàn bộ logic va chạm vật lý trong game (ball, paddle, wall, power-up...).
+ */
+public final class PhysicSystem extends InitialSystem {
+
+ private PhysicSystem() {}
+
+ public static PhysicSystem getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ @Override
+ public void apply() {
+ var world = FXGL.getPhysicsWorld();
+ world.setGravity(0, 0);
+
+ // Bullet vs Brick
+ world.addCollisionHandler(
+ new CollisionHandler(EntityType.BULLET, EntityType.BRICK) {
+ @Override
+ protected void onCollisionBegin(Entity bullet, Entity brick) {
+ var bulletPhysics = bullet.getComponent(PhysicsComponent.class);
+ var velocity = bulletPhysics.getLinearVelocity();
+
+ Side hitSide;
+
+ if (Math.abs(velocity.getY()) > Math.abs(velocity.getX())) {
+ if (velocity.getY() < 0) {
+ hitSide = Side.BOTTOM;
+ } else {
+ hitSide = Side.TOP;
+ }
+ } else {
+ if (velocity.getX() < 0) {
+ hitSide = Side.RIGHT;
+ } else {
+ hitSide = Side.LEFT;
+ }
+ }
+
+ var shieldOpt = brick.getComponentOptional(Shield.class);
+
+ // Kiểm tra xem mặt vừa bị va chạm có được bảo vệ không
+ if (shieldOpt.isPresent() && shieldOpt.get().hasSide(hitSide)) {
+ // Có khiên bảo vệ, không gây sát thương
+ } else {
+ // Không có khiên, gây sát thương
+ bullet.getComponentOptional(Attack.class)
+ .ifPresent(a -> a.execute(List.of(brick)));
+ }
+
+ // Viên đạn luôn tự hủy sau va chạm
+ bullet.removeFromWorld();
+ }
+ });
+
+ // Bullet vs Wall
+ world.addCollisionHandler(
+ new CollisionHandler(EntityType.BULLET, EntityType.WALL) {
+ @Override
+ protected void onCollisionBegin(Entity bullet, Entity wall) {
+ // Đơn giản là xóa viên đạn khi nó chạm vào bất kỳ bức tường nào
+ bullet.removeFromWorld();
+ }
+ });
+
+ // Ball vs Brick
+ world.addCollisionHandler(
+ new CollisionHandler(EntityType.BALL, EntityType.BRICK) {
+ @Override
+ protected void onCollisionBegin(Entity ball, Entity brick) {
+ var physics = ball.getComponent(PhysicsComponent.class);
+ var dir = Utilities.Collision.getCollisionDirection(ball, brick);
+
+ var shieldOpt = brick.getComponentOptional(Shield.class);
+ if (shieldOpt.isPresent() && shieldOpt.get().hasSide(dir.toSide())) {
+ bounce(physics, dir);
+ return;
+ }
+
+ bounce(physics, dir);
+ ball.getComponentOptional(
+ com.github.codestorm.bounceverse.components.behaviors.Attack
+ .class)
+ .ifPresent(a -> a.execute(java.util.List.of(brick)));
+ }
+ });
+
+ // Ball vs Paddle
+ world.addCollisionHandler(
+ new CollisionHandler(EntityType.BALL, EntityType.PADDLE) {
+ @Override
+ protected void onCollisionBegin(Entity ball, Entity paddle) {
+ var physics = ball.getComponent(PhysicsComponent.class);
+ var offset =
+ (ball.getCenter().getX() - paddle.getCenter().getX())
+ / (paddle.getWidth() / 2.0);
+ offset = Math.max(-1.0, Math.min(1.0, offset));
+ var angle = Math.toRadians(90 - 45 * offset);
+ var speed = FXGL.getd("ballSpeed");
+ physics.setLinearVelocity(
+ speed * Math.cos(angle), -speed * Math.sin(angle));
+ }
+ });
+
+ // Ball vs Shield (bottom shield power-up)
+ world.addCollisionHandler(
+ new CollisionHandler(EntityType.BALL, PowerUpType.SHIELD) {
+ @Override
+ protected void onCollisionBegin(Entity ball, Entity shield) {
+ ball.getComponentOptional(PhysicsComponent.class)
+ .ifPresent(
+ phys -> {
+ var velocity = phys.getLinearVelocity();
+ phys.setLinearVelocity(
+ new Point2D(
+ velocity.getX(),
+ -Math.abs(velocity.getY())));
+ });
+ }
+ });
+
+ // Ball vs Wall
+ world.addCollisionHandler(
+ new CollisionHandler(EntityType.BALL, EntityType.WALL) {
+ @Override
+ protected void onCollisionBegin(Entity ball, Entity wall) {
+ var phys = ball.getComponent(PhysicsComponent.class);
+ var v = phys.getLinearVelocity();
+ Side side = wall.getObject("side");
+ var eps = 0.5; // khoảng đệm nhỏ
+
+ switch (side) {
+ case LEFT:
+ {
+ ball.setX(wall.getRightX() + eps);
+ phys.setLinearVelocity(Math.abs(v.getX()), v.getY());
+ break;
+ }
+ case RIGHT:
+ {
+ ball.setX(wall.getX() - ball.getWidth() - eps);
+ phys.setLinearVelocity(-Math.abs(v.getX()), v.getY());
+ break;
+ }
+ case TOP:
+ {
+ ball.setY(wall.getBottomY() + eps);
+ phys.setLinearVelocity(v.getX(), Math.abs(v.getY()));
+ break;
+ }
+ case BOTTOM:
+ {
+ // KIỂM TRA: Có tấm khiên nào đang hoạt động trên màn hình
+ // không?
+ if (FXGL.getGameWorld()
+ .getEntitiesByType(PowerUpType.SHIELD)
+ .isEmpty()) {
+
+ // --- LOGIC CŨ: KHÔNG CÓ KHIÊN ---
+ // Tường đáy là "tường chết", hủy bóng và trừ mạng.
+ ball.removeFromWorld();
+
+ if (FXGL.getGameWorld()
+ .getEntitiesByType(EntityType.BALL)
+ .isEmpty()) {
+ HealthIntValue lives =
+ FXGL.getWorldProperties().getObject("lives");
+ lives.damage(1);
+
+ FXGL.getGameTimer()
+ .runOnceAfter(
+ () -> {
+ if (lives.getValue() > 0) {
+ var paddle =
+ FXGL.getGameWorld()
+ .getSingleton(
+ EntityType
+ .PADDLE);
+ paddle.getComponent(
+ PaddleViewManager
+ .class)
+ .reset();
+ PowerUpManager.getInstance()
+ .clearAll();
+ var x =
+ paddle.getCenter()
+ .getX()
+ - BallFactory
+ .DEFAULT_RADIUS;
+ var y =
+ paddle.getY()
+ - BallFactory
+ .DEFAULT_RADIUS
+ * 2;
+ FXGL.spawn(
+ "ball",
+ new SpawnData(x, y)
+ .put(
+ "attached",
+ true));
+ FXGL.set("ballAttached", true);
+ }
+ },
+ javafx.util.Duration.millis(100));
+ }
+
+ } else {
+ // Đảo ngược vận tốc Y để bóng nảy lên
+ var vel = phys.getLinearVelocity();
+ phys.setLinearVelocity(vel.getX(), -Math.abs(vel.getY()));
+ }
+ break;
+ }
+ }
+
+ var currentVelocity = phys.getLinearVelocity();
+ var currentSpeed = currentVelocity.magnitude();
+ var targetSpeed = FXGL.getd("ballSpeed");
+
+ // Đặt một khoảng an toàn để tránh điều chỉnh liên tục do sai số vật lý
+ var minAllowedSpeed = targetSpeed * 0.95;
+ var maxAllowedSpeed = targetSpeed * 1.05;
+
+ var newSpeed = currentSpeed;
+
+ if (currentSpeed < minAllowedSpeed) {
+ newSpeed = minAllowedSpeed;
+ } else if (currentSpeed > maxAllowedSpeed) {
+ newSpeed = maxAllowedSpeed;
+ }
+
+ // Chỉ cập nhật lại vận tốc nếu tốc độ thực sự bị thay đổi
+ if (Math.abs(newSpeed - currentSpeed) > 1e-3) {
+ phys.setLinearVelocity(currentVelocity.normalize().multiply(newSpeed));
+ }
+ }
+ });
+
+ world.addCollisionHandler(
+ new CollisionHandler(EntityType.PADDLE, EntityType.POWER_UP) {
+ @Override
+ protected void onCollisionBegin(Entity paddle, Entity powerUp) {
+ if (paddle.isActive() && powerUp.isActive()) {
+ powerUp.getComponentOptional(PowerUpContainer.class)
+ .ifPresent(
+ container -> {
+ container.addTo(paddle);
+ container
+ .getContainer()
+ .values()
+ .forEach(
+ c -> {
+ if (c instanceof PowerUp p) {
+ p.apply(paddle);
+ }
+ });
+ });
+ powerUp.removeFromWorld();
+ }
+ }
+ });
+ }
+
+ /**
+ * Đảo hướng vận tốc theo hướng va chạm.
+ *
+ * @param phys a {@link PhysicsComponent}
+ * @param dir a {@link DirectionUnit}
+ */
+ private static void bounce(PhysicsComponent phys, DirectionUnit dir) {
+ if (dir == null) {
+ return;
+ }
+
+ switch (dir) {
+ case UP, DOWN:
+ phys.setLinearVelocity(phys.getVelocityX(), -phys.getVelocityY());
+ break;
+ case LEFT, RIGHT:
+ phys.setLinearVelocity(-phys.getVelocityX(), phys.getVelocityY());
+ break;
+ default:
+ break;
+ }
+ }
+
+ private static final class Holder {
+
+ static final PhysicSystem INSTANCE = new PhysicSystem();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/init/PowerUpSpawner.java b/src/main/java/com/github/codestorm/bounceverse/systems/init/PowerUpSpawner.java
new file mode 100644
index 0000000..8e71b18
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/init/PowerUpSpawner.java
@@ -0,0 +1,114 @@
+package com.github.codestorm.bounceverse.systems.init;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.logging.Logger;
+import com.almasb.fxgl.texture.Texture;
+import com.github.codestorm.bounceverse.components.properties.powerup.PowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.ball.*;
+import com.github.codestorm.bounceverse.components.properties.powerup.misc.ExtraLifePowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.misc.ShieldPowerUp;
+import com.github.codestorm.bounceverse.components.properties.powerup.paddle.*;
+import com.github.codestorm.bounceverse.typing.enums.PowerUpType;
+
+import javafx.geometry.Point2D;
+
+import java.util.*;
+
+/**
+ *
+ *
+ * PowerUpSpawner
+ *
+ * Random loại Power-Up theo trọng số.
+ */
+public final class PowerUpSpawner {
+
+ private static final Logger LOGGER = Logger.get(PowerUpSpawner.class);
+
+ private static final Map WEIGHTS =
+ Map.ofEntries(
+ Map.entry(PowerUpType.EXPAND_PADDLE, 0.15),
+ Map.entry(PowerUpType.SHRINK_PADDLE, 0.10),
+ Map.entry(PowerUpType.MULTI_BALL, 0.10),
+ Map.entry(PowerUpType.SLOW_BALL, 0.10),
+ Map.entry(PowerUpType.FAST_BALL, 0.10),
+ Map.entry(PowerUpType.SHIELD, 0.10),
+ Map.entry(PowerUpType.GUN, 0.10),
+ Map.entry(PowerUpType.REVERSE_PADDLE, 0.15),
+ Map.entry(PowerUpType.EXTRA_LIFE, 0.08));
+
+ private static final Random RANDOM = new Random();
+
+ private PowerUpSpawner() {}
+
+ /** Random theo trọng số */
+ public static PowerUpType getRandomPowerUpType() {
+ var totalWeight = WEIGHTS.values().stream().mapToDouble(Double::doubleValue).sum();
+ var r = RANDOM.nextDouble() * totalWeight;
+ var cumulative = 0.0;
+ for (var entry : WEIGHTS.entrySet()) {
+ cumulative += entry.getValue();
+ if (r <= cumulative) return entry.getKey();
+ }
+ LOGGER.warning("PowerUp random fallback triggered.");
+ return PowerUpType.EXPAND_PADDLE;
+ }
+
+ /** Tạo instance Power-Up */
+ public static PowerUp getPowerUpInstance(PowerUpType type) {
+ return switch (type) {
+ // Paddle
+ case EXPAND_PADDLE -> new ExpandPaddlePowerUp();
+ case SHRINK_PADDLE -> new ShrinkPaddlePowerUp();
+ case GUN -> new GunPowerUp();
+ case REVERSE_PADDLE -> new ReversePaddlePowerUp();
+
+ // Ball
+ case MULTI_BALL -> new MultipleBallPowerUp();
+ case SLOW_BALL -> new SlowBallPowerUp();
+ case FAST_BALL -> new FastBallPowerUp();
+
+ // Misc
+ case SHIELD -> new ShieldPowerUp();
+ case EXTRA_LIFE -> new ExtraLifePowerUp();
+ };
+ }
+
+ /** Lấy texture theo loại Power-Up */
+ public static Texture getPowerUpTexture(PowerUpType type) {
+ var path =
+ switch (type) {
+ // Paddle
+ case EXPAND_PADDLE -> "power/paddle/Expand Paddle.png";
+ case SHRINK_PADDLE -> "power/paddle/Shrink Paddle.png";
+ case GUN -> "power/paddle/Gun.png";
+ case REVERSE_PADDLE -> "power/paddle/Reverse Paddle.png";
+
+ // Ball
+ case MULTI_BALL -> "power/ball/x2 Ball.png";
+ case SLOW_BALL -> "power/ball/Slow Ball.png";
+ case FAST_BALL -> "power/ball/Fast Ball.png";
+
+ // Misc
+ case SHIELD -> "power/misc/shield.png";
+ case EXTRA_LIFE -> "power/misc/extra_life.png";
+ };
+ return FXGL.texture(path);
+ }
+
+ /** Random một Power-Up (cả instance + texture) */
+ public static Map.Entry nextPowerUp() {
+ var type = getRandomPowerUpType();
+ return Map.entry(getPowerUpInstance(type), getPowerUpTexture(type));
+ }
+
+ /** Spawn random Power-Up tại vị trí */
+ public static void spawnRandom(Point2D position) {
+ var pair = nextPowerUp();
+ FXGL.spawn(
+ "powerUp",
+ new com.almasb.fxgl.entity.SpawnData(position)
+ .put("texture", pair.getValue())
+ .put("contains", pair.getKey()));
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/init/UISystem.java b/src/main/java/com/github/codestorm/bounceverse/systems/init/UISystem.java
new file mode 100644
index 0000000..7310b3a
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/init/UISystem.java
@@ -0,0 +1,239 @@
+package com.github.codestorm.bounceverse.systems.init;
+
+import com.almasb.fxgl.dsl.FXGL;
+
+import javafx.animation.AnimationTimer;
+import javafx.geometry.Point2D;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.effect.BlendMode;
+import javafx.scene.effect.GaussianBlur;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.CubicCurveTo;
+import javafx.scene.shape.MoveTo;
+import javafx.scene.shape.Path;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * UISystem với hiệu ứng sóng neon và nền "Siri" động. Hệ thống này vẽ hiệu ứng LÊN TRÊN nền đen có
+ * sẵn của GameScene.
+ */
+public final class UISystem extends InitialSystem {
+
+ // --- Hằng số điều khiển Sóng Neon ---
+ private static final double WAVE_Y_POSITION = FXGL.getAppHeight() / 2.0;
+ private static final double WAVE_AMPLITUDE = 50.0;
+ private static final double WAVE_STROKE_WIDTH = 3.0;
+ private static final double WAVE_SEPARATION = 12.0;
+ private static final double WAVE_TIME_OFFSET = 0.4;
+ private static final double WAVE_STEP = 20.0;
+ private static final double WAVE_SPEED = 0.8;
+ private static final double WAVE_BLUR_AMOUNT = 15.0;
+
+ // --- Hằng số điều khiển hiệu ứng nền Siri ---
+ private static final int BLOB_COUNT = 5;
+ private static final double BLOB_BASE_SIZE = 250.0;
+ private static final double BLOB_MAX_SPEED = 0.4;
+ private static final double BLOB_GAUSSIAN_BLUR = 150.0;
+
+ // --- Hằng số mục tiêu ---
+ private static final Set TARGET_COLORS =
+ Set.of(Color.BLUE, Color.GREEN, Color.ORANGE, Color.PINK, Color.RED, Color.YELLOW);
+
+ // --- Các biến thành viên ---
+ private Group backgroundBlobLayer;
+ private List blobs;
+ private final Random random = new Random();
+
+ private Group waveLayer;
+ private double time = 0;
+ private final Set collectedColors = new java.util.LinkedHashSet<>();
+ private boolean allColorsCollected = false;
+
+ private AnimationTimer animationTimer;
+
+ private UISystem() {}
+
+ public static UISystem getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ /** Dọn dẹp và reset lại toàn bộ trạng thái của UISystem. */
+ public void dispose() {
+ // 1. Dừng animation timer cũ nếu nó đang chạy
+ if (animationTimer != null) {
+ animationTimer.stop();
+ animationTimer = null;
+ }
+
+ // 2. Dọn dẹp nội dung của các layer và đặt chúng về null
+ // Không cần xóa chúng khỏi parent, vì toàn bộ scene sẽ được tạo lại.
+ if (waveLayer != null) {
+ waveLayer.getChildren().clear();
+ waveLayer = null;
+ }
+ if (backgroundBlobLayer != null) {
+ backgroundBlobLayer.getChildren().clear();
+ backgroundBlobLayer = null;
+ }
+
+ // 3. Reset lại tất cả các biến trạng thái về giá trị ban đầu
+ time = 0;
+ collectedColors.clear();
+ allColorsCollected = false;
+ }
+
+ @Override
+ public void apply() {
+ // 1. Khởi tạo các layer hiệu ứng (KHÔNG CÒN TẠO NỀN ĐEN Ở ĐÂY)
+ createSiriBlobs();
+ createWaveLayer();
+
+ // 2. Thêm các layer hiệu ứng vào GỐC của scene để chúng nằm dưới các thực thể game
+ FXGL.getGameScene()
+ .getRoot()
+ .getChildren()
+ .addAll(0, java.util.Arrays.asList(backgroundBlobLayer, waveLayer));
+
+ // 3. Bắt đầu vòng lặp animation
+ startAnimationLoop();
+ }
+
+ private void createSiriBlobs() {
+ backgroundBlobLayer = new Group();
+ blobs = new ArrayList<>();
+ var siriColors =
+ new Color[] {Color.web("#26F3FF"), Color.web("#A942FF"), Color.web("#FF34BB")};
+ for (var i = 0; i < BLOB_COUNT; i++) {
+ var blob = new Circle();
+ blob.setRadius(BLOB_BASE_SIZE + random.nextDouble() * 100);
+ blob.setCenterX(random.nextDouble() * FXGL.getAppWidth());
+ blob.setCenterY(random.nextDouble() * FXGL.getAppHeight());
+ blob.setFill(siriColors[i % siriColors.length].deriveColor(0, 1, 1, 0.3));
+ var velocity =
+ new Point2D(
+ (random.nextDouble() - 0.5) * 2 * BLOB_MAX_SPEED,
+ (random.nextDouble() - 0.5) * 2 * BLOB_MAX_SPEED);
+ blob.setUserData(velocity);
+ blobs.add(blob);
+ }
+ backgroundBlobLayer.getChildren().addAll(blobs);
+ backgroundBlobLayer.setEffect(new GaussianBlur(BLOB_GAUSSIAN_BLUR));
+ backgroundBlobLayer.setBlendMode(BlendMode.ADD);
+ backgroundBlobLayer.setVisible(false);
+ }
+
+ private void createWaveLayer() {
+ waveLayer = new Group();
+ waveLayer.setOpacity(0.9);
+ waveLayer.setEffect(new GaussianBlur(WAVE_BLUR_AMOUNT));
+ waveLayer.setBlendMode(BlendMode.ADD);
+ }
+
+ private void updateSiriBackground() {
+ if (!allColorsCollected) {
+ return;
+ }
+
+ if (!backgroundBlobLayer.isVisible()) {
+ backgroundBlobLayer.setVisible(true);
+ }
+
+ double appW = FXGL.getAppWidth();
+ double appH = FXGL.getAppHeight();
+ for (var node : blobs) {
+ var blob = (Circle) node;
+ var v = (Point2D) blob.getUserData();
+ blob.setCenterX(blob.getCenterX() + v.getX());
+ blob.setCenterY(blob.getCenterY() + v.getY());
+ if (blob.getCenterX() - blob.getRadius() < 0
+ || blob.getCenterX() + blob.getRadius() > appW) {
+ v = new Point2D(-v.getX(), v.getY());
+ }
+ if (blob.getCenterY() - blob.getRadius() < 0
+ || blob.getCenterY() + blob.getRadius() > appH) {
+ v = new Point2D(v.getX(), -v.getY());
+ }
+ blob.setUserData(v);
+ }
+ }
+
+ private Path createWavePath(Color color, double timeOffset, double yOffset) {
+ var wavePath = new Path();
+ wavePath.setStroke(color);
+ wavePath.setStrokeWidth(WAVE_STROKE_WIDTH);
+ wavePath.setFill(null);
+ wavePath.setSmooth(true);
+ double width = FXGL.getAppWidth();
+ var step = WAVE_STEP;
+ var adjustedTime = time + timeOffset;
+ var baseY = WAVE_Y_POSITION + yOffset;
+ var firstX = -step;
+ var firstY = baseY + Math.sin(firstX / 100 + adjustedTime * WAVE_SPEED) * WAVE_AMPLITUDE;
+ wavePath.getElements().add(new MoveTo(firstX, firstY));
+ for (double x = 0; x <= width + step; x += step) {
+ var prevX = x - step;
+ var prevY = baseY + Math.sin(prevX / 100 + adjustedTime * WAVE_SPEED) * WAVE_AMPLITUDE;
+ var currentY = baseY + Math.sin(x / 100 + adjustedTime * WAVE_SPEED) * WAVE_AMPLITUDE;
+ wavePath.getElements()
+ .add(
+ new CubicCurveTo(
+ prevX + step / 2, prevY, x - step / 2, currentY, x, currentY));
+ }
+ return wavePath;
+ }
+
+ public void addColorToWave(Color color) {
+ if (color != null && !allColorsCollected) {
+ if (collectedColors.add(color)) {
+ if (collectedColors.containsAll(TARGET_COLORS)) {
+ allColorsCollected = true;
+ System.out.println("Tất cả các màu đã được thu thập! Kích hoạt nền động.");
+ }
+ }
+ }
+ }
+
+ private void updateWaves() {
+ waveLayer.getChildren().clear();
+ var whiteWave = createWavePath(Color.WHITE, 0.0, 0.0);
+ waveLayer.getChildren().add(whiteWave);
+ var i = 0;
+ for (var brickColor : collectedColors) {
+ var yOffset = (i + 1) * WAVE_SEPARATION;
+ var timeOffsetValue = (i + 1) * WAVE_TIME_OFFSET;
+ var coloredWave = createWavePath(brickColor, timeOffsetValue, yOffset);
+ waveLayer.getChildren().add(coloredWave);
+ i++;
+ }
+ }
+
+ private void startAnimationLoop() {
+ // Dừng timer cũ trước khi tạo timer mới để tránh chạy nhiều timer song song
+ if (animationTimer != null) {
+ animationTimer.stop();
+ }
+
+ // Gán timer mới cho biến thành viên
+ animationTimer =
+ new AnimationTimer() {
+ @Override
+ public void handle(long now) {
+ time += 0.01;
+ updateWaves();
+ updateSiriBackground();
+ }
+ };
+ animationTimer.start();
+ }
+
+ private static final class Holder {
+
+ static final UISystem INSTANCE = new UISystem();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/manager/Manager.java b/src/main/java/com/github/codestorm/bounceverse/systems/manager/Manager.java
new file mode 100644
index 0000000..ffa854d
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/manager/Manager.java
@@ -0,0 +1,12 @@
+package com.github.codestorm.bounceverse.systems.manager;
+
+import com.github.codestorm.bounceverse.systems.System;
+
+/**
+ *
+ *
+ * {@link Manager}
+ *
+ * Hệ thống quản lý một thuộc tính nào đó tồn tại trong game.
+ */
+public abstract class Manager extends System {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/manager/metrics/LeaderboardManager.java b/src/main/java/com/github/codestorm/bounceverse/systems/manager/metrics/LeaderboardManager.java
new file mode 100644
index 0000000..fc703c5
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/manager/metrics/LeaderboardManager.java
@@ -0,0 +1,129 @@
+package com.github.codestorm.bounceverse.systems.manager.metrics;
+
+import com.almasb.fxgl.core.serialization.Bundle;
+import com.almasb.fxgl.logging.Logger;
+import com.github.codestorm.bounceverse.typing.records.EndlessScore;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.MinMaxPriorityQueue;
+
+import org.jspecify.annotations.NonNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ *
+ *
+ * {@link LeaderboardManager}
+ *
+ * Quản lý Bảng xếp hạng.
+ *
+ * @apiNote Đây là một Singleton, cần lấy instance thông qua {@link #getInstance()}.
+ */
+public final class LeaderboardManager extends MetricsManager {
+ public static final int MAX_SIZE = 10;
+ public static final String FILENAME = "leaderboard.dat";
+ private static final String ENDLESS = "endless";
+
+ public static LeaderboardManager getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ /** Tải lại thông tin BXH. */
+ public void reload() {
+ final var file = new File(FILENAME);
+ try {
+ final var ois = new ObjectInputStream(new FileInputStream(file));
+ final var bundle = (Bundle) ois.readObject();
+
+ final ArrayList<@NonNull EndlessScore> endlessLB = bundle.get(ENDLESS);
+
+ endlessLeaderboard.clear();
+ endlessLeaderboard.addAll(endlessLB);
+
+ Logger.get(LeaderboardManager.class)
+ .infof("Loaded leaderboard from: %s", file.getAbsolutePath());
+
+ } catch (Exception e) {
+ Logger.get(LeaderboardManager.class)
+ .warning("Leaderboard file not found or corrupted. Using new one.", e);
+ }
+ }
+
+ /** Lưu lại BXH. */
+ public void save() {
+ final var leaderboard = new Bundle("leaderboard");
+ leaderboard.put(ENDLESS, new ArrayList<>(endlessLeaderboard));
+
+ try {
+ final var file = new File(FILENAME);
+ final var oos = new ObjectOutputStream(new FileOutputStream(file));
+ oos.writeObject(leaderboard);
+ Logger.get(LeaderboardManager.class)
+ .infof("Saved leaderboard to: %s", file.getAbsolutePath());
+ oos.close();
+ } catch (Exception e) {
+ Logger.get(LeaderboardManager.class).warning("Cannot save leaderboard ", e);
+ }
+ }
+
+ private LeaderboardManager() {
+ reload();
+ }
+
+ private final MinMaxPriorityQueue<@NonNull EndlessScore> endlessLeaderboard =
+ MinMaxPriorityQueue.orderedBy(Comparator.reverseOrder())
+ .maximumSize(MAX_SIZE)
+ .create();
+
+ /**
+ * Kiểm tra xem điểm số có nằm trong top leaderboard không.
+ *
+ * @param score Điểm số cần kiểm tra
+ * @return true nếu điểm số đủ để vào leaderboard
+ */
+ public boolean isTopScore(int score) {
+ if (endlessLeaderboard.size() < LeaderboardManager.MAX_SIZE) {
+ return true;
+ }
+ final var lowestScore = endlessLeaderboard.peek();
+ assert lowestScore != null;
+ return score > lowestScore.score();
+ }
+
+ /**
+ * Thêm một điểm số mới vào leaderboard nếu đủ điều kiện.
+ *
+ * @param newScore Điểm số mới
+ */
+ public void addScore(EndlessScore newScore) {
+ if (!isTopScore(newScore.score())) {
+ return;
+ }
+ endlessLeaderboard.add(newScore);
+ save();
+ }
+
+ /**
+ * Lấy danh sách leaderboard hiện tại dưới dạng {@link ImmutableList}.
+ *
+ * @return Danh sách leaderboard
+ */
+ public ImmutableList<@NonNull EndlessScore> getViewLeaderboard() {
+ return ImmutableList.copyOf(endlessLeaderboard);
+ }
+
+ /**
+ * Lazy-loaded singleton holder.
+ * Follow
+ * Initialization-on-demand holder idiom .
+ */
+ private static final class Holder {
+ static final LeaderboardManager INSTANCE = new LeaderboardManager();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/manager/metrics/MetricsManager.java b/src/main/java/com/github/codestorm/bounceverse/systems/manager/metrics/MetricsManager.java
new file mode 100644
index 0000000..ed43c08
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/manager/metrics/MetricsManager.java
@@ -0,0 +1,12 @@
+package com.github.codestorm.bounceverse.systems.manager.metrics;
+
+import com.github.codestorm.bounceverse.systems.manager.Manager;
+
+/**
+ *
+ *
+ * {@link MetricsManager}
+ *
+ * Quản lý các thông số (sau khi chơi) đến từ người chơi.
+ */
+public abstract class MetricsManager extends Manager {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/GameSettingsManager.java b/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/GameSettingsManager.java
new file mode 100644
index 0000000..6077fde
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/GameSettingsManager.java
@@ -0,0 +1,75 @@
+package com.github.codestorm.bounceverse.systems.manager.settings;
+
+import com.almasb.fxgl.app.ApplicationMode;
+import com.almasb.fxgl.app.GameSettings;
+import com.almasb.fxgl.app.MenuItem;
+import com.almasb.fxgl.app.ReadOnlyGameSettings;
+import com.almasb.fxgl.dsl.FXGL;
+import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.factory.SceneFactory;
+
+import javafx.stage.StageStyle;
+
+import java.io.IOException;
+import java.util.EnumSet;
+
+/**
+ *
+ *
+ * {@link GameSettingsManager}
+ *
+ * Trình quản lý việc apply {@link GameSettings}.
+ *
+ * @see UserSettingsManager
+ */
+public final class GameSettingsManager extends SettingsManager {
+ private GameSettingsManager() {}
+
+ /**
+ * Tải các settings từ file đã thiết lập vào bộ nhớ.
+ * Cần tải {@link LaunchOptionsManager#load(String...)} và {@link UserSettingsManager#load()}
+ * trước khi dùng.
+ *
+ * @param settings Nơi tải vào
+ * @throws IOException if an error occurred when reading from the input stream.
+ */
+ public static void load(GameSettings settings) throws IOException {
+ final var gameSettings = Utilities.IO.loadProperties("/settings.properties");
+
+ // ? General
+ settings.setTitle(gameSettings.getProperty("general.name"));
+ settings.setVersion(gameSettings.getProperty("general.version"));
+ settings.setCredits(Utilities.IO.readTextFile("/assets/credits.txt"));
+ settings.setApplicationMode(
+ Boolean.parseBoolean(gameSettings.getProperty("general.devMode"))
+ ? ApplicationMode.DEVELOPER
+ : (LaunchOptionsManager.getInstance().get().debug())
+ ? ApplicationMode.DEBUG
+ : ApplicationMode.RELEASE);
+
+ // ? Display
+ settings.setWidth(Integer.parseInt(gameSettings.getProperty("video.width")));
+ settings.setHeight(Integer.parseInt(gameSettings.getProperty("video.height")));
+ settings.setStageStyle(StageStyle.DECORATED);
+ settings.setManualResizeEnabled(false);
+ settings.setFullScreenAllowed(true);
+ settings.setFullScreenFromStart(
+ UserSettingsManager.getInstance().get().getVideo().isFullscreen());
+
+ // ? Game
+ settings.setSceneFactory(new SceneFactory());
+ settings.setMainMenuEnabled(true);
+ // QUAN TRỌNG: Đảm bảo dòng này là 'true'
+ settings.setIntroEnabled(true);
+ settings.setEnabledMenuItems(EnumSet.of(MenuItem.EXTRA, MenuItem.SAVE_LOAD));
+ }
+
+ /**
+ * Lây settings trong game.
+ *
+ * @return Settings trong game (Read-only)
+ */
+ public static ReadOnlyGameSettings get() {
+ return FXGL.getSettings();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/LaunchOptionsManager.java b/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/LaunchOptionsManager.java
new file mode 100644
index 0000000..0b5bc48
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/LaunchOptionsManager.java
@@ -0,0 +1,48 @@
+package com.github.codestorm.bounceverse.systems.manager.settings;
+
+import com.github.codestorm.bounceverse.Utilities;
+import com.github.codestorm.bounceverse.typing.records.LaunchOptions;
+
+/**
+ *
+ *
+ * {@link LaunchOptionsManager}
+ *
+ * Các tùy chọn khi khởi động được áp dụng trong game.
+ * Sử dụng {@link #load(String...)} để tải các options.
+ *
+ * @apiNote Đây là một Singleton, cần lấy instance thông qua {@link #getInstance()}.
+ */
+public final class LaunchOptionsManager extends SettingsManager {
+ public static LaunchOptionsManager getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ private LaunchOptions options;
+
+ private LaunchOptionsManager() {}
+
+ /**
+ * Tải các launch options từ Command-line Arguments.
+ *
+ * @param args Command-Line Arguments - được truyền vào từ hàm {@code main()}
+ */
+ public void load(String... args) {
+ final var map = Utilities.IO.parseArgs(args, null, null);
+
+ options = new LaunchOptions(Boolean.parseBoolean(map.getOrDefault("debug", "false")));
+ }
+
+ public LaunchOptions get() {
+ return options;
+ }
+
+ /**
+ * Lazy-loaded singleton holder.
+ * Follow
+ * Initialization-on-demand holder idiom .
+ */
+ private static final class Holder {
+ static final LaunchOptionsManager INSTANCE = new LaunchOptionsManager();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/SettingsManager.java b/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/SettingsManager.java
new file mode 100644
index 0000000..786b58d
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/SettingsManager.java
@@ -0,0 +1,12 @@
+package com.github.codestorm.bounceverse.systems.manager.settings;
+
+import com.github.codestorm.bounceverse.systems.manager.Manager;
+
+/**
+ *
+ *
+ * {@link SettingsManager}
+ *
+ * Quản lý các thiết lập của trò chơi/application.
+ */
+abstract class SettingsManager extends Manager {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/UserSettingsManager.java b/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/UserSettingsManager.java
new file mode 100644
index 0000000..f4e0bad
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/systems/manager/settings/UserSettingsManager.java
@@ -0,0 +1,166 @@
+package com.github.codestorm.bounceverse.systems.manager.settings;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.logging.Logger;
+import com.github.codestorm.bounceverse.typing.records.UserSettings;
+import com.moandjiezana.toml.Toml;
+import com.moandjiezana.toml.TomlWriter;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ *
+ *
+ * {@link UserSettingsManager}
+ *
+ * Quản lý {@link UserSettings}.
+ *
+ * @apiNote Đây là một Singleton, cần lấy instance thông qua {@link #getInstance()}.
+ * @see GameSettingsManager
+ */
+public final class UserSettingsManager extends SettingsManager {
+ private static final String FORMAT = "toml";
+
+ public static UserSettingsManager getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ private UserSettings profile;
+
+ private UserSettingsManager() {}
+
+ /**
+ * Lấy tên file setting tương ứng của người chơi.
+ *
+ * @return Tên file
+ */
+ public static String getSettingsFilename() {
+ final var username = System.getProperty("user.name");
+ return String.format("settings.%s.%s", username, FORMAT);
+ }
+
+ /**
+ * Lấy địa chỉ file settings tương ứng của người chơi.
+ *
+ * @return Địa chỉ file setting tuyệt đối
+ */
+ public static Path getSettingsFilepath() {
+ return Paths.get(getSettingsFilename()).toAbsolutePath();
+ }
+
+ /**
+ * Áp dụng setting {@link #profile} mà đã được lưu vào bộ nhớ.
+ *
+ * @apiNote Chỉ thực thi khi {@link FXGL} có thể sử dụng an toàn
+ */
+ public void apply() {
+ // ? Video
+ FXGL.getPrimaryStage().setFullScreen(profile.getVideo().isFullscreen());
+
+ // ? Audio
+ FXGL.getSettings().setGlobalMusicVolume(profile.getAudio().getMusic());
+ FXGL.getSettings().setGlobalSoundVolume(profile.getAudio().getSound());
+ }
+
+ /**
+ * Load settings của file {@link #getSettingsFilepath()} vào bộ nhớ {@link #profile}.
+ *
+ * @apiNote Chỉ load vào bộ nhớ, không áp dụng. Hãy dùng {@link #apply()}.
+ */
+ public void load() {
+ final var filepath = getSettingsFilepath();
+ try {
+ final var file = new File(filepath.toUri());
+ final var parsed = new Toml().read(file);
+ profile = parsed.to(UserSettings.class);
+ Logger.get(UserSettingsManager.class).infof("Read user settings in: %s", filepath);
+ return;
+ } catch (Exception e) {
+ Logger.get(UserSettingsManager.class)
+ .warning("Cannot read user settings in: " + filepath, e);
+ }
+ Logger.get(UserSettingsManager.class).info("Using default user settings");
+ profile = new UserSettings();
+ }
+
+ /**
+ * Cập nhật {@link #profile} trong instance với giá trị thực tế.
+ *
+ * @apiNote Chỉ thực thi khi {@link FXGL} có thể sử dụng an toàn
+ */
+ public void sync() {
+ // ? Video
+ profile.getVideo().setFullscreen(FXGL.getSettings().getFullScreen().get());
+
+ // ? Audio
+ profile.getAudio().setMusic(FXGL.getSettings().getGlobalMusicVolume());
+ profile.getAudio().setSound(FXGL.getSettings().getGlobalSoundVolume());
+
+ // ? Controls
+ syncControls();
+ }
+
+ /** Đồng bộ controls settings từ input bindings hiện tại. */
+ private void syncControls() {
+ var bindings = FXGL.getInput().getAllBindings();
+
+ for (var entry : bindings.entrySet()) {
+ var action = entry.getKey();
+ var trigger = entry.getValue();
+
+ var triggerStr = trigger.toString();
+
+ switch (action.getName()) {
+ case "Move Left":
+ profile.getControls().setMoveLeft(triggerStr);
+ break;
+ case "Move Right":
+ profile.getControls().setMoveRight(triggerStr);
+ break;
+ case "Launch Ball":
+ profile.getControls().setLaunchBall(triggerStr);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Lấy Settings người dùng {@link #profile} trong bộ nhớ.
+ *
+ * @apiNote Sử dụng {@link #sync()} để cập nhật settings với giá trị thực tế.
+ * @return Settings người dùng
+ */
+ public UserSettings get() {
+ return profile;
+ }
+
+ /**
+ * Lưu thiết lập game tại {@link #getSettingsFilepath()}.
+ *
+ * @apiNote Cập nhật settings đến thời điểm mới nhất bằng {@link #sync()}
+ */
+ public void save() {
+ final var filepath = getSettingsFilepath();
+ final var file = new File(filepath.toUri());
+ final var writer = new TomlWriter();
+ try {
+ writer.write(profile, file);
+ Logger.get(UserSettingsManager.class).infof("Saved user settings to: %s", filepath);
+ } catch (IOException e) {
+ Logger.get(UserSettingsManager.class)
+ .fatal("Cannot save user settings to: " + filepath, e);
+ }
+ }
+
+ /**
+ * Lazy-loaded singleton holder.
+ * Follow
+ * Initialization-on-demand holder idiom .
+ */
+ private static final class Holder {
+ static final UserSettingsManager INSTANCE = new UserSettingsManager();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/annotations/ForEntity.java b/src/main/java/com/github/codestorm/bounceverse/typing/annotations/OnlyForEntity.java
similarity index 58%
rename from src/main/java/com/github/codestorm/bounceverse/typing/annotations/ForEntity.java
rename to src/main/java/com/github/codestorm/bounceverse/typing/annotations/OnlyForEntity.java
index 5d92ae2..e4ad424 100644
--- a/src/main/java/com/github/codestorm/bounceverse/typing/annotations/ForEntity.java
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/annotations/OnlyForEntity.java
@@ -3,6 +3,7 @@
import com.almasb.fxgl.entity.component.Component;
import com.github.codestorm.bounceverse.Utilities;
import com.github.codestorm.bounceverse.typing.enums.EntityType;
+
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -11,18 +12,26 @@
/**
*
*
- * @{@link ForEntity}
+ * @{@link OnlyForEntity}
*
- * Đá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 {}}.
+ * Ràng buộc đánh dấu class chỉ định là chỉ phù hợp cho entity nào.
*
* Sử dụng {@link Utilities.Compatibility#throwIfNotCompatible(EntityType, Component...)} để kiểm
- * tra.
+ * tra.
+ *
+ *
+ * Ví dụ
+ *
+ *
+ * Cho phép tất cả: Không thêm annotation này.
+ * Cho phép cụ thể: {@code @OnlyForEntity(A)} hoặc {@code @OnlyForEntity({A, B, C})}.
+ * Không cho phép: {@code @OnlyForEntity({})}.
+ *
*
* @see EntityType
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
-public @interface ForEntity {
+public @interface OnlyForEntity {
EntityType[] value();
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/enums/BrickType.java b/src/main/java/com/github/codestorm/bounceverse/typing/enums/BrickType.java
new file mode 100644
index 0000000..3f32df4
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/enums/BrickType.java
@@ -0,0 +1,16 @@
+package com.github.codestorm.bounceverse.typing.enums;
+
+/**
+ *
+ *
+ * {@link BrickType}
+ *
+ * Các loại {@link EntityType#BRICK}.
+ */
+public enum BrickType {
+ NORMAL,
+ STRONG,
+ SHIELD,
+ EXPLODING,
+ KEY
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/enums/CollisionGroup.java b/src/main/java/com/github/codestorm/bounceverse/typing/enums/CollisionGroup.java
new file mode 100644
index 0000000..38a0fbf
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/enums/CollisionGroup.java
@@ -0,0 +1,19 @@
+package com.github.codestorm.bounceverse.typing.enums;
+
+/**
+ * Định nghĩa các nhóm va chạm vật lý cho Collision Filtering. Mỗi giá trị là một lũy thừa của 2 để
+ * có thể kết hợp bằng phép toán bitwise OR.
+ */
+public enum CollisionGroup {
+ BALL(1), // 2^0
+ BULLET(2), // 2^1
+ PADDLE(4), // 2^2
+ BRICK(8), // 2^3
+ WALL(16); // 2^4
+
+ public final int bits;
+
+ CollisionGroup(int bits) {
+ this.bits = bits;
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/enums/DirectionUnit.java b/src/main/java/com/github/codestorm/bounceverse/typing/enums/DirectionUnit.java
index 6e6fa4c..f1a4e7c 100644
--- a/src/main/java/com/github/codestorm/bounceverse/typing/enums/DirectionUnit.java
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/enums/DirectionUnit.java
@@ -2,13 +2,16 @@
import com.almasb.fxgl.core.math.Vec2;
+import javafx.geometry.Side;
+
/**
*
*
- * #{@link DirectionUnit}
+ * {@link DirectionUnit}
*
- * Vector đơn vị đại diện cho hướng di chuyển.
+ * Vector đơn vị đại diện cho hướng di chuyển .
*
+ * @apiNote Tránh nhầm lẫn với {@link javafx.geometry.Side}
* @see Vec2
*/
public enum DirectionUnit {
@@ -24,12 +27,22 @@ public enum DirectionUnit {
this.vector = new Vec2(vx, vy).normalize();
}
- /**
- * Lấy vector {@link Vec2} tương ứng.
- *
- * @return Vector
- */
+ /** Lấy vector {@link Vec2} tương ứng. */
public Vec2 getVector() {
return vector;
}
+
+ /**
+ * Chuyển hướng logic (DirectionUnit) sang hướng hình học (Side). Dùng trong PhysicSystem để
+ * kiểm tra va chạm với Shield.
+ */
+ public Side toSide() {
+ return switch (this) {
+ case UP -> Side.TOP;
+ case DOWN -> Side.BOTTOM;
+ case LEFT -> Side.LEFT;
+ case RIGHT -> Side.RIGHT;
+ case STAND -> null;
+ };
+ }
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/enums/EntityType.java b/src/main/java/com/github/codestorm/bounceverse/typing/enums/EntityType.java
index 284d6ac..d98f86c 100644
--- a/src/main/java/com/github/codestorm/bounceverse/typing/enums/EntityType.java
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/enums/EntityType.java
@@ -9,9 +9,9 @@
* #{@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.
*
+ * @apiNote 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
*/
@@ -21,5 +21,6 @@ public enum EntityType {
BALL,
POWER_UP,
BULLET,
- WALL
+ WALL,
+ PADDLE_CLONE
}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/enums/PowerUpType.java b/src/main/java/com/github/codestorm/bounceverse/typing/enums/PowerUpType.java
new file mode 100644
index 0000000..0a68865
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/enums/PowerUpType.java
@@ -0,0 +1,13 @@
+package com.github.codestorm.bounceverse.typing.enums;
+
+public enum PowerUpType {
+ EXPAND_PADDLE,
+ SHRINK_PADDLE,
+ REVERSE_PADDLE,
+ MULTI_BALL,
+ SLOW_BALL,
+ FAST_BALL,
+ SHIELD,
+ GUN,
+ EXTRA_LIFE
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/CanUndo.java b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/CanUndo.java
deleted file mode 100644
index d9f5302..0000000
--- a/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/CanUndo.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.codestorm.bounceverse.typing.interfaces;
-
-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.
- *
- * @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/typing/interfaces/CanExecute.java b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/Executable.java
similarity index 82%
rename from src/main/java/com/github/codestorm/bounceverse/typing/interfaces/CanExecute.java
rename to src/main/java/com/github/codestorm/bounceverse/typing/interfaces/Executable.java
index 762d7c4..7825d66 100644
--- a/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/CanExecute.java
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/Executable.java
@@ -5,11 +5,11 @@
/**
*
*
- * %{@link CanExecute}
+ * %{@link Executable}
*
* Có thể thực thi hành động nào đó.
*/
-public interface CanExecute {
+public interface Executable {
/**
* Thực thi hành động.
*
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/HasCooldown.java b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/HasCooldown.java
new file mode 100644
index 0000000..f6db2fe
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/HasCooldown.java
@@ -0,0 +1,41 @@
+package com.github.codestorm.bounceverse.typing.interfaces;
+
+import com.github.codestorm.bounceverse.typing.structures.Cooldown;
+
+import java.util.List;
+
+/**
+ *
+ *
+ * %{@link HasCooldown}
+ *
+ * {@link Executable} nhưng bị giới hạn hồi chiêu ({@link Cooldown})
+ *
+ * @see Cooldown
+ */
+public interface HasCooldown extends Executable {
+ Cooldown getCooldown();
+
+ /**
+ * Thực thi Logic bên trong wrapper {@link Executable#execute(List)}.
+ *
+ * @param data Dữ liệu truyền vào
+ * @return {@code true} nếu cho phép thực thi, ngược lại {@code false}
+ */
+ boolean executeLogic(List data);
+
+ /**
+ * @deprecated Viết logic lên {@link #executeLogic(List)} thay vì method này.
+ * @param data Dữ liệu truyền vào
+ */
+ @Deprecated
+ @Override
+ default void execute(List data) {
+ if (!getCooldown().getCurrent().isExpired()) {
+ return;
+ }
+ if (executeLogic(data)) {
+ getCooldown().getCurrent().createNew();
+ }
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/ScoreLike.java b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/ScoreLike.java
new file mode 100644
index 0000000..1720921
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/ScoreLike.java
@@ -0,0 +1,27 @@
+package com.github.codestorm.bounceverse.typing.interfaces;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+/**
+ *
+ *
+ * %{@link ScoreLike}
+ *
+ * Giống Điểm số trong trò chơi.
+ *
+ * @param Loại Điểm số
+ */
+public interface ScoreLike> extends Serializable, Comparable {
+ int score();
+
+ Instant timestamp();
+
+ @Override
+ default int compareTo(T o) {
+ if (score() != o.score()) {
+ return Integer.compare(score(), o.score());
+ }
+ return -timestamp().compareTo(o.timestamp());
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/Undoable.java b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/Undoable.java
new file mode 100644
index 0000000..c802534
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/interfaces/Undoable.java
@@ -0,0 +1,119 @@
+package com.github.codestorm.bounceverse.typing.interfaces;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.time.TimerAction;
+import com.github.codestorm.bounceverse.components.Behavior;
+
+import javafx.util.Duration;
+
+import java.util.List;
+
+/**
+ *
+ *
+ * %{@link Undoable}
+ *
+ * Có thể thực thi và hoàn tác được.
+ */
+public interface Undoable extends Executable {
+ /**
+ * Lấy duration để tự động hoàn tác.
+ *
+ * @return Duration
+ */
+ Duration getDuration();
+
+ /**
+ * Lấy trạng thái có xóa behavior khi undo không.
+ *
+ * @return {@code true} nếu xóa, {@code false} nếu không
+ */
+ boolean isRemoveWhenUndo();
+
+ /**
+ * Lấy dữ liệu đã được modified.
+ *
+ * @return Dữ liệu modified
+ */
+ List getModified();
+
+ /**
+ * Set dữ liệu modified.
+ *
+ * @param modified Dữ liệu modified
+ */
+ void setModified(List modified);
+
+ /**
+ * Lấy timer action hiện tại.
+ *
+ * @return TimerAction
+ */
+ TimerAction getCurrent();
+
+ /**
+ * Set timer action hiện tại.
+ *
+ * @param current TimerAction
+ */
+ void setCurrent(TimerAction current);
+
+ /**
+ * Có phải hành động đã được thực thi không.
+ *
+ * @return {@code true} nếu đã thực thi, {@code false} nếu chưa
+ */
+ default boolean isActive() {
+ return getCurrent() != null && getCurrent().isExpired() && getModified() != null;
+ }
+
+ /**
+ * Logic bên trong {@link #execute(List)}.
+ *
+ * @param data Dữ liệu truyền vào
+ * @return {@code null} nếu không thực thi, {@link List} các dữ liệu cần hoàn tác lại sau này
+ * nếu ngược lại
+ */
+ List executeLogic(List data);
+
+ /**
+ * Logic bên trong {@link Undoable#undo()}.
+ *
+ * @param data Dữ liệu cần hoàn tác
+ * @return {@code true} nếu cho phép hoàn tác, {@code false} nếu không.
+ */
+ boolean undoLogic(List data);
+
+ /**
+ * @deprecated Viết logic lên {@link #executeLogic(List)} thay vì method này.
+ * @param data Dữ liệu truyền vào
+ */
+ @Deprecated
+ @Override
+ default void execute(List data) {
+ if (isActive()) {
+ return;
+ }
+
+ var modified = executeLogic(data);
+ if (modified != null) {
+ setModified(modified);
+ setCurrent(FXGL.getGameTimer().runOnceAfter(this::undo, getDuration()));
+ }
+ }
+
+ /** Hoàn tác trạng thái về trước đó. */
+ default void undo() {
+ if (!isActive()) {
+ return;
+ }
+
+ if (undoLogic(getModified())) {
+ setCurrent(null);
+ setModified(null);
+ if (isRemoveWhenUndo() && this instanceof Behavior behavior) {
+ behavior.getEntity().removeComponent(behavior.getClass());
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/records/EndlessScore.java b/src/main/java/com/github/codestorm/bounceverse/typing/records/EndlessScore.java
new file mode 100644
index 0000000..64843a8
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/records/EndlessScore.java
@@ -0,0 +1,20 @@
+package com.github.codestorm.bounceverse.typing.records;
+
+import com.github.codestorm.bounceverse.typing.interfaces.ScoreLike;
+
+import java.time.Instant;
+
+/**
+ *
+ *
+ * ${@link EndlessScore}
+ *
+ * Biểu diễn điểm số Endless mode.
+ *
+ * @param name Tên người chơi
+ * @param score Điểm số
+ * @param level Cấp độ đạt được
+ * @param timestamp Thời điểm ghi điểm
+ */
+public record EndlessScore(String name, int score, int level, Instant timestamp)
+ implements ScoreLike {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/records/LaunchOptions.java b/src/main/java/com/github/codestorm/bounceverse/typing/records/LaunchOptions.java
new file mode 100644
index 0000000..fd0976e
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/records/LaunchOptions.java
@@ -0,0 +1,12 @@
+package com.github.codestorm.bounceverse.typing.records;
+
+/**
+ *
+ *
+ * ${@link LaunchOptions}
+ *
+ * Biểu diễn Launch Options.
+ *
+ * @param debug Bật/Tắt chế độ DEBUG
+ */
+public record LaunchOptions(boolean debug) {}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/records/UserSettings.java b/src/main/java/com/github/codestorm/bounceverse/typing/records/UserSettings.java
new file mode 100644
index 0000000..bf82a23
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/records/UserSettings.java
@@ -0,0 +1,114 @@
+package com.github.codestorm.bounceverse.typing.records;
+
+import com.github.codestorm.bounceverse.systems.manager.settings.UserSettingsManager;
+
+/**
+ *
+ *
+ * {@link UserSettings}
+ *
+ * Biểu diễn Settings của người dùng.
+ *
+ * @see UserSettingsManager
+ */
+public final class UserSettings {
+ private final Video video = new Video();
+ private final Audio audio = new Audio();
+ private final Controls controls = new Controls();
+
+ public Video getVideo() {
+ return video;
+ }
+
+ public Audio getAudio() {
+ return audio;
+ }
+
+ public Controls getControls() {
+ return controls;
+ }
+
+ /** Setting về Hình ảnh. */
+ public static final class Video {
+ private double width = 1280;
+ private double height = 960;
+ private boolean fullscreen = false;
+
+ public double getWidth() {
+ return width;
+ }
+
+ public void setWidth(double width) {
+ this.width = width;
+ }
+
+ public double getHeight() {
+ return height;
+ }
+
+ public void setHeight(double height) {
+ this.height = height;
+ }
+
+ public boolean isFullscreen() {
+ return fullscreen;
+ }
+
+ public void setFullscreen(boolean fullscreen) {
+ this.fullscreen = fullscreen;
+ }
+ }
+
+ /** Setting về Âm thanh */
+ public static final class Audio {
+ private double music = 1;
+ private double sound = 1;
+
+ public double getMusic() {
+ return music;
+ }
+
+ public void setMusic(double music) {
+ this.music = music;
+ }
+
+ public double getSound() {
+ return sound;
+ }
+
+ public void setSound(double sound) {
+ this.sound = sound;
+ }
+ }
+
+ /** Setting về Controls/Phím điều khiển */
+ public static final class Controls {
+ private String moveLeft = "LEFT";
+ private String moveRight = "RIGHT";
+ private String launchBall = "SPACE";
+
+ public String getMoveLeft() {
+ return moveLeft;
+ }
+
+ public void setMoveLeft(String moveLeft) {
+ this.moveLeft = moveLeft;
+ }
+
+ public String getMoveRight() {
+ return moveRight;
+ }
+
+ public void setMoveRight(String moveRight) {
+ this.moveRight = moveRight;
+ }
+
+ public String getLaunchBall() {
+ return launchBall;
+ }
+
+ public void setLaunchBall(String launchBall) {
+ this.launchBall = launchBall;
+ }
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/structures/Cooldown.java b/src/main/java/com/github/codestorm/bounceverse/typing/structures/Cooldown.java
new file mode 100644
index 0000000..04ea173
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/structures/Cooldown.java
@@ -0,0 +1,107 @@
+package com.github.codestorm.bounceverse.typing.structures;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.time.TimerAction;
+
+import javafx.util.Duration;
+
+/**
+ * Lớp Cooldown đã được sửa lỗi dứt điểm. Logic tính toán thời gian còn lại đã được chuyển ra đúng
+ * chỗ và hoạt động chính xác.
+ */
+public class Cooldown {
+
+ protected final ActiveCooldown current = new ActiveCooldown();
+ protected Duration duration = Duration.INDEFINITE;
+
+ public Duration getDuration() {
+ return duration;
+ }
+
+ public void setDuration(Duration duration) {
+ this.duration = duration;
+ }
+
+ public ActiveCooldown getCurrent() {
+ return current;
+ }
+
+ public Duration getTimeLeft() {
+ if (current.isExpired()) {
+ return Duration.ZERO;
+ }
+
+ // Tính thời gian đã trôi qua kể từ khi cooldown bắt đầu
+ final var elapsed = Duration.millis(FXGL.getGameTimer().getNow() - current.timestamp);
+
+ // Lấy tổng thời gian trừ đi thời gian đã trôi qua
+ var timeLeft = duration.subtract(elapsed);
+
+ // Đảm bảo không bao giờ trả về giá trị âm
+ return timeLeft.lessThan(Duration.ZERO) ? Duration.ZERO : timeLeft;
+ }
+
+ public Cooldown() {}
+
+ public Cooldown(Duration duration) {
+ this.duration = duration;
+ }
+
+ // Lớp con ActiveCooldown giờ chỉ tập trung quản lý trạng thái (start, stop, pause)
+ public class ActiveCooldown {
+
+ protected TimerAction waiter = null;
+ protected double timestamp = Double.NaN; // Thời điểm cooldown bắt đầu
+ protected Runnable onExpiredCallback = null;
+
+ protected void onExpired() {
+ timestamp = Double.NaN;
+ if (onExpiredCallback != null) {
+ onExpiredCallback.run();
+ }
+ }
+
+ public void setOnExpired(Runnable callback) {
+ this.onExpiredCallback = callback;
+ }
+
+ public boolean isExpired() {
+ return (waiter == null) || waiter.isExpired();
+ }
+
+ public void expire() {
+ if (!isExpired()) {
+ waiter.expire();
+ }
+ }
+
+ public void createNew() {
+ if (waiter != null && !waiter.isExpired()) {
+ waiter.expire();
+ }
+
+ var gameTimer = FXGL.getGameTimer();
+ waiter = gameTimer.runOnceAfter(() -> timestamp = Double.NaN, duration);
+
+ timestamp = gameTimer.getNow();
+ }
+
+ public void pause() {
+ if (!isExpired()) {
+ waiter.pause();
+ }
+ }
+
+ public void resume() {
+ if (!isExpired()) {
+ waiter.resume();
+ }
+ }
+
+ public boolean isPaused() {
+ return !isExpired() && waiter.isPaused();
+ }
+
+ protected ActiveCooldown() {}
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/typing/structures/HealthIntValue.java b/src/main/java/com/github/codestorm/bounceverse/typing/structures/HealthIntValue.java
new file mode 100644
index 0000000..173e7e7
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/typing/structures/HealthIntValue.java
@@ -0,0 +1,179 @@
+package com.github.codestorm.bounceverse.typing.structures;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ *
+ * {@link HealthIntValue}
+ *
+ * Giá trị máu (health) kiểu số nguyên với giới hạn tối đa.
+ * Cung cấp các phương thức để quản lý máu như damage, restore, và kiểm tra trạng thái.
+ *
+ * @see com.almasb.fxgl.dsl.components.HealthIntComponent
+ */
+public class HealthIntValue {
+ private int value;
+ private int maxValue;
+
+ /** Danh sách callbacks được gọi khi giá trị máu thay đổi. */
+ public final List onChangedListeners = new ArrayList<>();
+
+ /** Danh sách callbacks được gọi khi nhận sát thương. */
+ public final List onDamageListeners = new ArrayList<>();
+
+ /** Danh sách callbacks được gọi khi hồi máu. */
+ public final List onHealListeners = new ArrayList<>();
+
+ /** Danh sách callbacks được gọi khi máu về 0. */
+ public final List onZeroListeners = new ArrayList<>();
+
+ /** Danh sách callbacks được gọi khi máu lên full. */
+ public final List onFullListeners = new ArrayList<>();
+
+ /**
+ * Khởi tạo HealthIntValue với giá trị tối đa.
+ *
+ * @param maxValue Giá trị máu tối đa
+ */
+ public HealthIntValue(int maxValue) {
+ this.maxValue = maxValue;
+ this.value = maxValue;
+ }
+
+ public HealthIntValue(int maxValue, int value) {
+ this(maxValue);
+ this.value = value;
+ }
+
+ /**
+ * Lấy giá trị máu hiện tại.
+ *
+ * @return Giá trị máu hiện tại
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Set giá trị máu hiện tại. Giá trị sẽ được giới hạn trong khoảng [0, maxValue].
+ *
+ * @param value Giá trị máu mới
+ */
+ public void setValue(int value) {
+ var oldValue = this.value;
+ this.value = Math.max(0, Math.min(value, maxValue));
+
+ if (oldValue != this.value) {
+ triggerEvents(onChangedListeners);
+
+ if (isZero()) {
+ triggerEvents(onZeroListeners);
+ }
+ if (isFull()) {
+ triggerEvents(onFullListeners);
+ }
+ }
+ }
+
+ /**
+ * Lấy giá trị máu tối đa.
+ *
+ * @return Giá trị máu tối đa
+ */
+ public int getMaxValue() {
+ return maxValue;
+ }
+
+ /**
+ * Set giá trị máu tối đa. Nếu giá trị hiện tại vượt quá maxValue mới, nó sẽ được điều chỉnh.
+ *
+ * @param maxValue Giá trị máu tối đa mới
+ */
+ public void setMaxValue(int maxValue) {
+ this.maxValue = Math.max(0, maxValue);
+ if (value > maxValue) {
+ value = maxValue;
+ }
+ }
+
+ /**
+ * Gây sát thương (giảm máu).
+ *
+ * @param damage Lượng sát thương (giá trị dương)
+ */
+ public void damage(int damage) {
+ var oldValue = getValue();
+ setValue(getValue() - damage);
+ if (getValue() < oldValue) {
+ triggerEvents(onDamageListeners);
+ }
+ }
+
+ /**
+ * Hồi máu.
+ *
+ * @param amount Lượng máu hồi (giá trị dương)
+ */
+ public void restore(int amount) {
+ var oldValue = getValue();
+ setValue(getValue() + amount);
+ if (getValue() > oldValue) {
+ triggerEvents(onHealListeners);
+ }
+ }
+
+ /** Hồi máu về tối đa. */
+ public void restoreToMax() {
+ setValue(maxValue);
+ }
+
+ /**
+ * Kiểm tra máu có bằng 0 không.
+ *
+ * @return {@code true} nếu máu bằng 0, ngược lại {@code false}
+ */
+ public boolean isZero() {
+ return value == 0;
+ }
+
+ /**
+ * Kiểm tra máu có đầy không.
+ *
+ * @return {@code true} nếu máu bằng maxValue, ngược lại {@code false}
+ */
+ public boolean isFull() {
+ return value == maxValue;
+ }
+
+ /**
+ * Lấy phần trăm máu hiện tại (0-100).
+ *
+ * @return Phần trăm máu (0-100)
+ */
+ public double getValuePercent() {
+ if (maxValue == 0) {
+ return 0;
+ }
+ return (double) value / maxValue * 100.0;
+ }
+
+ /**
+ * Kích hoạt tất cả event callbacks trong danh sách.
+ *
+ * @param listeners Danh sách callbacks cần kích hoạt
+ */
+ private void triggerEvents(List listeners) {
+ for (var listener : listeners) {
+ if (listener != null) {
+ listener.run();
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "HealthIntValue{" + "value=" + value + ", maxValue=" + maxValue + '}';
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/ui/elements/HorizontalPositiveInteger.java b/src/main/java/com/github/codestorm/bounceverse/ui/elements/HorizontalPositiveInteger.java
new file mode 100644
index 0000000..848917e
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/ui/elements/HorizontalPositiveInteger.java
@@ -0,0 +1,125 @@
+package com.github.codestorm.bounceverse.ui.elements;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.texture.Texture;
+import com.github.codestorm.bounceverse.AssetsPath;
+
+import javafx.beans.property.ReadOnlyIntegerProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.scene.Node;
+import javafx.scene.layout.HBox;
+
+/**
+ *
+ *
+ * {@link HorizontalPositiveInteger}
+ *
+ * Hiển thị {@link Integer} dương theo chiều ngang, sử dụng các {@link Texture} chữ số.
+ */
+public class HorizontalPositiveInteger extends ViewElement {
+ private static final int DEFAULT_MIN_DIGITS = 0;
+ private static final double DEFAULT_SPACING = 2;
+ private static final double DEFAULT_DIGIT_WIDTH = 16;
+ private static final double DEFAULT_DIGIT_HEIGHT = 24;
+
+ private final ChangeListener listener = (obs, oldVal, newVal) -> update();
+ private final HBox container = new HBox(DEFAULT_SPACING);
+ private int minDigits = DEFAULT_MIN_DIGITS;
+ private ReadOnlyIntegerProperty value;
+ private String lastDisplayedValue = "";
+
+ public HorizontalPositiveInteger(ReadOnlyIntegerProperty value) {
+ setValue(value);
+ }
+
+ @Override
+ public Node getView() {
+ return container;
+ }
+
+ @Override
+ public void update() {
+ final var score = value.get();
+ final var positiveScore = Math.max(0, score);
+ final var actualDigits = String.valueOf(positiveScore).length();
+ final var digitsAmount = Math.max(minDigits, actualDigits);
+ final var scoreStr = String.format("%0" + digitsAmount + "d", positiveScore);
+
+ final var oldLength = lastDisplayedValue.length();
+ final var newLength = scoreStr.length();
+
+ if (newLength > oldLength) {
+ // Thêm
+ for (var i = 0; i < newLength - oldLength; i++) {
+ var digit = Character.getNumericValue(scoreStr.charAt(i));
+ var digitTexture = createDigitTexture(digit);
+ container.getChildren().addFirst(digitTexture);
+ }
+ } else if (newLength < oldLength) {
+ // Bớt
+ container.getChildren().remove(0, oldLength - newLength);
+ }
+
+ // Cập nhật những chữ số thay đổi
+ var startIndex = Math.max(0, newLength - oldLength);
+ for (var index = startIndex; index < newLength; index++) {
+ var oldIndex = index - (newLength - oldLength);
+ if (oldIndex < 0
+ || oldIndex >= oldLength
+ || scoreStr.charAt(index) != lastDisplayedValue.charAt(oldIndex)) {
+ updateDigitAt(index, Character.getNumericValue(scoreStr.charAt(index)));
+ }
+ }
+
+ lastDisplayedValue = scoreStr;
+ }
+
+ /**
+ * Update một chữ số tại vị trí cụ thể.
+ *
+ * @param index Vị trí chữ số
+ * @param digit Chữ số mới (0-9)
+ */
+ private void updateDigitAt(int index, int digit) {
+ if (index >= container.getChildren().size()) {
+ return;
+ }
+
+ var digitTexture = createDigitTexture(digit);
+ container.getChildren().set(index, digitTexture);
+ }
+
+ /**
+ * Tạo texture cho một chữ số.
+ *
+ * @param digit Chữ số (0-9)
+ * @return Texture của chữ số
+ */
+ private Texture createDigitTexture(int digit) {
+ var texturePath = AssetsPath.Textures.NUMBERS.get(digit);
+ return FXGL.getAssetLoader()
+ .loadTexture(texturePath, DEFAULT_DIGIT_WIDTH, DEFAULT_DIGIT_HEIGHT);
+ }
+
+ public int getMinDigits() {
+ return minDigits;
+ }
+
+ public void setMinDigits(int minDigits) {
+ this.minDigits = minDigits;
+ update();
+ }
+
+ public ReadOnlyIntegerProperty getValue() {
+ return value;
+ }
+
+ public void setValue(ReadOnlyIntegerProperty value) {
+ if (this.value != null) {
+ this.value.removeListener(listener);
+ }
+ this.value = value;
+ this.value.addListener(listener);
+ update();
+ }
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/ui/elements/ViewElement.java b/src/main/java/com/github/codestorm/bounceverse/ui/elements/ViewElement.java
new file mode 100644
index 0000000..2c82cba
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/ui/elements/ViewElement.java
@@ -0,0 +1,22 @@
+package com.github.codestorm.bounceverse.ui.elements;
+
+import com.almasb.fxgl.entity.Entity;
+import com.almasb.fxgl.entity.components.ViewComponent;
+
+import javafx.scene.Node;
+
+/**
+ *
+ *
+ * %{@link ViewElement}
+ *
+ * Phần tử UI chứa logic riêng, độc lập và có thể hiển thị trên màn hình.
+ * Đừng nhầm lẫn với {@link ViewComponent} - nó là một thành phần của {@link Entity}, không độc
+ * lập.
+ */
+public abstract class ViewElement {
+ public abstract Node getView();
+
+ /** Cập nhật Element. */
+ public void update() {}
+}
diff --git a/src/main/java/com/github/codestorm/bounceverse/ui/elements/ingame/Hearts.java b/src/main/java/com/github/codestorm/bounceverse/ui/elements/ingame/Hearts.java
new file mode 100644
index 0000000..ab8a0d4
--- /dev/null
+++ b/src/main/java/com/github/codestorm/bounceverse/ui/elements/ingame/Hearts.java
@@ -0,0 +1,62 @@
+package com.github.codestorm.bounceverse.ui.elements.ingame;
+
+import com.almasb.fxgl.dsl.FXGL;
+import com.almasb.fxgl.texture.Texture;
+import com.github.codestorm.bounceverse.AssetsPath;
+import com.github.codestorm.bounceverse.typing.structures.HealthIntValue;
+import com.github.codestorm.bounceverse.ui.elements.ViewElement;
+
+import javafx.scene.Node;
+import javafx.scene.layout.HBox;
+import javafx.scene.shape.Rectangle;
+
+/**
+ *
+ *
+ * {@link Hearts}
+ *
+ * Hiển thị số lượng trái tim (lives) của người chơi trên UI.
+ */
+public class Hearts extends ViewElement {
+ public static final double DEFAULT_SPACING = 5;
+ public static final Rectangle DEFAULT_SIZE = new Rectangle(32, 32);
+
+ private final Texture single;
+ private final HBox container = new HBox(DEFAULT_SPACING);
+
+ public Hearts() {
+ single =
+ FXGL.getAssetLoader()
+ .loadTexture(
+ AssetsPath.Textures.HEART,
+ DEFAULT_SIZE.getWidth(),
+ DEFAULT_SIZE.getHeight());
+
+ update();
+ final HealthIntValue livesProps = FXGL.getWorldProperties().getObject("lives");
+ livesProps.onChangedListeners.add(this::update);
+ }
+
+ @Override
+ public void update() {
+ final HealthIntValue livesProps = FXGL.getWorldProperties().getObject("lives");
+ final var lives = livesProps.getValue();
+
+ if (lives < 0) {
+ container.getChildren().clear();
+ return;
+ }
+ while (container.getChildren().size() > lives) {
+ container.getChildren().removeLast();
+ }
+ while (container.getChildren().size() < lives) {
+ var heart = single.copy();
+ container.getChildren().add(heart);
+ }
+ }
+
+ @Override
+ public Node getView() {
+ return container;
+ }
+}
diff --git a/src/main/java/libs/FastNoiseLite.java b/src/main/java/libs/FastNoiseLite.java
new file mode 100644
index 0000000..a958d81
--- /dev/null
+++ b/src/main/java/libs/FastNoiseLite.java
@@ -0,0 +1,2611 @@
+// MIT License
+//
+// Copyright(c) 2023 Jordan Peck (jordan.me2@gmail.com)
+// Copyright(c) 2023 Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files(the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions :
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// .'',;:cldxkO00KKXXNNWWWNNXKOkxdollcc::::::;:::ccllloooolllllllllooollc:,'... ...........',;cldxkO000Okxdlc::;;;,,;;;::cclllllll
+// ..',;:ldxO0KXXNNNNNNNNXXK0kxdolcc::::::;;;,,,,,,;;;;;;;;;;:::cclllllc:;'.... ...........',;:ldxO0KXXXK0Okxdolc::;;;;::cllodddddo
+// ...',:loxO0KXNNNNNXXKK0Okxdolc::;::::::::;;;,,'''''.....''',;:clllllc:;,'............''''''''',;:loxO0KXNNNNNXK0Okxdollccccllodxxxxxxd
+// ....';:ldkO0KXXXKK00Okxdolcc:;;;;;::cclllcc:;;,''..... ....',;clooddolcc:;;;;,,;;;;;::::;;;;;;:cloxk0KXNWWWWWWNXKK0Okxddoooddxxkkkkkxx
+// .....';:ldxkOOOOOkxxdolcc:;;;,,,;;:cllooooolcc:;'... ..,:codxkkkxddooollloooooooollcc:::::clodkO0KXNWWWWWWNNXK00Okxxxxxxxxkkkkxxx
+// . ....';:cloddddo___________,,,,;;:clooddddoolc:,... ..,:ldx__00OOOkkk___kkkkkkxxdollc::::cclodkO0KXXNNNNNNXXK0OOkxxxxxxxxxxxxddd
+// .......',;:cccc:| |,,,;;:cclooddddoll:;'.. ..';cox| \KKK000| |KK00OOkxdocc___;::clldxxkO0KKKKK00Okkxdddddddddddddddoo
+// .......'',,,,,''| ________|',,;;::cclloooooolc:;'......___:ldk| \KK000| |XKKK0Okxolc| |;;::cclodxxkkkkxxdoolllcclllooodddooooo
+// ''......''''....| | ....'',,,,;;;::cclloooollc:;,''.'| |oxk| \OOO0| |KKK00Oxdoll|___|;;;;;::ccllllllcc::;;,,;;;:cclloooooooo
+// ;;,''.......... | |_____',,;;;____:___cllo________.___| |___| \xkk| |KK_______ool___:::;________;;;_______...'',;;:ccclllloo
+// c:;,''......... | |:::/ ' |lo/ | | \dx| |0/ \d| |cc/ |'/ \......',,;;:ccllo
+// ol:;,'..........| _____|ll/ __ |o/ ______|____ ___| | \o| |/ ___ \| |o/ ______|/ ___ \ .......'',;:clo
+// dlc;,...........| |::clooo| / | |x\___ \KXKKK0| |dol| |\ \| | | | | |d\___ \..| | / / ....',:cl
+// xoc;'... .....'| |llodddd| \__| |_____\ \KKK0O| |lc:| |'\ | |___| | |_____\ \.| |_/___/... ...',;:c
+// dlc;'... ....',;| |oddddddo\ | |Okkx| |::;| |..\ |\ /| | | \ |... ....',;:c
+// ol:,'.......',:c|___|xxxddollc\_____,___|_________/ddoll|___|,,,|___|...\_____|:\ ______/l|___|_________/...\________|'........',;::cc
+// c:;'.......';:codxxkkkkxxolc::;::clodxkOO0OOkkxdollc::;;,,''''',,,,''''''''''',,'''''',;:loxkkOOkxol:;,'''',,;:ccllcc:;,'''''',;::ccll
+// ;,'.......',:codxkOO0OOkxdlc:;,,;;:cldxxkkxxdolc:;;,,''.....'',;;:::;;,,,'''''........,;cldkO0KK0Okdoc::;;::cloodddoolc:;;;;;::ccllooo
+// .........',;:lodxOO0000Okdoc:,,',,;:clloddoolc:;,''.......'',;:clooollc:;;,,''.......',:ldkOKXNNXX0Oxdolllloddxxxxxxdolccccccllooodddd
+// . .....';:cldxkO0000Okxol:;,''',,;::cccc:;,,'.......'',;:cldxxkkxxdolc:;;,'.......';coxOKXNWWWNXKOkxddddxxkkkkkkxdoollllooddxxxxkkk
+// ....',;:codxkO000OOxdoc:;,''',,,;;;;,''.......',,;:clodkO00000Okxolc::;,,''..',;:ldxOKXNWWWNNK0OkkkkkkkkkkkxxddooooodxxkOOOOO000
+// ....',;;clodxkkOOOkkdolc:;,,,,,,,,'..........,;:clodxkO0KKXKK0Okxdolcc::;;,,,;;:codkO0XXNNNNXKK0OOOOOkkkkxxdoollloodxkO0KKKXXXXX
+//
+// VERSION: 1.1.1
+// https://github.com/Auburn/FastNoiseLite
+
+// To switch between using floats or doubles for input position,
+// perform a file-wide replace on the following strings (including /*FNLfloat*/)
+// /*FNLfloat*/ float
+// /*FNLfloat*/ double
+
+package libs;
+
+public class FastNoiseLite
+{
+ public enum NoiseType
+ {
+ OpenSimplex2,
+ OpenSimplex2S,
+ Cellular,
+ Perlin,
+ ValueCubic,
+ Value
+ };
+
+ public enum RotationType3D
+ {
+ None,
+ ImproveXYPlanes,
+ ImproveXZPlanes
+ };
+
+ public enum FractalType
+ {
+ None,
+ FBm,
+ Ridged,
+ PingPong,
+ DomainWarpProgressive,
+ DomainWarpIndependent
+ };
+
+ public enum CellularDistanceFunction
+ {
+ Euclidean,
+ EuclideanSq,
+ Manhattan,
+ Hybrid
+ };
+
+ public enum CellularReturnType
+ {
+ CellValue,
+ Distance,
+ Distance2,
+ Distance2Add,
+ Distance2Sub,
+ Distance2Mul,
+ Distance2Div
+ };
+
+ public enum DomainWarpType
+ {
+ OpenSimplex2,
+ OpenSimplex2Reduced,
+ BasicGrid
+ };
+
+ private enum TransformType3D
+ {
+ None,
+ ImproveXYPlanes,
+ ImproveXZPlanes,
+ DefaultOpenSimplex2
+ };
+
+ private int mSeed = 1337;
+ private float mFrequency = 0.01f;
+ private NoiseType mNoiseType = NoiseType.OpenSimplex2;
+ private RotationType3D mRotationType3D = RotationType3D.None;
+ private TransformType3D mTransformType3D = TransformType3D.DefaultOpenSimplex2;
+
+ private FractalType mFractalType = FractalType.None;
+ private int mOctaves = 3;
+ private float mLacunarity = 2.0f;
+ private float mGain = 0.5f;
+ private float mWeightedStrength = 0.0f;
+ private float mPingPongStrength = 2.0f;
+
+ private float mFractalBounding = 1 / 1.75f;
+
+ private CellularDistanceFunction mCellularDistanceFunction = CellularDistanceFunction.EuclideanSq;
+ private CellularReturnType mCellularReturnType = CellularReturnType.Distance;
+ private float mCellularJitterModifier = 1.0f;
+
+ private DomainWarpType mDomainWarpType = DomainWarpType.OpenSimplex2;
+ private TransformType3D mWarpTransformType3D = TransformType3D.DefaultOpenSimplex2;
+ private float mDomainWarpAmp = 1.0f;
+
+ ///
+ /// Create new FastNoise object with default seed
+ ///
+ public FastNoiseLite() { }
+
+ ///
+ /// Create new FastNoise object with specified seed
+ ///
+ public FastNoiseLite(int seed)
+ {
+ SetSeed(seed);
+ }
+
+ ///
+ /// Sets seed used for all noise types
+ ///
+ ///
+ /// Default: 1337
+ ///
+ public void SetSeed(int seed) { mSeed = seed; }
+
+ ///
+ /// Sets frequency for all noise types
+ ///
+ ///
+ /// Default: 0.01
+ ///
+ public void SetFrequency(float frequency) { mFrequency = frequency; }
+
+ ///
+ /// Sets noise algorithm used for GetNoise(...)
+ ///
+ ///
+ /// Default: OpenSimplex2
+ ///
+ public void SetNoiseType(NoiseType noiseType)
+ {
+ mNoiseType = noiseType;
+ UpdateTransformType3D();
+ }
+
+ ///
+ /// Sets domain rotation type for 3D Noise and 3D DomainWarp.
+ /// Can aid in reducing directional artifacts when sampling a 2D plane in 3D
+ ///
+ ///
+ /// Default: None
+ ///
+ public void SetRotationType3D(RotationType3D rotationType3D)
+ {
+ mRotationType3D = rotationType3D;
+ UpdateTransformType3D();
+ UpdateWarpTransformType3D();
+ }
+
+ ///
+ /// Sets method for combining octaves in all fractal noise types
+ ///
+ ///
+ /// Default: None
+ /// Note: FractalType.DomainWarp... only affects DomainWarp(...)
+ ///
+ public void SetFractalType(FractalType fractalType) { mFractalType = fractalType; }
+
+ ///
+ /// Sets octave count for all fractal noise types
+ ///
+ ///
+ /// Default: 3
+ ///
+ public void SetFractalOctaves(int octaves)
+ {
+ mOctaves = octaves;
+ CalculateFractalBounding();
+ }
+
+ ///
+ /// Sets octave lacunarity for all fractal noise types
+ ///
+ ///
+ /// Default: 2.0
+ ///
+ public void SetFractalLacunarity(float lacunarity) { mLacunarity = lacunarity; }
+
+ ///
+ /// Sets octave gain for all fractal noise types
+ ///
+ ///
+ /// Default: 0.5
+ ///
+ public void SetFractalGain(float gain)
+ {
+ mGain = gain;
+ CalculateFractalBounding();
+ }
+
+ ///
+ /// Sets octave weighting for all none DomainWarp fratal types
+ ///
+ ///
+ /// Default: 0.0
+ /// Note: Keep between 0...1 to maintain -1...1 output bounding
+ ///
+ public void SetFractalWeightedStrength(float weightedStrength) { mWeightedStrength = weightedStrength; }
+
+ ///
+ /// Sets strength of the fractal ping pong effect
+ ///
+ ///
+ /// Default: 2.0
+ ///
+ public void SetFractalPingPongStrength(float pingPongStrength) { mPingPongStrength = pingPongStrength; }
+
+
+ ///
+ /// Sets distance function used in cellular noise calculations
+ ///
+ ///
+ /// Default: Distance
+ ///
+ public void SetCellularDistanceFunction(CellularDistanceFunction cellularDistanceFunction) { mCellularDistanceFunction = cellularDistanceFunction; }
+
+ ///
+ /// Sets return type from cellular noise calculations
+ ///
+ ///
+ /// Default: EuclideanSq
+ ///
+ public void SetCellularReturnType(CellularReturnType cellularReturnType) { mCellularReturnType = cellularReturnType; }
+
+ ///
+ /// Sets the maximum distance a cellular point can move from it's grid position
+ ///
+ ///
+ /// Default: 1.0
+ /// Note: Setting this higher than 1 will cause artifacts
+ ///
+ public void SetCellularJitter(float cellularJitter) { mCellularJitterModifier = cellularJitter; }
+
+
+ ///
+ /// Sets the warp algorithm when using DomainWarp(...)
+ ///
+ ///
+ /// Default: OpenSimplex2
+ ///
+ public void SetDomainWarpType(DomainWarpType domainWarpType)
+ {
+ mDomainWarpType = domainWarpType;
+ UpdateWarpTransformType3D();
+ }
+
+
+ ///
+ /// Sets the maximum warp distance from original position when using DomainWarp(...)
+ ///
+ ///
+ /// Default: 1.0
+ ///
+ public void SetDomainWarpAmp(float domainWarpAmp) { mDomainWarpAmp = domainWarpAmp; }
+
+
+ ///
+ /// 2D noise at given position using current settings
+ ///
+ ///
+ /// Noise output bounded between -1...1
+ ///
+ public float GetNoise(/*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ x *= mFrequency;
+ y *= mFrequency;
+
+ switch (mNoiseType)
+ {
+ case OpenSimplex2:
+ case OpenSimplex2S:
+ {
+ final /*FNLfloat*/ double SQRT3 = (/*FNLfloat*/ double)1.7320508075688772935274463415059;
+ final /*FNLfloat*/ double F2 = 0.5f * (SQRT3 - 1);
+ /*FNLfloat*/ double t = (x + y) * F2;
+ x += t;
+ y += t;
+ }
+ break;
+ default:
+ break;
+ }
+
+ switch (mFractalType)
+ {
+ default:
+ return GenNoiseSingle(mSeed, x, y);
+ case FBm:
+ return GenFractalFBm(x, y);
+ case Ridged:
+ return GenFractalRidged(x, y);
+ case PingPong:
+ return GenFractalPingPong(x, y);
+ }
+ }
+
+ ///
+ /// 3D noise at given position using current settings
+ ///
+ ///
+ /// Noise output bounded between -1...1
+ ///
+ public float GetNoise(/*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ x *= mFrequency;
+ y *= mFrequency;
+ z *= mFrequency;
+
+ switch (mTransformType3D)
+ {
+ case ImproveXYPlanes:
+ {
+ /*FNLfloat*/ double xy = x + y;
+ /*FNLfloat*/ double s2 = xy * -(/*FNLfloat*/ double)0.211324865405187;
+ z *= (/*FNLfloat*/ double)0.577350269189626;
+ x += s2 - z;
+ y = y + s2 - z;
+ z += xy * (/*FNLfloat*/ double)0.577350269189626;
+ }
+ break;
+ case ImproveXZPlanes:
+ {
+ /*FNLfloat*/ double xz = x + z;
+ /*FNLfloat*/ double s2 = xz * -(/*FNLfloat*/ double)0.211324865405187;
+ y *= (/*FNLfloat*/ double)0.577350269189626;
+ x += s2 - y;
+ z += s2 - y;
+ y += xz * (/*FNLfloat*/ double)0.577350269189626;
+ }
+ break;
+ case DefaultOpenSimplex2:
+ {
+ final /*FNLfloat*/ double R3 = (/*FNLfloat*/ double)(2.0 / 3.0);
+ /*FNLfloat*/ double r = (x + y + z) * R3; // Rotation, not skew
+ x = r - x;
+ y = r - y;
+ z = r - z;
+ }
+ break;
+ default:
+ break;
+ }
+
+ switch (mFractalType)
+ {
+ default:
+ return GenNoiseSingle(mSeed, x, y, z);
+ case FBm:
+ return GenFractalFBm(x, y, z);
+ case Ridged:
+ return GenFractalRidged(x, y, z);
+ case PingPong:
+ return GenFractalPingPong(x, y, z);
+ }
+ }
+
+
+ ///
+ /// 2D warps the input position using current domain warp settings
+ ///
+ ///
+ /// Example usage with GetNoise
+ /// DomainWarp(coord)
+ /// noise = GetNoise(x, y)
+ ///
+ public void DomainWarp(Vector2 coord)
+ {
+ switch (mFractalType)
+ {
+ default:
+ DomainWarpSingle(coord);
+ break;
+ case DomainWarpProgressive:
+ DomainWarpFractalProgressive(coord);
+ break;
+ case DomainWarpIndependent:
+ DomainWarpFractalIndependent(coord);
+ break;
+ }
+ }
+
+ ///
+ /// 3D warps the input position using current domain warp settings
+ ///
+ ///
+ /// Example usage with GetNoise
+ /// DomainWarp(coord)
+ /// noise = GetNoise(x, y, z)
+ ///
+ public void DomainWarp(Vector3 coord)
+ {
+ switch (mFractalType)
+ {
+ default:
+ DomainWarpSingle(coord);
+ break;
+ case DomainWarpProgressive:
+ DomainWarpFractalProgressive(coord);
+ break;
+ case DomainWarpIndependent:
+ DomainWarpFractalIndependent(coord);
+ break;
+ }
+ }
+
+
+ private static final float[] Gradients2D = {
+ 0.130526192220052f, 0.99144486137381f, 0.38268343236509f, 0.923879532511287f, 0.608761429008721f, 0.793353340291235f, 0.793353340291235f, 0.608761429008721f,
+ 0.923879532511287f, 0.38268343236509f, 0.99144486137381f, 0.130526192220051f, 0.99144486137381f, -0.130526192220051f, 0.923879532511287f, -0.38268343236509f,
+ 0.793353340291235f, -0.60876142900872f, 0.608761429008721f, -0.793353340291235f, 0.38268343236509f, -0.923879532511287f, 0.130526192220052f, -0.99144486137381f,
+ -0.130526192220052f, -0.99144486137381f, -0.38268343236509f, -0.923879532511287f, -0.608761429008721f, -0.793353340291235f, -0.793353340291235f, -0.608761429008721f,
+ -0.923879532511287f, -0.38268343236509f, -0.99144486137381f, -0.130526192220052f, -0.99144486137381f, 0.130526192220051f, -0.923879532511287f, 0.38268343236509f,
+ -0.793353340291235f, 0.608761429008721f, -0.608761429008721f, 0.793353340291235f, -0.38268343236509f, 0.923879532511287f, -0.130526192220052f, 0.99144486137381f,
+ 0.130526192220052f, 0.99144486137381f, 0.38268343236509f, 0.923879532511287f, 0.608761429008721f, 0.793353340291235f, 0.793353340291235f, 0.608761429008721f,
+ 0.923879532511287f, 0.38268343236509f, 0.99144486137381f, 0.130526192220051f, 0.99144486137381f, -0.130526192220051f, 0.923879532511287f, -0.38268343236509f,
+ 0.793353340291235f, -0.60876142900872f, 0.608761429008721f, -0.793353340291235f, 0.38268343236509f, -0.923879532511287f, 0.130526192220052f, -0.99144486137381f,
+ -0.130526192220052f, -0.99144486137381f, -0.38268343236509f, -0.923879532511287f, -0.608761429008721f, -0.793353340291235f, -0.793353340291235f, -0.608761429008721f,
+ -0.923879532511287f, -0.38268343236509f, -0.99144486137381f, -0.130526192220052f, -0.99144486137381f, 0.130526192220051f, -0.923879532511287f, 0.38268343236509f,
+ -0.793353340291235f, 0.608761429008721f, -0.608761429008721f, 0.793353340291235f, -0.38268343236509f, 0.923879532511287f, -0.130526192220052f, 0.99144486137381f,
+ 0.130526192220052f, 0.99144486137381f, 0.38268343236509f, 0.923879532511287f, 0.608761429008721f, 0.793353340291235f, 0.793353340291235f, 0.608761429008721f,
+ 0.923879532511287f, 0.38268343236509f, 0.99144486137381f, 0.130526192220051f, 0.99144486137381f, -0.130526192220051f, 0.923879532511287f, -0.38268343236509f,
+ 0.793353340291235f, -0.60876142900872f, 0.608761429008721f, -0.793353340291235f, 0.38268343236509f, -0.923879532511287f, 0.130526192220052f, -0.99144486137381f,
+ -0.130526192220052f, -0.99144486137381f, -0.38268343236509f, -0.923879532511287f, -0.608761429008721f, -0.793353340291235f, -0.793353340291235f, -0.608761429008721f,
+ -0.923879532511287f, -0.38268343236509f, -0.99144486137381f, -0.130526192220052f, -0.99144486137381f, 0.130526192220051f, -0.923879532511287f, 0.38268343236509f,
+ -0.793353340291235f, 0.608761429008721f, -0.608761429008721f, 0.793353340291235f, -0.38268343236509f, 0.923879532511287f, -0.130526192220052f, 0.99144486137381f,
+ 0.130526192220052f, 0.99144486137381f, 0.38268343236509f, 0.923879532511287f, 0.608761429008721f, 0.793353340291235f, 0.793353340291235f, 0.608761429008721f,
+ 0.923879532511287f, 0.38268343236509f, 0.99144486137381f, 0.130526192220051f, 0.99144486137381f, -0.130526192220051f, 0.923879532511287f, -0.38268343236509f,
+ 0.793353340291235f, -0.60876142900872f, 0.608761429008721f, -0.793353340291235f, 0.38268343236509f, -0.923879532511287f, 0.130526192220052f, -0.99144486137381f,
+ -0.130526192220052f, -0.99144486137381f, -0.38268343236509f, -0.923879532511287f, -0.608761429008721f, -0.793353340291235f, -0.793353340291235f, -0.608761429008721f,
+ -0.923879532511287f, -0.38268343236509f, -0.99144486137381f, -0.130526192220052f, -0.99144486137381f, 0.130526192220051f, -0.923879532511287f, 0.38268343236509f,
+ -0.793353340291235f, 0.608761429008721f, -0.608761429008721f, 0.793353340291235f, -0.38268343236509f, 0.923879532511287f, -0.130526192220052f, 0.99144486137381f,
+ 0.130526192220052f, 0.99144486137381f, 0.38268343236509f, 0.923879532511287f, 0.608761429008721f, 0.793353340291235f, 0.793353340291235f, 0.608761429008721f,
+ 0.923879532511287f, 0.38268343236509f, 0.99144486137381f, 0.130526192220051f, 0.99144486137381f, -0.130526192220051f, 0.923879532511287f, -0.38268343236509f,
+ 0.793353340291235f, -0.60876142900872f, 0.608761429008721f, -0.793353340291235f, 0.38268343236509f, -0.923879532511287f, 0.130526192220052f, -0.99144486137381f,
+ -0.130526192220052f, -0.99144486137381f, -0.38268343236509f, -0.923879532511287f, -0.608761429008721f, -0.793353340291235f, -0.793353340291235f, -0.608761429008721f,
+ -0.923879532511287f, -0.38268343236509f, -0.99144486137381f, -0.130526192220052f, -0.99144486137381f, 0.130526192220051f, -0.923879532511287f, 0.38268343236509f,
+ -0.793353340291235f, 0.608761429008721f, -0.608761429008721f, 0.793353340291235f, -0.38268343236509f, 0.923879532511287f, -0.130526192220052f, 0.99144486137381f,
+ 0.38268343236509f, 0.923879532511287f, 0.923879532511287f, 0.38268343236509f, 0.923879532511287f, -0.38268343236509f, 0.38268343236509f, -0.923879532511287f,
+ -0.38268343236509f, -0.923879532511287f, -0.923879532511287f, -0.38268343236509f, -0.923879532511287f, 0.38268343236509f, -0.38268343236509f, 0.923879532511287f,
+ };
+
+ private static final float[] RandVecs2D = {
+ -0.2700222198f, -0.9628540911f, 0.3863092627f, -0.9223693152f, 0.04444859006f, -0.999011673f, -0.5992523158f, -0.8005602176f, -0.7819280288f, 0.6233687174f, 0.9464672271f, 0.3227999196f, -0.6514146797f, -0.7587218957f, 0.9378472289f, 0.347048376f,
+ -0.8497875957f, -0.5271252623f, -0.879042592f, 0.4767432447f, -0.892300288f, -0.4514423508f, -0.379844434f, -0.9250503802f, -0.9951650832f, 0.0982163789f, 0.7724397808f, -0.6350880136f, 0.7573283322f, -0.6530343002f, -0.9928004525f, -0.119780055f,
+ -0.0532665713f, 0.9985803285f, 0.9754253726f, -0.2203300762f, -0.7665018163f, 0.6422421394f, 0.991636706f, 0.1290606184f, -0.994696838f, 0.1028503788f, -0.5379205513f, -0.84299554f, 0.5022815471f, -0.8647041387f, 0.4559821461f, -0.8899889226f,
+ -0.8659131224f, -0.5001944266f, 0.0879458407f, -0.9961252577f, -0.5051684983f, 0.8630207346f, 0.7753185226f, -0.6315704146f, -0.6921944612f, 0.7217110418f, -0.5191659449f, -0.8546734591f, 0.8978622882f, -0.4402764035f, -0.1706774107f, 0.9853269617f,
+ -0.9353430106f, -0.3537420705f, -0.9992404798f, 0.03896746794f, -0.2882064021f, -0.9575683108f, -0.9663811329f, 0.2571137995f, -0.8759714238f, -0.4823630009f, -0.8303123018f, -0.5572983775f, 0.05110133755f, -0.9986934731f, -0.8558373281f, -0.5172450752f,
+ 0.09887025282f, 0.9951003332f, 0.9189016087f, 0.3944867976f, -0.2439375892f, -0.9697909324f, -0.8121409387f, -0.5834613061f, -0.9910431363f, 0.1335421355f, 0.8492423985f, -0.5280031709f, -0.9717838994f, -0.2358729591f, 0.9949457207f, 0.1004142068f,
+ 0.6241065508f, -0.7813392434f, 0.662910307f, 0.7486988212f, -0.7197418176f, 0.6942418282f, -0.8143370775f, -0.5803922158f, 0.104521054f, -0.9945226741f, -0.1065926113f, -0.9943027784f, 0.445799684f, -0.8951327509f, 0.105547406f, 0.9944142724f,
+ -0.992790267f, 0.1198644477f, -0.8334366408f, 0.552615025f, 0.9115561563f, -0.4111755999f, 0.8285544909f, -0.5599084351f, 0.7217097654f, -0.6921957921f, 0.4940492677f, -0.8694339084f, -0.3652321272f, -0.9309164803f, -0.9696606758f, 0.2444548501f,
+ 0.08925509731f, -0.996008799f, 0.5354071276f, -0.8445941083f, -0.1053576186f, 0.9944343981f, -0.9890284586f, 0.1477251101f, 0.004856104961f, 0.9999882091f, 0.9885598478f, 0.1508291331f, 0.9286129562f, -0.3710498316f, -0.5832393863f, -0.8123003252f,
+ 0.3015207509f, 0.9534596146f, -0.9575110528f, 0.2883965738f, 0.9715802154f, -0.2367105511f, 0.229981792f, 0.9731949318f, 0.955763816f, -0.2941352207f, 0.740956116f, 0.6715534485f, -0.9971513787f, -0.07542630764f, 0.6905710663f, -0.7232645452f,
+ -0.290713703f, -0.9568100872f, 0.5912777791f, -0.8064679708f, -0.9454592212f, -0.325740481f, 0.6664455681f, 0.74555369f, 0.6236134912f, 0.7817328275f, 0.9126993851f, -0.4086316587f, -0.8191762011f, 0.5735419353f, -0.8812745759f, -0.4726046147f,
+ 0.9953313627f, 0.09651672651f, 0.9855650846f, -0.1692969699f, -0.8495980887f, 0.5274306472f, 0.6174853946f, -0.7865823463f, 0.8508156371f, 0.52546432f, 0.9985032451f, -0.05469249926f, 0.1971371563f, -0.9803759185f, 0.6607855748f, -0.7505747292f,
+ -0.03097494063f, 0.9995201614f, -0.6731660801f, 0.739491331f, -0.7195018362f, -0.6944905383f, 0.9727511689f, 0.2318515979f, 0.9997059088f, -0.0242506907f, 0.4421787429f, -0.8969269532f, 0.9981350961f, -0.061043673f, -0.9173660799f, -0.3980445648f,
+ -0.8150056635f, -0.5794529907f, -0.8789331304f, 0.4769450202f, 0.0158605829f, 0.999874213f, -0.8095464474f, 0.5870558317f, -0.9165898907f, -0.3998286786f, -0.8023542565f, 0.5968480938f, -0.5176737917f, 0.8555780767f, -0.8154407307f, -0.5788405779f,
+ 0.4022010347f, -0.9155513791f, -0.9052556868f, -0.4248672045f, 0.7317445619f, 0.6815789728f, -0.5647632201f, -0.8252529947f, -0.8403276335f, -0.5420788397f, -0.9314281527f, 0.363925262f, 0.5238198472f, 0.8518290719f, 0.7432803869f, -0.6689800195f,
+ -0.985371561f, -0.1704197369f, 0.4601468731f, 0.88784281f, 0.825855404f, 0.5638819483f, 0.6182366099f, 0.7859920446f, 0.8331502863f, -0.553046653f, 0.1500307506f, 0.9886813308f, -0.662330369f, -0.7492119075f, -0.668598664f, 0.743623444f,
+ 0.7025606278f, 0.7116238924f, -0.5419389763f, -0.8404178401f, -0.3388616456f, 0.9408362159f, 0.8331530315f, 0.5530425174f, -0.2989720662f, -0.9542618632f, 0.2638522993f, 0.9645630949f, 0.124108739f, -0.9922686234f, -0.7282649308f, -0.6852956957f,
+ 0.6962500149f, 0.7177993569f, -0.9183535368f, 0.3957610156f, -0.6326102274f, -0.7744703352f, -0.9331891859f, -0.359385508f, -0.1153779357f, -0.9933216659f, 0.9514974788f, -0.3076565421f, -0.08987977445f, -0.9959526224f, 0.6678496916f, 0.7442961705f,
+ 0.7952400393f, -0.6062947138f, -0.6462007402f, -0.7631674805f, -0.2733598753f, 0.9619118351f, 0.9669590226f, -0.254931851f, -0.9792894595f, 0.2024651934f, -0.5369502995f, -0.8436138784f, -0.270036471f, -0.9628500944f, -0.6400277131f, 0.7683518247f,
+ -0.7854537493f, -0.6189203566f, 0.06005905383f, -0.9981948257f, -0.02455770378f, 0.9996984141f, -0.65983623f, 0.751409442f, -0.6253894466f, -0.7803127835f, -0.6210408851f, -0.7837781695f, 0.8348888491f, 0.5504185768f, -0.1592275245f, 0.9872419133f,
+ 0.8367622488f, 0.5475663786f, -0.8675753916f, -0.4973056806f, -0.2022662628f, -0.9793305667f, 0.9399189937f, 0.3413975472f, 0.9877404807f, -0.1561049093f, -0.9034455656f, 0.4287028224f, 0.1269804218f, -0.9919052235f, -0.3819600854f, 0.924178821f,
+ 0.9754625894f, 0.2201652486f, -0.3204015856f, -0.9472818081f, -0.9874760884f, 0.1577687387f, 0.02535348474f, -0.9996785487f, 0.4835130794f, -0.8753371362f, -0.2850799925f, -0.9585037287f, -0.06805516006f, -0.99768156f, -0.7885244045f, -0.6150034663f,
+ 0.3185392127f, -0.9479096845f, 0.8880043089f, 0.4598351306f, 0.6476921488f, -0.7619021462f, 0.9820241299f, 0.1887554194f, 0.9357275128f, -0.3527237187f, -0.8894895414f, 0.4569555293f, 0.7922791302f, 0.6101588153f, 0.7483818261f, 0.6632681526f,
+ -0.7288929755f, -0.6846276581f, 0.8729032783f, -0.4878932944f, 0.8288345784f, 0.5594937369f, 0.08074567077f, 0.9967347374f, 0.9799148216f, -0.1994165048f, -0.580730673f, -0.8140957471f, -0.4700049791f, -0.8826637636f, 0.2409492979f, 0.9705377045f,
+ 0.9437816757f, -0.3305694308f, -0.8927998638f, -0.4504535528f, -0.8069622304f, 0.5906030467f, 0.06258973166f, 0.9980393407f, -0.9312597469f, 0.3643559849f, 0.5777449785f, 0.8162173362f, -0.3360095855f, -0.941858566f, 0.697932075f, -0.7161639607f,
+ -0.002008157227f, -0.9999979837f, -0.1827294312f, -0.9831632392f, -0.6523911722f, 0.7578824173f, -0.4302626911f, -0.9027037258f, -0.9985126289f, -0.05452091251f, -0.01028102172f, -0.9999471489f, -0.4946071129f, 0.8691166802f, -0.2999350194f, 0.9539596344f,
+ 0.8165471961f, 0.5772786819f, 0.2697460475f, 0.962931498f, -0.7306287391f, -0.6827749597f, -0.7590952064f, -0.6509796216f, -0.907053853f, 0.4210146171f, -0.5104861064f, -0.8598860013f, 0.8613350597f, 0.5080373165f, 0.5007881595f, -0.8655698812f,
+ -0.654158152f, 0.7563577938f, -0.8382755311f, -0.545246856f, 0.6940070834f, 0.7199681717f, 0.06950936031f, 0.9975812994f, 0.1702942185f, -0.9853932612f, 0.2695973274f, 0.9629731466f, 0.5519612192f, -0.8338697815f, 0.225657487f, -0.9742067022f,
+ 0.4215262855f, -0.9068161835f, 0.4881873305f, -0.8727388672f, -0.3683854996f, -0.9296731273f, -0.9825390578f, 0.1860564427f, 0.81256471f, 0.5828709909f, 0.3196460933f, -0.9475370046f, 0.9570913859f, 0.2897862643f, -0.6876655497f, -0.7260276109f,
+ -0.9988770922f, -0.047376731f, -0.1250179027f, 0.992154486f, -0.8280133617f, 0.560708367f, 0.9324863769f, -0.3612051451f, 0.6394653183f, 0.7688199442f, -0.01623847064f, -0.9998681473f, -0.9955014666f, -0.09474613458f, -0.81453315f, 0.580117012f,
+ 0.4037327978f, -0.9148769469f, 0.9944263371f, 0.1054336766f, -0.1624711654f, 0.9867132919f, -0.9949487814f, -0.100383875f, -0.6995302564f, 0.7146029809f, 0.5263414922f, -0.85027327f, -0.5395221479f, 0.841971408f, 0.6579370318f, 0.7530729462f,
+ 0.01426758847f, -0.9998982128f, -0.6734383991f, 0.7392433447f, 0.639412098f, -0.7688642071f, 0.9211571421f, 0.3891908523f, -0.146637214f, -0.9891903394f, -0.782318098f, 0.6228791163f, -0.5039610839f, -0.8637263605f, -0.7743120191f, -0.6328039957f,
+ };
+
+ private static final float[] Gradients3D = {
+ 0, 1, 1, 0, 0,-1, 1, 0, 0, 1,-1, 0, 0,-1,-1, 0,
+ 1, 0, 1, 0, -1, 0, 1, 0, 1, 0,-1, 0, -1, 0,-1, 0,
+ 1, 1, 0, 0, -1, 1, 0, 0, 1,-1, 0, 0, -1,-1, 0, 0,
+ 0, 1, 1, 0, 0,-1, 1, 0, 0, 1,-1, 0, 0,-1,-1, 0,
+ 1, 0, 1, 0, -1, 0, 1, 0, 1, 0,-1, 0, -1, 0,-1, 0,
+ 1, 1, 0, 0, -1, 1, 0, 0, 1,-1, 0, 0, -1,-1, 0, 0,
+ 0, 1, 1, 0, 0,-1, 1, 0, 0, 1,-1, 0, 0,-1,-1, 0,
+ 1, 0, 1, 0, -1, 0, 1, 0, 1, 0,-1, 0, -1, 0,-1, 0,
+ 1, 1, 0, 0, -1, 1, 0, 0, 1,-1, 0, 0, -1,-1, 0, 0,
+ 0, 1, 1, 0, 0,-1, 1, 0, 0, 1,-1, 0, 0,-1,-1, 0,
+ 1, 0, 1, 0, -1, 0, 1, 0, 1, 0,-1, 0, -1, 0,-1, 0,
+ 1, 1, 0, 0, -1, 1, 0, 0, 1,-1, 0, 0, -1,-1, 0, 0,
+ 0, 1, 1, 0, 0,-1, 1, 0, 0, 1,-1, 0, 0,-1,-1, 0,
+ 1, 0, 1, 0, -1, 0, 1, 0, 1, 0,-1, 0, -1, 0,-1, 0,
+ 1, 1, 0, 0, -1, 1, 0, 0, 1,-1, 0, 0, -1,-1, 0, 0,
+ 1, 1, 0, 0, 0,-1, 1, 0, -1, 1, 0, 0, 0,-1,-1, 0
+ };
+
+ private static final float[] RandVecs3D = {
+ -0.7292736885f, -0.6618439697f, 0.1735581948f, 0, 0.790292081f, -0.5480887466f, -0.2739291014f, 0, 0.7217578935f, 0.6226212466f, -0.3023380997f, 0, 0.565683137f, -0.8208298145f, -0.0790000257f, 0, 0.760049034f, -0.5555979497f, -0.3370999617f, 0, 0.3713945616f, 0.5011264475f, 0.7816254623f, 0, -0.1277062463f, -0.4254438999f, -0.8959289049f, 0, -0.2881560924f, -0.5815838982f, 0.7607405838f, 0,
+ 0.5849561111f, -0.662820239f, -0.4674352136f, 0, 0.3307171178f, 0.0391653737f, 0.94291689f, 0, 0.8712121778f, -0.4113374369f, -0.2679381538f, 0, 0.580981015f, 0.7021915846f, 0.4115677815f, 0, 0.503756873f, 0.6330056931f, -0.5878203852f, 0, 0.4493712205f, 0.601390195f, 0.6606022552f, 0, -0.6878403724f, 0.09018890807f, -0.7202371714f, 0, -0.5958956522f, -0.6469350577f, 0.475797649f, 0,
+ -0.5127052122f, 0.1946921978f, -0.8361987284f, 0, -0.9911507142f, -0.05410276466f, -0.1212153153f, 0, -0.2149721042f, 0.9720882117f, -0.09397607749f, 0, -0.7518650936f, -0.5428057603f, 0.3742469607f, 0, 0.5237068895f, 0.8516377189f, -0.02107817834f, 0, 0.6333504779f, 0.1926167129f, -0.7495104896f, 0, -0.06788241606f, 0.3998305789f, 0.9140719259f, 0, -0.5538628599f, -0.4729896695f, -0.6852128902f, 0,
+ -0.7261455366f, -0.5911990757f, 0.3509933228f, 0, -0.9229274737f, -0.1782808786f, 0.3412049336f, 0, -0.6968815002f, 0.6511274338f, 0.3006480328f, 0, 0.9608044783f, -0.2098363234f, -0.1811724921f, 0, 0.06817146062f, -0.9743405129f, 0.2145069156f, 0, -0.3577285196f, -0.6697087264f, -0.6507845481f, 0, -0.1868621131f, 0.7648617052f, -0.6164974636f, 0, -0.6541697588f, 0.3967914832f, 0.6439087246f, 0,
+ 0.6993340405f, -0.6164538506f, 0.3618239211f, 0, -0.1546665739f, 0.6291283928f, 0.7617583057f, 0, -0.6841612949f, -0.2580482182f, -0.6821542638f, 0, 0.5383980957f, 0.4258654885f, 0.7271630328f, 0, -0.5026987823f, -0.7939832935f, -0.3418836993f, 0, 0.3202971715f, 0.2834415347f, 0.9039195862f, 0, 0.8683227101f, -0.0003762656404f, -0.4959995258f, 0, 0.791120031f, -0.08511045745f, 0.6057105799f, 0,
+ -0.04011016052f, -0.4397248749f, 0.8972364289f, 0, 0.9145119872f, 0.3579346169f, -0.1885487608f, 0, -0.9612039066f, -0.2756484276f, 0.01024666929f, 0, 0.6510361721f, -0.2877799159f, -0.7023778346f, 0, -0.2041786351f, 0.7365237271f, 0.644859585f, 0, -0.7718263711f, 0.3790626912f, 0.5104855816f, 0, -0.3060082741f, -0.7692987727f, 0.5608371729f, 0, 0.454007341f, -0.5024843065f, 0.7357899537f, 0,
+ 0.4816795475f, 0.6021208291f, -0.6367380315f, 0, 0.6961980369f, -0.3222197429f, 0.641469197f, 0, -0.6532160499f, -0.6781148932f, 0.3368515753f, 0, 0.5089301236f, -0.6154662304f, -0.6018234363f, 0, -0.1635919754f, -0.9133604627f, -0.372840892f, 0, 0.52408019f, -0.8437664109f, 0.1157505864f, 0, 0.5902587356f, 0.4983817807f, -0.6349883666f, 0, 0.5863227872f, 0.494764745f, 0.6414307729f, 0,
+ 0.6779335087f, 0.2341345225f, 0.6968408593f, 0, 0.7177054546f, -0.6858979348f, 0.120178631f, 0, -0.5328819713f, -0.5205125012f, 0.6671608058f, 0, -0.8654874251f, -0.0700727088f, -0.4960053754f, 0, -0.2861810166f, 0.7952089234f, 0.5345495242f, 0, -0.04849529634f, 0.9810836427f, -0.1874115585f, 0, -0.6358521667f, 0.6058348682f, 0.4781800233f, 0, 0.6254794696f, -0.2861619734f, 0.7258696564f, 0,
+ -0.2585259868f, 0.5061949264f, -0.8227581726f, 0, 0.02136306781f, 0.5064016808f, -0.8620330371f, 0, 0.200111773f, 0.8599263484f, 0.4695550591f, 0, 0.4743561372f, 0.6014985084f, -0.6427953014f, 0, 0.6622993731f, -0.5202474575f, -0.5391679918f, 0, 0.08084972818f, -0.6532720452f, 0.7527940996f, 0, -0.6893687501f, 0.0592860349f, 0.7219805347f, 0, -0.1121887082f, -0.9673185067f, 0.2273952515f, 0,
+ 0.7344116094f, 0.5979668656f, -0.3210532909f, 0, 0.5789393465f, -0.2488849713f, 0.7764570201f, 0, 0.6988182827f, 0.3557169806f, -0.6205791146f, 0, -0.8636845529f, -0.2748771249f, -0.4224826141f, 0, -0.4247027957f, -0.4640880967f, 0.777335046f, 0, 0.5257722489f, -0.8427017621f, 0.1158329937f, 0, 0.9343830603f, 0.316302472f, -0.1639543925f, 0, -0.1016836419f, -0.8057303073f, -0.5834887393f, 0,
+ -0.6529238969f, 0.50602126f, -0.5635892736f, 0, -0.2465286165f, -0.9668205684f, -0.06694497494f, 0, -0.9776897119f, -0.2099250524f, -0.007368825344f, 0, 0.7736893337f, 0.5734244712f, 0.2694238123f, 0, -0.6095087895f, 0.4995678998f, 0.6155736747f, 0, 0.5794535482f, 0.7434546771f, 0.3339292269f, 0, -0.8226211154f, 0.08142581855f, 0.5627293636f, 0, -0.510385483f, 0.4703667658f, 0.7199039967f, 0,
+ -0.5764971849f, -0.07231656274f, -0.8138926898f, 0, 0.7250628871f, 0.3949971505f, -0.5641463116f, 0, -0.1525424005f, 0.4860840828f, -0.8604958341f, 0, -0.5550976208f, -0.4957820792f, 0.667882296f, 0, -0.1883614327f, 0.9145869398f, 0.357841725f, 0, 0.7625556724f, -0.5414408243f, -0.3540489801f, 0, -0.5870231946f, -0.3226498013f, -0.7424963803f, 0, 0.3051124198f, 0.2262544068f, -0.9250488391f, 0,
+ 0.6379576059f, 0.577242424f, -0.5097070502f, 0, -0.5966775796f, 0.1454852398f, -0.7891830656f, 0, -0.658330573f, 0.6555487542f, -0.3699414651f, 0, 0.7434892426f, 0.2351084581f, 0.6260573129f, 0, 0.5562114096f, 0.8264360377f, -0.0873632843f, 0, -0.3028940016f, -0.8251527185f, 0.4768419182f, 0, 0.1129343818f, -0.985888439f, -0.1235710781f, 0, 0.5937652891f, -0.5896813806f, 0.5474656618f, 0,
+ 0.6757964092f, -0.5835758614f, -0.4502648413f, 0, 0.7242302609f, -0.1152719764f, 0.6798550586f, 0, -0.9511914166f, 0.0753623979f, -0.2992580792f, 0, 0.2539470961f, -0.1886339355f, 0.9486454084f, 0, 0.571433621f, -0.1679450851f, -0.8032795685f, 0, -0.06778234979f, 0.3978269256f, 0.9149531629f, 0, 0.6074972649f, 0.733060024f, -0.3058922593f, 0, -0.5435478392f, 0.1675822484f, 0.8224791405f, 0,
+ -0.5876678086f, -0.3380045064f, -0.7351186982f, 0, -0.7967562402f, 0.04097822706f, -0.6029098428f, 0, -0.1996350917f, 0.8706294745f, 0.4496111079f, 0, -0.02787660336f, -0.9106232682f, -0.4122962022f, 0, -0.7797625996f, -0.6257634692f, 0.01975775581f, 0, -0.5211232846f, 0.7401644346f, -0.4249554471f, 0, 0.8575424857f, 0.4053272873f, -0.3167501783f, 0, 0.1045223322f, 0.8390195772f, -0.5339674439f, 0,
+ 0.3501822831f, 0.9242524096f, -0.1520850155f, 0, 0.1987849858f, 0.07647613266f, 0.9770547224f, 0, 0.7845996363f, 0.6066256811f, -0.1280964233f, 0, 0.09006737436f, -0.9750989929f, -0.2026569073f, 0, -0.8274343547f, -0.542299559f, 0.1458203587f, 0, -0.3485797732f, -0.415802277f, 0.840000362f, 0, -0.2471778936f, -0.7304819962f, -0.6366310879f, 0, -0.3700154943f, 0.8577948156f, 0.3567584454f, 0,
+ 0.5913394901f, -0.548311967f, -0.5913303597f, 0, 0.1204873514f, -0.7626472379f, -0.6354935001f, 0, 0.616959265f, 0.03079647928f, 0.7863922953f, 0, 0.1258156836f, -0.6640829889f, -0.7369967419f, 0, -0.6477565124f, -0.1740147258f, -0.7417077429f, 0, 0.6217889313f, -0.7804430448f, -0.06547655076f, 0, 0.6589943422f, -0.6096987708f, 0.4404473475f, 0, -0.2689837504f, -0.6732403169f, -0.6887635427f, 0,
+ -0.3849775103f, 0.5676542638f, 0.7277093879f, 0, 0.5754444408f, 0.8110471154f, -0.1051963504f, 0, 0.9141593684f, 0.3832947817f, 0.131900567f, 0, -0.107925319f, 0.9245493968f, 0.3654593525f, 0, 0.377977089f, 0.3043148782f, 0.8743716458f, 0, -0.2142885215f, -0.8259286236f, 0.5214617324f, 0, 0.5802544474f, 0.4148098596f, -0.7008834116f, 0, -0.1982660881f, 0.8567161266f, -0.4761596756f, 0,
+ -0.03381553704f, 0.3773180787f, -0.9254661404f, 0, -0.6867922841f, -0.6656597827f, 0.2919133642f, 0, 0.7731742607f, -0.2875793547f, -0.5652430251f, 0, -0.09655941928f, 0.9193708367f, -0.3813575004f, 0, 0.2715702457f, -0.9577909544f, -0.09426605581f, 0, 0.2451015704f, -0.6917998565f, -0.6792188003f, 0, 0.977700782f, -0.1753855374f, 0.1155036542f, 0, -0.5224739938f, 0.8521606816f, 0.02903615945f, 0,
+ -0.7734880599f, -0.5261292347f, 0.3534179531f, 0, -0.7134492443f, -0.269547243f, 0.6467878011f, 0, 0.1644037271f, 0.5105846203f, -0.8439637196f, 0, 0.6494635788f, 0.05585611296f, 0.7583384168f, 0, -0.4711970882f, 0.5017280509f, -0.7254255765f, 0, -0.6335764307f, -0.2381686273f, -0.7361091029f, 0, -0.9021533097f, -0.270947803f, -0.3357181763f, 0, -0.3793711033f, 0.872258117f, 0.3086152025f, 0,
+ -0.6855598966f, -0.3250143309f, 0.6514394162f, 0, 0.2900942212f, -0.7799057743f, -0.5546100667f, 0, -0.2098319339f, 0.85037073f, 0.4825351604f, 0, -0.4592603758f, 0.6598504336f, -0.5947077538f, 0, 0.8715945488f, 0.09616365406f, -0.4807031248f, 0, -0.6776666319f, 0.7118504878f, -0.1844907016f, 0, 0.7044377633f, 0.312427597f, 0.637304036f, 0, -0.7052318886f, -0.2401093292f, -0.6670798253f, 0,
+ 0.081921007f, -0.7207336136f, -0.6883545647f, 0, -0.6993680906f, -0.5875763221f, -0.4069869034f, 0, -0.1281454481f, 0.6419895885f, 0.7559286424f, 0, -0.6337388239f, -0.6785471501f, -0.3714146849f, 0, 0.5565051903f, -0.2168887573f, -0.8020356851f, 0, -0.5791554484f, 0.7244372011f, -0.3738578718f, 0, 0.1175779076f, -0.7096451073f, 0.6946792478f, 0, -0.6134619607f, 0.1323631078f, 0.7785527795f, 0,
+ 0.6984635305f, -0.02980516237f, -0.715024719f, 0, 0.8318082963f, -0.3930171956f, 0.3919597455f, 0, 0.1469576422f, 0.05541651717f, -0.9875892167f, 0, 0.708868575f, -0.2690503865f, 0.6520101478f, 0, 0.2726053183f, 0.67369766f, -0.68688995f, 0, -0.6591295371f, 0.3035458599f, -0.6880466294f, 0, 0.4815131379f, -0.7528270071f, 0.4487723203f, 0, 0.9430009463f, 0.1675647412f, -0.2875261255f, 0,
+ 0.434802957f, 0.7695304522f, -0.4677277752f, 0, 0.3931996188f, 0.594473625f, 0.7014236729f, 0, 0.7254336655f, -0.603925654f, 0.3301814672f, 0, 0.7590235227f, -0.6506083235f, 0.02433313207f, 0, -0.8552768592f, -0.3430042733f, 0.3883935666f, 0, -0.6139746835f, 0.6981725247f, 0.3682257648f, 0, -0.7465905486f, -0.5752009504f, 0.3342849376f, 0, 0.5730065677f, 0.810555537f, -0.1210916791f, 0,
+ -0.9225877367f, -0.3475211012f, -0.167514036f, 0, -0.7105816789f, -0.4719692027f, -0.5218416899f, 0, -0.08564609717f, 0.3583001386f, 0.929669703f, 0, -0.8279697606f, -0.2043157126f, 0.5222271202f, 0, 0.427944023f, 0.278165994f, 0.8599346446f, 0, 0.5399079671f, -0.7857120652f, -0.3019204161f, 0, 0.5678404253f, -0.5495413974f, -0.6128307303f, 0, -0.9896071041f, 0.1365639107f, -0.04503418428f, 0,
+ -0.6154342638f, -0.6440875597f, 0.4543037336f, 0, 0.1074204368f, -0.7946340692f, 0.5975094525f, 0, -0.3595449969f, -0.8885529948f, 0.28495784f, 0, -0.2180405296f, 0.1529888965f, 0.9638738118f, 0, -0.7277432317f, -0.6164050508f, -0.3007234646f, 0, 0.7249729114f, -0.00669719484f, 0.6887448187f, 0, -0.5553659455f, -0.5336586252f, 0.6377908264f, 0, 0.5137558015f, 0.7976208196f, -0.3160000073f, 0,
+ -0.3794024848f, 0.9245608561f, -0.03522751494f, 0, 0.8229248658f, 0.2745365933f, -0.4974176556f, 0, -0.5404114394f, 0.6091141441f, 0.5804613989f, 0, 0.8036581901f, -0.2703029469f, 0.5301601931f, 0, 0.6044318879f, 0.6832968393f, 0.4095943388f, 0, 0.06389988817f, 0.9658208605f, -0.2512108074f, 0, 0.1087113286f, 0.7402471173f, -0.6634877936f, 0, -0.713427712f, -0.6926784018f, 0.1059128479f, 0,
+ 0.6458897819f, -0.5724548511f, -0.5050958653f, 0, -0.6553931414f, 0.7381471625f, 0.159995615f, 0, 0.3910961323f, 0.9188871375f, -0.05186755998f, 0, -0.4879022471f, -0.5904376907f, 0.6429111375f, 0, 0.6014790094f, 0.7707441366f, -0.2101820095f, 0, -0.5677173047f, 0.7511360995f, 0.3368851762f, 0, 0.7858573506f, 0.226674665f, 0.5753666838f, 0, -0.4520345543f, -0.604222686f, -0.6561857263f, 0,
+ 0.002272116345f, 0.4132844051f, -0.9105991643f, 0, -0.5815751419f, -0.5162925989f, 0.6286591339f, 0, -0.03703704785f, 0.8273785755f, 0.5604221175f, 0, -0.5119692504f, 0.7953543429f, -0.3244980058f, 0, -0.2682417366f, -0.9572290247f, -0.1084387619f, 0, -0.2322482736f, -0.9679131102f, -0.09594243324f, 0, 0.3554328906f, -0.8881505545f, 0.2913006227f, 0, 0.7346520519f, -0.4371373164f, 0.5188422971f, 0,
+ 0.9985120116f, 0.04659011161f, -0.02833944577f, 0, -0.3727687496f, -0.9082481361f, 0.1900757285f, 0, 0.91737377f, -0.3483642108f, 0.1925298489f, 0, 0.2714911074f, 0.4147529736f, -0.8684886582f, 0, 0.5131763485f, -0.7116334161f, 0.4798207128f, 0, -0.8737353606f, 0.18886992f, -0.4482350644f, 0, 0.8460043821f, -0.3725217914f, 0.3814499973f, 0, 0.8978727456f, -0.1780209141f, -0.4026575304f, 0,
+ 0.2178065647f, -0.9698322841f, -0.1094789531f, 0, -0.1518031304f, -0.7788918132f, -0.6085091231f, 0, -0.2600384876f, -0.4755398075f, -0.8403819825f, 0, 0.572313509f, -0.7474340931f, -0.3373418503f, 0, -0.7174141009f, 0.1699017182f, -0.6756111411f, 0, -0.684180784f, 0.02145707593f, -0.7289967412f, 0, -0.2007447902f, 0.06555605789f, -0.9774476623f, 0, -0.1148803697f, -0.8044887315f, 0.5827524187f, 0,
+ -0.7870349638f, 0.03447489231f, 0.6159443543f, 0, -0.2015596421f, 0.6859872284f, 0.6991389226f, 0, -0.08581082512f, -0.10920836f, -0.9903080513f, 0, 0.5532693395f, 0.7325250401f, -0.396610771f, 0, -0.1842489331f, -0.9777375055f, -0.1004076743f, 0, 0.0775473789f, -0.9111505856f, 0.4047110257f, 0, 0.1399838409f, 0.7601631212f, -0.6344734459f, 0, 0.4484419361f, -0.845289248f, 0.2904925424f, 0
+ };
+
+
+ private static float FastMin(float a, float b) { return a < b ? a : b; }
+
+ private static float FastMax(float a, float b) { return a > b ? a : b; }
+
+ private static float FastAbs(float f) { return f < 0 ? -f : f; }
+
+ private static float FastSqrt(float f) { return (float)Math.sqrt(f); }
+
+ private static int FastFloor(/*FNLfloat*/ double f) { return f >= 0 ? (int)f : (int)f - 1; }
+
+ private static int FastRound(/*FNLfloat*/ double f) { return f >= 0 ? (int)(f + 0.5f) : (int)(f - 0.5f); }
+
+ private static float Lerp(float a, float b, float t) { return a + t * (b - a); }
+
+ private static float InterpHermite(float t) { return t * t * (3 - 2 * t); }
+
+ private static float InterpQuintic(float t) { return t * t * t * (t * (t * 6 - 15) + 10); }
+
+ private static float CubicLerp(float a, float b, float c, float d, float t)
+ {
+ float p = (d - c) - (a - b);
+ return t * t * t * p + t * t * ((a - b) - p) + t * (c - a) + b;
+ }
+
+ private static float PingPong(float t)
+ {
+ t -= (int)(t * 0.5f) * 2;
+ return t < 1 ? t : 2 - t;
+ }
+
+ private void CalculateFractalBounding()
+ {
+ float gain = FastAbs(mGain);
+ float amp = gain;
+ float ampFractal = 1.0f;
+ for (int i = 1; i < mOctaves; i++)
+ {
+ ampFractal += amp;
+ amp *= gain;
+ }
+ mFractalBounding = 1 / ampFractal;
+ }
+
+ // Hashing
+ private static final int PrimeX = 501125321;
+ private static final int PrimeY = 1136930381;
+ private static final int PrimeZ = 1720413743;
+
+ private static int Hash(int seed, int xPrimed, int yPrimed)
+ {
+ int hash = seed ^ xPrimed ^ yPrimed;
+
+ hash *= 0x27d4eb2d;
+ return hash;
+ }
+
+ private static int Hash(int seed, int xPrimed, int yPrimed, int zPrimed)
+ {
+ int hash = seed ^ xPrimed ^ yPrimed ^ zPrimed;
+
+ hash *= 0x27d4eb2d;
+ return hash;
+ }
+
+ private static float ValCoord(int seed, int xPrimed, int yPrimed)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed);
+
+ hash *= hash;
+ hash ^= hash << 19;
+ return hash * (1 / 2147483648.0f);
+ }
+
+ private static float ValCoord(int seed, int xPrimed, int yPrimed, int zPrimed)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed, zPrimed);
+
+ hash *= hash;
+ hash ^= hash << 19;
+ return hash * (1 / 2147483648.0f);
+ }
+
+ private static float GradCoord(int seed, int xPrimed, int yPrimed, float xd, float yd)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed);
+ hash ^= hash >> 15;
+ hash &= 127 << 1;
+
+ float xg = Gradients2D[hash];
+ float yg = Gradients2D[hash | 1];
+
+ return xd * xg + yd * yg;
+ }
+
+ private static float GradCoord(int seed, int xPrimed, int yPrimed, int zPrimed, float xd, float yd, float zd)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed, zPrimed);
+ hash ^= hash >> 15;
+ hash &= 63 << 2;
+
+ float xg = Gradients3D[hash];
+ float yg = Gradients3D[hash | 1];
+ float zg = Gradients3D[hash | 2];
+
+ return xd * xg + yd * yg + zd * zg;
+ }
+
+
+ // Generic noise gen
+
+ private float GenNoiseSingle(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ switch (mNoiseType)
+ {
+ case OpenSimplex2:
+ return SingleSimplex(seed, x, y);
+ case OpenSimplex2S:
+ return SingleOpenSimplex2S(seed, x, y);
+ case Cellular:
+ return SingleCellular(seed, x, y);
+ case Perlin:
+ return SinglePerlin(seed, x, y);
+ case ValueCubic:
+ return SingleValueCubic(seed, x, y);
+ case Value:
+ return SingleValue(seed, x, y);
+ default:
+ return 0;
+ }
+ }
+
+ private float GenNoiseSingle(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ switch (mNoiseType)
+ {
+ case OpenSimplex2:
+ return SingleOpenSimplex2(seed, x, y, z);
+ case OpenSimplex2S:
+ return SingleOpenSimplex2S(seed, x, y, z);
+ case Cellular:
+ return SingleCellular(seed, x, y, z);
+ case Perlin:
+ return SinglePerlin(seed, x, y, z);
+ case ValueCubic:
+ return SingleValueCubic(seed, x, y, z);
+ case Value:
+ return SingleValue(seed, x, y, z);
+ default:
+ return 0;
+ }
+ }
+
+
+ // Noise Coordinate Transforms (frequency, and possible skew or rotation)
+
+ private void UpdateTransformType3D()
+ {
+ switch (mRotationType3D)
+ {
+ case ImproveXYPlanes:
+ mTransformType3D = TransformType3D.ImproveXYPlanes;
+ break;
+ case ImproveXZPlanes:
+ mTransformType3D = TransformType3D.ImproveXZPlanes;
+ break;
+ default:
+ switch (mNoiseType)
+ {
+ case OpenSimplex2:
+ case OpenSimplex2S:
+ mTransformType3D = TransformType3D.DefaultOpenSimplex2;
+ break;
+ default:
+ mTransformType3D = TransformType3D.None;
+ break;
+ }
+ break;
+ }
+ }
+
+ private void UpdateWarpTransformType3D()
+ {
+ switch (mRotationType3D)
+ {
+ case ImproveXYPlanes:
+ mWarpTransformType3D = TransformType3D.ImproveXYPlanes;
+ break;
+ case ImproveXZPlanes:
+ mWarpTransformType3D = TransformType3D.ImproveXZPlanes;
+ break;
+ default:
+ switch (mDomainWarpType)
+ {
+ case OpenSimplex2:
+ case OpenSimplex2Reduced:
+ mWarpTransformType3D = TransformType3D.DefaultOpenSimplex2;
+ break;
+ default:
+ mWarpTransformType3D = TransformType3D.None;
+ break;
+ }
+ break;
+ }
+ }
+
+
+ // Fractal FBm
+
+ private float GenFractalFBm(/*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ int seed = mSeed;
+ float sum = 0;
+ float amp = mFractalBounding;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ float noise = GenNoiseSingle(seed++, x, y);
+ sum += noise * amp;
+ amp *= Lerp(1.0f, FastMin(noise + 1, 2) * 0.5f, mWeightedStrength);
+
+ x *= mLacunarity;
+ y *= mLacunarity;
+ amp *= mGain;
+ }
+
+ return sum;
+ }
+
+ private float GenFractalFBm(/*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ int seed = mSeed;
+ float sum = 0;
+ float amp = mFractalBounding;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ float noise = GenNoiseSingle(seed++, x, y, z);
+ sum += noise * amp;
+ amp *= Lerp(1.0f, (noise + 1) * 0.5f, mWeightedStrength);
+
+ x *= mLacunarity;
+ y *= mLacunarity;
+ z *= mLacunarity;
+ amp *= mGain;
+ }
+
+ return sum;
+ }
+
+
+ // Fractal Ridged
+
+ private float GenFractalRidged(/*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ int seed = mSeed;
+ float sum = 0;
+ float amp = mFractalBounding;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ float noise = FastAbs(GenNoiseSingle(seed++, x, y));
+ sum += (noise * -2 + 1) * amp;
+ amp *= Lerp(1.0f, 1 - noise, mWeightedStrength);
+
+ x *= mLacunarity;
+ y *= mLacunarity;
+ amp *= mGain;
+ }
+
+ return sum;
+ }
+
+ private float GenFractalRidged(/*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ int seed = mSeed;
+ float sum = 0;
+ float amp = mFractalBounding;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ float noise = FastAbs(GenNoiseSingle(seed++, x, y, z));
+ sum += (noise * -2 + 1) * amp;
+ amp *= Lerp(1.0f, 1 - noise, mWeightedStrength);
+
+ x *= mLacunarity;
+ y *= mLacunarity;
+ z *= mLacunarity;
+ amp *= mGain;
+ }
+
+ return sum;
+ }
+
+
+ // Fractal PingPong
+
+ private float GenFractalPingPong(/*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ int seed = mSeed;
+ float sum = 0;
+ float amp = mFractalBounding;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ float noise = PingPong((GenNoiseSingle(seed++, x, y) + 1) * mPingPongStrength);
+ sum += (noise - 0.5f) * 2 * amp;
+ amp *= Lerp(1.0f, noise, mWeightedStrength);
+
+ x *= mLacunarity;
+ y *= mLacunarity;
+ amp *= mGain;
+ }
+
+ return sum;
+ }
+
+ private float GenFractalPingPong(/*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ int seed = mSeed;
+ float sum = 0;
+ float amp = mFractalBounding;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ float noise = PingPong((GenNoiseSingle(seed++, x, y, z) + 1) * mPingPongStrength);
+ sum += (noise - 0.5f) * 2 * amp;
+ amp *= Lerp(1.0f, noise, mWeightedStrength);
+
+ x *= mLacunarity;
+ y *= mLacunarity;
+ z *= mLacunarity;
+ amp *= mGain;
+ }
+
+ return sum;
+ }
+
+
+ // Simplex/OpenSimplex2 Noise
+
+ private float SingleSimplex(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ // 2D OpenSimplex2 case uses the same algorithm as ordinary Simplex.
+
+ final float SQRT3 = 1.7320508075688772935274463415059f;
+ final float G2 = (3 - SQRT3) / 6;
+
+ /*
+ * --- Skew moved to switch statements before fractal evaluation ---
+ * final FNLfloat F2 = 0.5f * (SQRT3 - 1);
+ * FNLfloat s = (x + y) * F2;
+ * x += s; y += s;
+ */
+
+ int i = FastFloor(x);
+ int j = FastFloor(y);
+ float xi = (float)(x - i);
+ float yi = (float)(y - j);
+
+ float t = (xi + yi) * G2;
+ float x0 = (float)(xi - t);
+ float y0 = (float)(yi - t);
+
+ i *= PrimeX;
+ j *= PrimeY;
+
+ float n0, n1, n2;
+
+ float a = 0.5f - x0 * x0 - y0 * y0;
+ if (a <= 0) n0 = 0;
+ else
+ {
+ n0 = (a * a) * (a * a) * GradCoord(seed, i, j, x0, y0);
+ }
+
+ float c = (float)(2 * (1 - 2 * G2) * (1 / G2 - 2)) * t + ((float)(-2 * (1 - 2 * G2) * (1 - 2 * G2)) + a);
+ if (c <= 0) n2 = 0;
+ else
+ {
+ float x2 = x0 + (2 * (float)G2 - 1);
+ float y2 = y0 + (2 * (float)G2 - 1);
+ n2 = (c * c) * (c * c) * GradCoord(seed, i + PrimeX, j + PrimeY, x2, y2);
+ }
+
+ if (y0 > x0)
+ {
+ float x1 = x0 + (float)G2;
+ float y1 = y0 + ((float)G2 - 1);
+ float b = 0.5f - x1 * x1 - y1 * y1;
+ if (b <= 0) n1 = 0;
+ else
+ {
+ n1 = (b * b) * (b * b) * GradCoord(seed, i, j + PrimeY, x1, y1);
+ }
+ }
+ else
+ {
+ float x1 = x0 + ((float)G2 - 1);
+ float y1 = y0 + (float)G2;
+ float b = 0.5f - x1 * x1 - y1 * y1;
+ if (b <= 0) n1 = 0;
+ else
+ {
+ n1 = (b * b) * (b * b) * GradCoord(seed, i + PrimeX, j, x1, y1);
+ }
+ }
+
+ return (n0 + n1 + n2) * 99.83685446303647f;
+ }
+
+ private float SingleOpenSimplex2(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ // 3D OpenSimplex2 case uses two offset rotated cube grids.
+
+ /*
+ * --- Rotation moved to switch statements before fractal evaluation ---
+ * final FNLfloat R3 = (FNLfloat)(2.0 / 3.0);
+ * FNLfloat r = (x + y + z) * R3; // Rotation, not skew
+ * x = r - x; y = r - y; z = r - z;
+ */
+
+ int i = FastRound(x);
+ int j = FastRound(y);
+ int k = FastRound(z);
+ float x0 = (float)(x - i);
+ float y0 = (float)(y - j);
+ float z0 = (float)(z - k);
+
+ int xNSign = (int)(-1.0f - x0) | 1;
+ int yNSign = (int)(-1.0f - y0) | 1;
+ int zNSign = (int)(-1.0f - z0) | 1;
+
+ float ax0 = xNSign * -x0;
+ float ay0 = yNSign * -y0;
+ float az0 = zNSign * -z0;
+
+ i *= PrimeX;
+ j *= PrimeY;
+ k *= PrimeZ;
+
+ float value = 0;
+ float a = (0.6f - x0 * x0) - (y0 * y0 + z0 * z0);
+
+ for (int l = 0; ; l++)
+ {
+ if (a > 0)
+ {
+ value += (a * a) * (a * a) * GradCoord(seed, i, j, k, x0, y0, z0);
+ }
+
+ if (ax0 >= ay0 && ax0 >= az0)
+ {
+ float b = a + ax0 + ax0;
+ if (b > 1)
+ {
+ b -= 1;
+ value += (b * b) * (b * b) * GradCoord(seed, i - xNSign * PrimeX, j, k, x0 + xNSign, y0, z0);
+ }
+ }
+ else if (ay0 > ax0 && ay0 >= az0)
+ {
+ float b = a + ay0 + ay0;
+ if (b > 1)
+ {
+ b -= 1;
+ value += (b * b) * (b * b) * GradCoord(seed, i, j - yNSign * PrimeY, k, x0, y0 + yNSign, z0);
+ }
+ }
+ else
+ {
+ float b = a + az0 + az0;
+ if (b > 1)
+ {
+ b -= 1;
+ value += (b * b) * (b * b) * GradCoord(seed, i, j, k - zNSign * PrimeZ, x0, y0, z0 + zNSign);
+ }
+ }
+
+ if (l == 1) break;
+
+ ax0 = 0.5f - ax0;
+ ay0 = 0.5f - ay0;
+ az0 = 0.5f - az0;
+
+ x0 = xNSign * ax0;
+ y0 = yNSign * ay0;
+ z0 = zNSign * az0;
+
+ a += (0.75f - ax0) - (ay0 + az0);
+
+ i += (xNSign >> 1) & PrimeX;
+ j += (yNSign >> 1) & PrimeY;
+ k += (zNSign >> 1) & PrimeZ;
+
+ xNSign = -xNSign;
+ yNSign = -yNSign;
+ zNSign = -zNSign;
+
+ seed = ~seed;
+ }
+
+ return value * 32.69428253173828125f;
+ }
+
+
+ // OpenSimplex2S Noise
+
+ private float SingleOpenSimplex2S(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ // 2D OpenSimplex2S case is a modified 2D simplex noise.
+
+ final /*FNLfloat*/ double SQRT3 = (/*FNLfloat*/ double)1.7320508075688772935274463415059;
+ final /*FNLfloat*/ double G2 = (3 - SQRT3) / 6;
+
+ /*
+ * --- Skew moved to TransformNoiseCoordinate method ---
+ * final FNLfloat F2 = 0.5f * (SQRT3 - 1);
+ * FNLfloat s = (x + y) * F2;
+ * x += s; y += s;
+ */
+
+ int i = FastFloor(x);
+ int j = FastFloor(y);
+ float xi = (float)(x - i);
+ float yi = (float)(y - j);
+
+ i *= PrimeX;
+ j *= PrimeY;
+ int i1 = i + PrimeX;
+ int j1 = j + PrimeY;
+
+ float t = (xi + yi) * (float)G2;
+ float x0 = xi - t;
+ float y0 = yi - t;
+
+ float a0 = (2.0f / 3.0f) - x0 * x0 - y0 * y0;
+ float value = (a0 * a0) * (a0 * a0) * GradCoord(seed, i, j, x0, y0);
+
+ float a1 = (float)(2 * (1 - 2 * G2) * (1 / G2 - 2)) * t + ((float)(-2 * (1 - 2 * G2) * (1 - 2 * G2)) + a0);
+ float x1 = x0 - (float)(1 - 2 * G2);
+ float y1 = y0 - (float)(1 - 2 * G2);
+ value += (a1 * a1) * (a1 * a1) * GradCoord(seed, i1, j1, x1, y1);
+
+ // Nested conditionals were faster than compact bit logic/arithmetic.
+ float xmyi = xi - yi;
+ if (t > G2)
+ {
+ if (xi + xmyi > 1)
+ {
+ float x2 = x0 + (float)(3 * G2 - 2);
+ float y2 = y0 + (float)(3 * G2 - 1);
+ float a2 = (2.0f / 3.0f) - x2 * x2 - y2 * y2;
+ if (a2 > 0)
+ {
+ value += (a2 * a2) * (a2 * a2) * GradCoord(seed, i + (PrimeX << 1), j + PrimeY, x2, y2);
+ }
+ }
+ else
+ {
+ float x2 = x0 + (float)G2;
+ float y2 = y0 + (float)(G2 - 1);
+ float a2 = (2.0f / 3.0f) - x2 * x2 - y2 * y2;
+ if (a2 > 0)
+ {
+ value += (a2 * a2) * (a2 * a2) * GradCoord(seed, i, j + PrimeY, x2, y2);
+ }
+ }
+
+ if (yi - xmyi > 1)
+ {
+ float x3 = x0 + (float)(3 * G2 - 1);
+ float y3 = y0 + (float)(3 * G2 - 2);
+ float a3 = (2.0f / 3.0f) - x3 * x3 - y3 * y3;
+ if (a3 > 0)
+ {
+ value += (a3 * a3) * (a3 * a3) * GradCoord(seed, i + PrimeX, j + (PrimeY << 1), x3, y3);
+ }
+ }
+ else
+ {
+ float x3 = x0 + (float)(G2 - 1);
+ float y3 = y0 + (float)G2;
+ float a3 = (2.0f / 3.0f) - x3 * x3 - y3 * y3;
+ if (a3 > 0)
+ {
+ value += (a3 * a3) * (a3 * a3) * GradCoord(seed, i + PrimeX, j, x3, y3);
+ }
+ }
+ }
+ else
+ {
+ if (xi + xmyi < 0)
+ {
+ float x2 = x0 + (float)(1 - G2);
+ float y2 = y0 - (float)G2;
+ float a2 = (2.0f / 3.0f) - x2 * x2 - y2 * y2;
+ if (a2 > 0)
+ {
+ value += (a2 * a2) * (a2 * a2) * GradCoord(seed, i - PrimeX, j, x2, y2);
+ }
+ }
+ else
+ {
+ float x2 = x0 + (float)(G2 - 1);
+ float y2 = y0 + (float)G2;
+ float a2 = (2.0f / 3.0f) - x2 * x2 - y2 * y2;
+ if (a2 > 0)
+ {
+ value += (a2 * a2) * (a2 * a2) * GradCoord(seed, i + PrimeX, j, x2, y2);
+ }
+ }
+
+ if (yi < xmyi)
+ {
+ float x2 = x0 - (float)G2;
+ float y2 = y0 - (float)(G2 - 1);
+ float a2 = (2.0f / 3.0f) - x2 * x2 - y2 * y2;
+ if (a2 > 0)
+ {
+ value += (a2 * a2) * (a2 * a2) * GradCoord(seed, i, j - PrimeY, x2, y2);
+ }
+ }
+ else
+ {
+ float x2 = x0 + (float)G2;
+ float y2 = y0 + (float)(G2 - 1);
+ float a2 = (2.0f / 3.0f) - x2 * x2 - y2 * y2;
+ if (a2 > 0)
+ {
+ value += (a2 * a2) * (a2 * a2) * GradCoord(seed, i, j + PrimeY, x2, y2);
+ }
+ }
+ }
+
+ return value * 18.24196194486065f;
+ }
+
+ private float SingleOpenSimplex2S(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ // 3D OpenSimplex2S case uses two offset rotated cube grids.
+
+ /*
+ * --- Rotation moved to TransformNoiseCoordinate method ---
+ * final FNLfloat R3 = (FNLfloat)(2.0 / 3.0);
+ * FNLfloat r = (x + y + z) * R3; // Rotation, not skew
+ * x = r - x; y = r - y; z = r - z;
+ */
+
+ int i = FastFloor(x);
+ int j = FastFloor(y);
+ int k = FastFloor(z);
+ float xi = (float)(x - i);
+ float yi = (float)(y - j);
+ float zi = (float)(z - k);
+
+ i *= PrimeX;
+ j *= PrimeY;
+ k *= PrimeZ;
+ int seed2 = seed + 1293373;
+
+ int xNMask = (int)(-0.5f - xi);
+ int yNMask = (int)(-0.5f - yi);
+ int zNMask = (int)(-0.5f - zi);
+
+ float x0 = xi + xNMask;
+ float y0 = yi + yNMask;
+ float z0 = zi + zNMask;
+ float a0 = 0.75f - x0 * x0 - y0 * y0 - z0 * z0;
+ float value = (a0 * a0) * (a0 * a0) * GradCoord(seed,
+ i + (xNMask & PrimeX), j + (yNMask & PrimeY), k + (zNMask & PrimeZ), x0, y0, z0);
+
+ float x1 = xi - 0.5f;
+ float y1 = yi - 0.5f;
+ float z1 = zi - 0.5f;
+ float a1 = 0.75f - x1 * x1 - y1 * y1 - z1 * z1;
+ value += (a1 * a1) * (a1 * a1) * GradCoord(seed2,
+ i + PrimeX, j + PrimeY, k + PrimeZ, x1, y1, z1);
+
+ float xAFlipMask0 = ((xNMask | 1) << 1) * x1;
+ float yAFlipMask0 = ((yNMask | 1) << 1) * y1;
+ float zAFlipMask0 = ((zNMask | 1) << 1) * z1;
+ float xAFlipMask1 = (-2 - (xNMask << 2)) * x1 - 1.0f;
+ float yAFlipMask1 = (-2 - (yNMask << 2)) * y1 - 1.0f;
+ float zAFlipMask1 = (-2 - (zNMask << 2)) * z1 - 1.0f;
+
+ boolean skip5 = false;
+ float a2 = xAFlipMask0 + a0;
+ if (a2 > 0)
+ {
+ float x2 = x0 - (xNMask | 1);
+ float y2 = y0;
+ float z2 = z0;
+ value += (a2 * a2) * (a2 * a2) * GradCoord(seed,
+ i + (~xNMask & PrimeX), j + (yNMask & PrimeY), k + (zNMask & PrimeZ), x2, y2, z2);
+ }
+ else
+ {
+ float a3 = yAFlipMask0 + zAFlipMask0 + a0;
+ if (a3 > 0)
+ {
+ float x3 = x0;
+ float y3 = y0 - (yNMask | 1);
+ float z3 = z0 - (zNMask | 1);
+ value += (a3 * a3) * (a3 * a3) * GradCoord(seed,
+ i + (xNMask & PrimeX), j + (~yNMask & PrimeY), k + (~zNMask & PrimeZ), x3, y3, z3);
+ }
+
+ float a4 = xAFlipMask1 + a1;
+ if (a4 > 0)
+ {
+ float x4 = (xNMask | 1) + x1;
+ float y4 = y1;
+ float z4 = z1;
+ value += (a4 * a4) * (a4 * a4) * GradCoord(seed2,
+ i + (xNMask & (PrimeX * 2)), j + PrimeY, k + PrimeZ, x4, y4, z4);
+ skip5 = true;
+ }
+ }
+
+ boolean skip9 = false;
+ float a6 = yAFlipMask0 + a0;
+ if (a6 > 0)
+ {
+ float x6 = x0;
+ float y6 = y0 - (yNMask | 1);
+ float z6 = z0;
+ value += (a6 * a6) * (a6 * a6) * GradCoord(seed,
+ i + (xNMask & PrimeX), j + (~yNMask & PrimeY), k + (zNMask & PrimeZ), x6, y6, z6);
+ }
+ else
+ {
+ float a7 = xAFlipMask0 + zAFlipMask0 + a0;
+ if (a7 > 0)
+ {
+ float x7 = x0 - (xNMask | 1);
+ float y7 = y0;
+ float z7 = z0 - (zNMask | 1);
+ value += (a7 * a7) * (a7 * a7) * GradCoord(seed,
+ i + (~xNMask & PrimeX), j + (yNMask & PrimeY), k + (~zNMask & PrimeZ), x7, y7, z7);
+ }
+
+ float a8 = yAFlipMask1 + a1;
+ if (a8 > 0)
+ {
+ float x8 = x1;
+ float y8 = (yNMask | 1) + y1;
+ float z8 = z1;
+ value += (a8 * a8) * (a8 * a8) * GradCoord(seed2,
+ i + PrimeX, j + (yNMask & (PrimeY << 1)), k + PrimeZ, x8, y8, z8);
+ skip9 = true;
+ }
+ }
+
+ boolean skipD = false;
+ float aA = zAFlipMask0 + a0;
+ if (aA > 0)
+ {
+ float xA = x0;
+ float yA = y0;
+ float zA = z0 - (zNMask | 1);
+ value += (aA * aA) * (aA * aA) * GradCoord(seed,
+ i + (xNMask & PrimeX), j + (yNMask & PrimeY), k + (~zNMask & PrimeZ), xA, yA, zA);
+ }
+ else
+ {
+ float aB = xAFlipMask0 + yAFlipMask0 + a0;
+ if (aB > 0)
+ {
+ float xB = x0 - (xNMask | 1);
+ float yB = y0 - (yNMask | 1);
+ float zB = z0;
+ value += (aB * aB) * (aB * aB) * GradCoord(seed,
+ i + (~xNMask & PrimeX), j + (~yNMask & PrimeY), k + (zNMask & PrimeZ), xB, yB, zB);
+ }
+
+ float aC = zAFlipMask1 + a1;
+ if (aC > 0)
+ {
+ float xC = x1;
+ float yC = y1;
+ float zC = (zNMask | 1) + z1;
+ value += (aC * aC) * (aC * aC) * GradCoord(seed2,
+ i + PrimeX, j + PrimeY, k + (zNMask & (PrimeZ << 1)), xC, yC, zC);
+ skipD = true;
+ }
+ }
+
+ if (!skip5)
+ {
+ float a5 = yAFlipMask1 + zAFlipMask1 + a1;
+ if (a5 > 0)
+ {
+ float x5 = x1;
+ float y5 = (yNMask | 1) + y1;
+ float z5 = (zNMask | 1) + z1;
+ value += (a5 * a5) * (a5 * a5) * GradCoord(seed2,
+ i + PrimeX, j + (yNMask & (PrimeY << 1)), k + (zNMask & (PrimeZ << 1)), x5, y5, z5);
+ }
+ }
+
+ if (!skip9)
+ {
+ float a9 = xAFlipMask1 + zAFlipMask1 + a1;
+ if (a9 > 0)
+ {
+ float x9 = (xNMask | 1) + x1;
+ float y9 = y1;
+ float z9 = (zNMask | 1) + z1;
+ value += (a9 * a9) * (a9 * a9) * GradCoord(seed2,
+ i + (xNMask & (PrimeX * 2)), j + PrimeY, k + (zNMask & (PrimeZ << 1)), x9, y9, z9);
+ }
+ }
+
+ if (!skipD)
+ {
+ float aD = xAFlipMask1 + yAFlipMask1 + a1;
+ if (aD > 0)
+ {
+ float xD = (xNMask | 1) + x1;
+ float yD = (yNMask | 1) + y1;
+ float zD = z1;
+ value += (aD * aD) * (aD * aD) * GradCoord(seed2,
+ i + (xNMask & (PrimeX << 1)), j + (yNMask & (PrimeY << 1)), k + PrimeZ, xD, yD, zD);
+ }
+ }
+
+ return value * 9.046026385208288f;
+ }
+
+
+ // Cellular Noise
+
+ private float SingleCellular(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ int xr = FastRound(x);
+ int yr = FastRound(y);
+
+ float distance0 = Float.MAX_VALUE;
+ float distance1 = Float.MAX_VALUE;
+ int closestHash = 0;
+
+ float cellularJitter = 0.43701595f * mCellularJitterModifier;
+
+ int xPrimed = (xr - 1) * PrimeX;
+ int yPrimedBase = (yr - 1) * PrimeY;
+
+ switch (mCellularDistanceFunction)
+ {
+ default:
+ case Euclidean:
+ case EuclideanSq:
+ for (int xi = xr - 1; xi <= xr + 1; xi++)
+ {
+ int yPrimed = yPrimedBase;
+
+ for (int yi = yr - 1; yi <= yr + 1; yi++)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed);
+ int idx = hash & (255 << 1);
+
+ float vecX = (float)(xi - x) + RandVecs2D[idx] * cellularJitter;
+ float vecY = (float)(yi - y) + RandVecs2D[idx | 1] * cellularJitter;
+
+ float newDistance = vecX * vecX + vecY * vecY;
+
+ distance1 = FastMax(FastMin(distance1, newDistance), distance0);
+ if (newDistance < distance0)
+ {
+ distance0 = newDistance;
+ closestHash = hash;
+ }
+ yPrimed += PrimeY;
+ }
+ xPrimed += PrimeX;
+ }
+ break;
+ case Manhattan:
+ for (int xi = xr - 1; xi <= xr + 1; xi++)
+ {
+ int yPrimed = yPrimedBase;
+
+ for (int yi = yr - 1; yi <= yr + 1; yi++)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed);
+ int idx = hash & (255 << 1);
+
+ float vecX = (float)(xi - x) + RandVecs2D[idx] * cellularJitter;
+ float vecY = (float)(yi - y) + RandVecs2D[idx | 1] * cellularJitter;
+
+ float newDistance = FastAbs(vecX) + FastAbs(vecY);
+
+ distance1 = FastMax(FastMin(distance1, newDistance), distance0);
+ if (newDistance < distance0)
+ {
+ distance0 = newDistance;
+ closestHash = hash;
+ }
+ yPrimed += PrimeY;
+ }
+ xPrimed += PrimeX;
+ }
+ break;
+ case Hybrid:
+ for (int xi = xr - 1; xi <= xr + 1; xi++)
+ {
+ int yPrimed = yPrimedBase;
+
+ for (int yi = yr - 1; yi <= yr + 1; yi++)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed);
+ int idx = hash & (255 << 1);
+
+ float vecX = (float)(xi - x) + RandVecs2D[idx] * cellularJitter;
+ float vecY = (float)(yi - y) + RandVecs2D[idx | 1] * cellularJitter;
+
+ float newDistance = (FastAbs(vecX) + FastAbs(vecY)) + (vecX * vecX + vecY * vecY);
+
+ distance1 = FastMax(FastMin(distance1, newDistance), distance0);
+ if (newDistance < distance0)
+ {
+ distance0 = newDistance;
+ closestHash = hash;
+ }
+ yPrimed += PrimeY;
+ }
+ xPrimed += PrimeX;
+ }
+ break;
+ }
+
+ if (mCellularDistanceFunction == CellularDistanceFunction.Euclidean && mCellularReturnType != CellularReturnType.CellValue)
+ {
+ distance0 = FastSqrt(distance0);
+
+ if (mCellularReturnType != CellularReturnType.Distance)
+ {
+ distance1 = FastSqrt(distance1);
+ }
+ }
+
+ switch (mCellularReturnType)
+ {
+ case CellValue:
+ return closestHash * (1 / 2147483648.0f);
+ case Distance:
+ return distance0 - 1;
+ case Distance2:
+ return distance1 - 1;
+ case Distance2Add:
+ return (distance1 + distance0) * 0.5f - 1;
+ case Distance2Sub:
+ return distance1 - distance0 - 1;
+ case Distance2Mul:
+ return distance1 * distance0 * 0.5f - 1;
+ case Distance2Div:
+ return distance0 / distance1 - 1;
+ default:
+ return 0;
+ }
+ }
+
+ private float SingleCellular(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ int xr = FastRound(x);
+ int yr = FastRound(y);
+ int zr = FastRound(z);
+
+ float distance0 = Float.MAX_VALUE;
+ float distance1 = Float.MAX_VALUE;
+ int closestHash = 0;
+
+ float cellularJitter = 0.39614353f * mCellularJitterModifier;
+
+ int xPrimed = (xr - 1) * PrimeX;
+ int yPrimedBase = (yr - 1) * PrimeY;
+ int zPrimedBase = (zr - 1) * PrimeZ;
+
+ switch (mCellularDistanceFunction)
+ {
+ case Euclidean:
+ case EuclideanSq:
+ for (int xi = xr - 1; xi <= xr + 1; xi++)
+ {
+ int yPrimed = yPrimedBase;
+
+ for (int yi = yr - 1; yi <= yr + 1; yi++)
+ {
+ int zPrimed = zPrimedBase;
+
+ for (int zi = zr - 1; zi <= zr + 1; zi++)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed, zPrimed);
+ int idx = hash & (255 << 2);
+
+ float vecX = (float)(xi - x) + RandVecs3D[idx] * cellularJitter;
+ float vecY = (float)(yi - y) + RandVecs3D[idx | 1] * cellularJitter;
+ float vecZ = (float)(zi - z) + RandVecs3D[idx | 2] * cellularJitter;
+
+ float newDistance = vecX * vecX + vecY * vecY + vecZ * vecZ;
+
+ distance1 = FastMax(FastMin(distance1, newDistance), distance0);
+ if (newDistance < distance0)
+ {
+ distance0 = newDistance;
+ closestHash = hash;
+ }
+ zPrimed += PrimeZ;
+ }
+ yPrimed += PrimeY;
+ }
+ xPrimed += PrimeX;
+ }
+ break;
+ case Manhattan:
+ for (int xi = xr - 1; xi <= xr + 1; xi++)
+ {
+ int yPrimed = yPrimedBase;
+
+ for (int yi = yr - 1; yi <= yr + 1; yi++)
+ {
+ int zPrimed = zPrimedBase;
+
+ for (int zi = zr - 1; zi <= zr + 1; zi++)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed, zPrimed);
+ int idx = hash & (255 << 2);
+
+ float vecX = (float)(xi - x) + RandVecs3D[idx] * cellularJitter;
+ float vecY = (float)(yi - y) + RandVecs3D[idx | 1] * cellularJitter;
+ float vecZ = (float)(zi - z) + RandVecs3D[idx | 2] * cellularJitter;
+
+ float newDistance = FastAbs(vecX) + FastAbs(vecY) + FastAbs(vecZ);
+
+ distance1 = FastMax(FastMin(distance1, newDistance), distance0);
+ if (newDistance < distance0)
+ {
+ distance0 = newDistance;
+ closestHash = hash;
+ }
+ zPrimed += PrimeZ;
+ }
+ yPrimed += PrimeY;
+ }
+ xPrimed += PrimeX;
+ }
+ break;
+ case Hybrid:
+ for (int xi = xr - 1; xi <= xr + 1; xi++)
+ {
+ int yPrimed = yPrimedBase;
+
+ for (int yi = yr - 1; yi <= yr + 1; yi++)
+ {
+ int zPrimed = zPrimedBase;
+
+ for (int zi = zr - 1; zi <= zr + 1; zi++)
+ {
+ int hash = Hash(seed, xPrimed, yPrimed, zPrimed);
+ int idx = hash & (255 << 2);
+
+ float vecX = (float)(xi - x) + RandVecs3D[idx] * cellularJitter;
+ float vecY = (float)(yi - y) + RandVecs3D[idx | 1] * cellularJitter;
+ float vecZ = (float)(zi - z) + RandVecs3D[idx | 2] * cellularJitter;
+
+ float newDistance = (FastAbs(vecX) + FastAbs(vecY) + FastAbs(vecZ)) + (vecX * vecX + vecY * vecY + vecZ * vecZ);
+
+ distance1 = FastMax(FastMin(distance1, newDistance), distance0);
+ if (newDistance < distance0)
+ {
+ distance0 = newDistance;
+ closestHash = hash;
+ }
+ zPrimed += PrimeZ;
+ }
+ yPrimed += PrimeY;
+ }
+ xPrimed += PrimeX;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (mCellularDistanceFunction == CellularDistanceFunction.Euclidean && mCellularReturnType != CellularReturnType.CellValue)
+ {
+ distance0 = FastSqrt(distance0);
+
+ if (mCellularReturnType != CellularReturnType.Distance)
+ {
+ distance1 = FastSqrt(distance1);
+ }
+ }
+
+ switch (mCellularReturnType)
+ {
+ case CellValue:
+ return closestHash * (1 / 2147483648.0f);
+ case Distance:
+ return distance0 - 1;
+ case Distance2:
+ return distance1 - 1;
+ case Distance2Add:
+ return (distance1 + distance0) * 0.5f - 1;
+ case Distance2Sub:
+ return distance1 - distance0 - 1;
+ case Distance2Mul:
+ return distance1 * distance0 * 0.5f - 1;
+ case Distance2Div:
+ return distance0 / distance1 - 1;
+ default:
+ return 0;
+ }
+ }
+
+
+ // Perlin Noise
+
+ private float SinglePerlin(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ int x0 = FastFloor(x);
+ int y0 = FastFloor(y);
+
+ float xd0 = (float)(x - x0);
+ float yd0 = (float)(y - y0);
+ float xd1 = xd0 - 1;
+ float yd1 = yd0 - 1;
+
+ float xs = InterpQuintic(xd0);
+ float ys = InterpQuintic(yd0);
+
+ x0 *= PrimeX;
+ y0 *= PrimeY;
+ int x1 = x0 + PrimeX;
+ int y1 = y0 + PrimeY;
+
+ float xf0 = Lerp(GradCoord(seed, x0, y0, xd0, yd0), GradCoord(seed, x1, y0, xd1, yd0), xs);
+ float xf1 = Lerp(GradCoord(seed, x0, y1, xd0, yd1), GradCoord(seed, x1, y1, xd1, yd1), xs);
+
+ return Lerp(xf0, xf1, ys) * 1.4247691104677813f;
+ }
+
+ private float SinglePerlin(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ int x0 = FastFloor(x);
+ int y0 = FastFloor(y);
+ int z0 = FastFloor(z);
+
+ float xd0 = (float)(x - x0);
+ float yd0 = (float)(y - y0);
+ float zd0 = (float)(z - z0);
+ float xd1 = xd0 - 1;
+ float yd1 = yd0 - 1;
+ float zd1 = zd0 - 1;
+
+ float xs = InterpQuintic(xd0);
+ float ys = InterpQuintic(yd0);
+ float zs = InterpQuintic(zd0);
+
+ x0 *= PrimeX;
+ y0 *= PrimeY;
+ z0 *= PrimeZ;
+ int x1 = x0 + PrimeX;
+ int y1 = y0 + PrimeY;
+ int z1 = z0 + PrimeZ;
+
+ float xf00 = Lerp(GradCoord(seed, x0, y0, z0, xd0, yd0, zd0), GradCoord(seed, x1, y0, z0, xd1, yd0, zd0), xs);
+ float xf10 = Lerp(GradCoord(seed, x0, y1, z0, xd0, yd1, zd0), GradCoord(seed, x1, y1, z0, xd1, yd1, zd0), xs);
+ float xf01 = Lerp(GradCoord(seed, x0, y0, z1, xd0, yd0, zd1), GradCoord(seed, x1, y0, z1, xd1, yd0, zd1), xs);
+ float xf11 = Lerp(GradCoord(seed, x0, y1, z1, xd0, yd1, zd1), GradCoord(seed, x1, y1, z1, xd1, yd1, zd1), xs);
+
+ float yf0 = Lerp(xf00, xf10, ys);
+ float yf1 = Lerp(xf01, xf11, ys);
+
+ return Lerp(yf0, yf1, zs) * 0.964921414852142333984375f;
+ }
+
+
+ // Value Cubic Noise
+
+ private float SingleValueCubic(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ int x1 = FastFloor(x);
+ int y1 = FastFloor(y);
+
+ float xs = (float)(x - x1);
+ float ys = (float)(y - y1);
+
+ x1 *= PrimeX;
+ y1 *= PrimeY;
+ int x0 = x1 - PrimeX;
+ int y0 = y1 - PrimeY;
+ int x2 = x1 + PrimeX;
+ int y2 = y1 + PrimeY;
+ int x3 = x1 + (PrimeX << 1);
+ int y3 = y1 + (PrimeY << 1);
+
+ return CubicLerp(
+ CubicLerp(ValCoord(seed, x0, y0), ValCoord(seed, x1, y0), ValCoord(seed, x2, y0), ValCoord(seed, x3, y0),
+ xs),
+ CubicLerp(ValCoord(seed, x0, y1), ValCoord(seed, x1, y1), ValCoord(seed, x2, y1), ValCoord(seed, x3, y1),
+ xs),
+ CubicLerp(ValCoord(seed, x0, y2), ValCoord(seed, x1, y2), ValCoord(seed, x2, y2), ValCoord(seed, x3, y2),
+ xs),
+ CubicLerp(ValCoord(seed, x0, y3), ValCoord(seed, x1, y3), ValCoord(seed, x2, y3), ValCoord(seed, x3, y3),
+ xs),
+ ys) * (1 / (1.5f * 1.5f));
+ }
+
+ private float SingleValueCubic(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ int x1 = FastFloor(x);
+ int y1 = FastFloor(y);
+ int z1 = FastFloor(z);
+
+ float xs = (float)(x - x1);
+ float ys = (float)(y - y1);
+ float zs = (float)(z - z1);
+
+ x1 *= PrimeX;
+ y1 *= PrimeY;
+ z1 *= PrimeZ;
+
+ int x0 = x1 - PrimeX;
+ int y0 = y1 - PrimeY;
+ int z0 = z1 - PrimeZ;
+ int x2 = x1 + PrimeX;
+ int y2 = y1 + PrimeY;
+ int z2 = z1 + PrimeZ;
+ int x3 = x1 + (PrimeX << 1);
+ int y3 = y1 + (PrimeY << 1);
+ int z3 = z1 + (PrimeZ << 1);
+
+
+ return CubicLerp(
+ CubicLerp(
+ CubicLerp(ValCoord(seed, x0, y0, z0), ValCoord(seed, x1, y0, z0), ValCoord(seed, x2, y0, z0), ValCoord(seed, x3, y0, z0), xs),
+ CubicLerp(ValCoord(seed, x0, y1, z0), ValCoord(seed, x1, y1, z0), ValCoord(seed, x2, y1, z0), ValCoord(seed, x3, y1, z0), xs),
+ CubicLerp(ValCoord(seed, x0, y2, z0), ValCoord(seed, x1, y2, z0), ValCoord(seed, x2, y2, z0), ValCoord(seed, x3, y2, z0), xs),
+ CubicLerp(ValCoord(seed, x0, y3, z0), ValCoord(seed, x1, y3, z0), ValCoord(seed, x2, y3, z0), ValCoord(seed, x3, y3, z0), xs),
+ ys),
+ CubicLerp(
+ CubicLerp(ValCoord(seed, x0, y0, z1), ValCoord(seed, x1, y0, z1), ValCoord(seed, x2, y0, z1), ValCoord(seed, x3, y0, z1), xs),
+ CubicLerp(ValCoord(seed, x0, y1, z1), ValCoord(seed, x1, y1, z1), ValCoord(seed, x2, y1, z1), ValCoord(seed, x3, y1, z1), xs),
+ CubicLerp(ValCoord(seed, x0, y2, z1), ValCoord(seed, x1, y2, z1), ValCoord(seed, x2, y2, z1), ValCoord(seed, x3, y2, z1), xs),
+ CubicLerp(ValCoord(seed, x0, y3, z1), ValCoord(seed, x1, y3, z1), ValCoord(seed, x2, y3, z1), ValCoord(seed, x3, y3, z1), xs),
+ ys),
+ CubicLerp(
+ CubicLerp(ValCoord(seed, x0, y0, z2), ValCoord(seed, x1, y0, z2), ValCoord(seed, x2, y0, z2), ValCoord(seed, x3, y0, z2), xs),
+ CubicLerp(ValCoord(seed, x0, y1, z2), ValCoord(seed, x1, y1, z2), ValCoord(seed, x2, y1, z2), ValCoord(seed, x3, y1, z2), xs),
+ CubicLerp(ValCoord(seed, x0, y2, z2), ValCoord(seed, x1, y2, z2), ValCoord(seed, x2, y2, z2), ValCoord(seed, x3, y2, z2), xs),
+ CubicLerp(ValCoord(seed, x0, y3, z2), ValCoord(seed, x1, y3, z2), ValCoord(seed, x2, y3, z2), ValCoord(seed, x3, y3, z2), xs),
+ ys),
+ CubicLerp(
+ CubicLerp(ValCoord(seed, x0, y0, z3), ValCoord(seed, x1, y0, z3), ValCoord(seed, x2, y0, z3), ValCoord(seed, x3, y0, z3), xs),
+ CubicLerp(ValCoord(seed, x0, y1, z3), ValCoord(seed, x1, y1, z3), ValCoord(seed, x2, y1, z3), ValCoord(seed, x3, y1, z3), xs),
+ CubicLerp(ValCoord(seed, x0, y2, z3), ValCoord(seed, x1, y2, z3), ValCoord(seed, x2, y2, z3), ValCoord(seed, x3, y2, z3), xs),
+ CubicLerp(ValCoord(seed, x0, y3, z3), ValCoord(seed, x1, y3, z3), ValCoord(seed, x2, y3, z3), ValCoord(seed, x3, y3, z3), xs),
+ ys),
+ zs) * (1 / (1.5f * 1.5f * 1.5f));
+ }
+
+
+ // Value Noise
+
+ private float SingleValue(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ int x0 = FastFloor(x);
+ int y0 = FastFloor(y);
+
+ float xs = InterpHermite((float)(x - x0));
+ float ys = InterpHermite((float)(y - y0));
+
+ x0 *= PrimeX;
+ y0 *= PrimeY;
+ int x1 = x0 + PrimeX;
+ int y1 = y0 + PrimeY;
+
+ float xf0 = Lerp(ValCoord(seed, x0, y0), ValCoord(seed, x1, y0), xs);
+ float xf1 = Lerp(ValCoord(seed, x0, y1), ValCoord(seed, x1, y1), xs);
+
+ return Lerp(xf0, xf1, ys);
+ }
+
+ private float SingleValue(int seed, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ int x0 = FastFloor(x);
+ int y0 = FastFloor(y);
+ int z0 = FastFloor(z);
+
+ float xs = InterpHermite((float)(x - x0));
+ float ys = InterpHermite((float)(y - y0));
+ float zs = InterpHermite((float)(z - z0));
+
+ x0 *= PrimeX;
+ y0 *= PrimeY;
+ z0 *= PrimeZ;
+ int x1 = x0 + PrimeX;
+ int y1 = y0 + PrimeY;
+ int z1 = z0 + PrimeZ;
+
+ float xf00 = Lerp(ValCoord(seed, x0, y0, z0), ValCoord(seed, x1, y0, z0), xs);
+ float xf10 = Lerp(ValCoord(seed, x0, y1, z0), ValCoord(seed, x1, y1, z0), xs);
+ float xf01 = Lerp(ValCoord(seed, x0, y0, z1), ValCoord(seed, x1, y0, z1), xs);
+ float xf11 = Lerp(ValCoord(seed, x0, y1, z1), ValCoord(seed, x1, y1, z1), xs);
+
+ float yf0 = Lerp(xf00, xf10, ys);
+ float yf1 = Lerp(xf01, xf11, ys);
+
+ return Lerp(yf0, yf1, zs);
+ }
+
+
+ // Domain Warp
+
+ private void DoSingleDomainWarp(int seed, float amp, float freq, /*FNLfloat*/ double x, /*FNLfloat*/ double y, Vector2 coord)
+ {
+ switch (mDomainWarpType)
+ {
+ case OpenSimplex2:
+ SingleDomainWarpSimplexGradient(seed, amp * 38.283687591552734375f, freq, x, y, coord, false);
+ break;
+ case OpenSimplex2Reduced:
+ SingleDomainWarpSimplexGradient(seed, amp * 16.0f, freq, x, y, coord, true);
+ break;
+ case BasicGrid:
+ SingleDomainWarpBasicGrid(seed, amp, freq, x, y, coord);
+ break;
+ }
+ }
+
+ private void DoSingleDomainWarp(int seed, float amp, float freq, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z, Vector3 coord)
+ {
+ switch (mDomainWarpType)
+ {
+ case OpenSimplex2:
+ SingleDomainWarpOpenSimplex2Gradient(seed, amp * 32.69428253173828125f, freq, x, y, z, coord, false);
+ break;
+ case OpenSimplex2Reduced:
+ SingleDomainWarpOpenSimplex2Gradient(seed, amp * 7.71604938271605f, freq, x, y, z, coord, true);
+ break;
+ case BasicGrid:
+ SingleDomainWarpBasicGrid(seed, amp, freq, x, y, z, coord);
+ break;
+ }
+ }
+
+
+ // Domain Warp Single Wrapper
+
+ private void DomainWarpSingle(Vector2 coord)
+ {
+ int seed = mSeed;
+ float amp = mDomainWarpAmp * mFractalBounding;
+ float freq = mFrequency;
+
+ /*FNLfloat*/ double xs = coord.x;
+ /*FNLfloat*/ double ys = coord.y;
+ switch (mDomainWarpType)
+ {
+ case OpenSimplex2:
+ case OpenSimplex2Reduced:
+ {
+ final /*FNLfloat*/ double SQRT3 = (/*FNLfloat*/ double)1.7320508075688772935274463415059;
+ final /*FNLfloat*/ double F2 = 0.5f * (SQRT3 - 1);
+ /*FNLfloat*/ double t = (xs + ys) * F2;
+ xs += t; ys += t;
+ }
+ break;
+ default:
+ break;
+ }
+
+ DoSingleDomainWarp(seed, amp, freq, xs, ys, coord);
+ }
+
+ private void DomainWarpSingle(Vector3 coord)
+ {
+ int seed = mSeed;
+ float amp = mDomainWarpAmp * mFractalBounding;
+ float freq = mFrequency;
+
+ /*FNLfloat*/ double xs = coord.x;
+ /*FNLfloat*/ double ys = coord.y;
+ /*FNLfloat*/ double zs = coord.z;
+ switch (mWarpTransformType3D)
+ {
+ case ImproveXYPlanes:
+ {
+ /*FNLfloat*/ double xy = xs + ys;
+ /*FNLfloat*/ double s2 = xy * -(/*FNLfloat*/ double)0.211324865405187;
+ zs *= (/*FNLfloat*/ double)0.577350269189626;
+ xs += s2 - zs;
+ ys = ys + s2 - zs;
+ zs += xy * (/*FNLfloat*/ double)0.577350269189626;
+ }
+ break;
+ case ImproveXZPlanes:
+ {
+ /*FNLfloat*/ double xz = xs + zs;
+ /*FNLfloat*/ double s2 = xz * -(/*FNLfloat*/ double)0.211324865405187;
+ ys *= (/*FNLfloat*/ double)0.577350269189626;
+ xs += s2 - ys; zs += s2 - ys;
+ ys += xz * (/*FNLfloat*/ double)0.577350269189626;
+ }
+ break;
+ case DefaultOpenSimplex2:
+ {
+ final /*FNLfloat*/ double R3 = (/*FNLfloat*/ double)(2.0 / 3.0);
+ /*FNLfloat*/ double r = (xs + ys + zs) * R3; // Rotation, not skew
+ xs = r - xs;
+ ys = r - ys;
+ zs = r - zs;
+ }
+ break;
+ default:
+ break;
+ }
+
+ DoSingleDomainWarp(seed, amp, freq, xs, ys, zs, coord);
+ }
+
+
+ // Domain Warp Fractal Progressive
+
+ private void DomainWarpFractalProgressive(Vector2 coord)
+ {
+ int seed = mSeed;
+ float amp = mDomainWarpAmp * mFractalBounding;
+ float freq = mFrequency;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ /*FNLfloat*/ double xs = coord.x;
+ /*FNLfloat*/ double ys = coord.y;
+ switch (mDomainWarpType)
+ {
+ case OpenSimplex2:
+ case OpenSimplex2Reduced:
+ {
+ final /*FNLfloat*/ double SQRT3 = (/*FNLfloat*/ double)1.7320508075688772935274463415059;
+ final /*FNLfloat*/ double F2 = 0.5f * (SQRT3 - 1);
+ /*FNLfloat*/ double t = (xs + ys) * F2;
+ xs += t; ys += t;
+ }
+ break;
+ default:
+ break;
+ }
+
+ DoSingleDomainWarp(seed, amp, freq, xs, ys, coord);
+
+ seed++;
+ amp *= mGain;
+ freq *= mLacunarity;
+ }
+ }
+
+ private void DomainWarpFractalProgressive(Vector3 coord)
+ {
+ int seed = mSeed;
+ float amp = mDomainWarpAmp * mFractalBounding;
+ float freq = mFrequency;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ /*FNLfloat*/ double xs = coord.x;
+ /*FNLfloat*/ double ys = coord.y;
+ /*FNLfloat*/ double zs = coord.z;
+ switch (mWarpTransformType3D)
+ {
+ case ImproveXYPlanes:
+ {
+ /*FNLfloat*/ double xy = xs + ys;
+ /*FNLfloat*/ double s2 = xy * -(/*FNLfloat*/ double)0.211324865405187;
+ zs *= (/*FNLfloat*/ double)0.577350269189626;
+ xs += s2 - zs;
+ ys = ys + s2 - zs;
+ zs += xy * (/*FNLfloat*/ double)0.577350269189626;
+ }
+ break;
+ case ImproveXZPlanes:
+ {
+ /*FNLfloat*/ double xz = xs + zs;
+ /*FNLfloat*/ double s2 = xz * -(/*FNLfloat*/ double)0.211324865405187;
+ ys *= (/*FNLfloat*/ double)0.577350269189626;
+ xs += s2 - ys; zs += s2 - ys;
+ ys += xz * (/*FNLfloat*/ double)0.577350269189626;
+ }
+ break;
+ case DefaultOpenSimplex2:
+ {
+ final /*FNLfloat*/ double R3 = (/*FNLfloat*/ double)(2.0 / 3.0);
+ /*FNLfloat*/ double r = (xs + ys + zs) * R3; // Rotation, not skew
+ xs = r - xs;
+ ys = r - ys;
+ zs = r - zs;
+ }
+ break;
+ default:
+ break;
+ }
+
+ DoSingleDomainWarp(seed, amp, freq, xs, ys, zs, coord);
+
+ seed++;
+ amp *= mGain;
+ freq *= mLacunarity;
+ }
+ }
+
+
+ // Domain Warp Fractal Independant
+ private void DomainWarpFractalIndependent(Vector2 coord)
+ {
+ /*FNLfloat*/ double xs = coord.x;
+ /*FNLfloat*/ double ys = coord.y;
+ switch (mDomainWarpType)
+ {
+ case OpenSimplex2:
+ case OpenSimplex2Reduced:
+ {
+ final /*FNLfloat*/ double SQRT3 = (/*FNLfloat*/ double)1.7320508075688772935274463415059;
+ final /*FNLfloat*/ double F2 = 0.5f * (SQRT3 - 1);
+ /*FNLfloat*/ double t = (xs + ys) * F2;
+ xs += t; ys += t;
+ }
+ break;
+ default:
+ break;
+ }
+
+ int seed = mSeed;
+ float amp = mDomainWarpAmp * mFractalBounding;
+ float freq = mFrequency;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ DoSingleDomainWarp(seed, amp, freq, xs, ys, coord);
+
+ seed++;
+ amp *= mGain;
+ freq *= mLacunarity;
+ }
+ }
+
+ private void DomainWarpFractalIndependent(Vector3 coord)
+ {
+ /*FNLfloat*/ double xs = coord.x;
+ /*FNLfloat*/ double ys = coord.y;
+ /*FNLfloat*/ double zs = coord.z;
+ switch (mWarpTransformType3D)
+ {
+ case ImproveXYPlanes:
+ {
+ /*FNLfloat*/ double xy = xs + ys;
+ /*FNLfloat*/ double s2 = xy * -(/*FNLfloat*/ double)0.211324865405187;
+ zs *= (/*FNLfloat*/ double)0.577350269189626;
+ xs += s2 - zs;
+ ys = ys + s2 - zs;
+ zs += xy * (/*FNLfloat*/ double)0.577350269189626;
+ }
+ break;
+ case ImproveXZPlanes:
+ {
+ /*FNLfloat*/ double xz = xs + zs;
+ /*FNLfloat*/ double s2 = xz * -(/*FNLfloat*/ double)0.211324865405187;
+ ys *= (/*FNLfloat*/ double)0.577350269189626;
+ xs += s2 - ys; zs += s2 - ys;
+ ys += xz * (/*FNLfloat*/ double)0.577350269189626;
+ }
+ break;
+ case DefaultOpenSimplex2:
+ {
+ final /*FNLfloat*/ double R3 = (/*FNLfloat*/ double)(2.0 / 3.0);
+ /*FNLfloat*/ double r = (xs + ys + zs) * R3; // Rotation, not skew
+ xs = r - xs;
+ ys = r - ys;
+ zs = r - zs;
+ }
+ break;
+ default:
+ break;
+ }
+
+ int seed = mSeed;
+ float amp = mDomainWarpAmp * mFractalBounding;
+ float freq = mFrequency;
+
+ for (int i = 0; i < mOctaves; i++)
+ {
+ DoSingleDomainWarp(seed, amp, freq, xs, ys, zs, coord);
+
+ seed++;
+ amp *= mGain;
+ freq *= mLacunarity;
+ }
+ }
+
+
+ // Domain Warp Basic Grid
+
+ private void SingleDomainWarpBasicGrid(int seed, float warpAmp, float frequency, /*FNLfloat*/ double x, /*FNLfloat*/ double y, Vector2 coord)
+ {
+ /*FNLfloat*/ double xf = x * frequency;
+ /*FNLfloat*/ double yf = y * frequency;
+
+ int x0 = FastFloor(xf);
+ int y0 = FastFloor(yf);
+
+ float xs = InterpHermite((float)(xf - x0));
+ float ys = InterpHermite((float)(yf - y0));
+
+ x0 *= PrimeX;
+ y0 *= PrimeY;
+ int x1 = x0 + PrimeX;
+ int y1 = y0 + PrimeY;
+
+ int hash0 = Hash(seed, x0, y0) & (255 << 1);
+ int hash1 = Hash(seed, x1, y0) & (255 << 1);
+
+ float lx0x = Lerp(RandVecs2D[hash0], RandVecs2D[hash1], xs);
+ float ly0x = Lerp(RandVecs2D[hash0 | 1], RandVecs2D[hash1 | 1], xs);
+
+ hash0 = Hash(seed, x0, y1) & (255 << 1);
+ hash1 = Hash(seed, x1, y1) & (255 << 1);
+
+ float lx1x = Lerp(RandVecs2D[hash0], RandVecs2D[hash1], xs);
+ float ly1x = Lerp(RandVecs2D[hash0 | 1], RandVecs2D[hash1 | 1], xs);
+
+ coord.x += Lerp(lx0x, lx1x, ys) * warpAmp;
+ coord.y += Lerp(ly0x, ly1x, ys) * warpAmp;
+ }
+
+ private void SingleDomainWarpBasicGrid(int seed, float warpAmp, float frequency, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z, Vector3 coord)
+ {
+ /*FNLfloat*/ double xf = x * frequency;
+ /*FNLfloat*/ double yf = y * frequency;
+ /*FNLfloat*/ double zf = z * frequency;
+
+ int x0 = FastFloor(xf);
+ int y0 = FastFloor(yf);
+ int z0 = FastFloor(zf);
+
+ float xs = InterpHermite((float)(xf - x0));
+ float ys = InterpHermite((float)(yf - y0));
+ float zs = InterpHermite((float)(zf - z0));
+
+ x0 *= PrimeX;
+ y0 *= PrimeY;
+ z0 *= PrimeZ;
+ int x1 = x0 + PrimeX;
+ int y1 = y0 + PrimeY;
+ int z1 = z0 + PrimeZ;
+
+ int hash0 = Hash(seed, x0, y0, z0) & (255 << 2);
+ int hash1 = Hash(seed, x1, y0, z0) & (255 << 2);
+
+ float lx0x = Lerp(RandVecs3D[hash0], RandVecs3D[hash1], xs);
+ float ly0x = Lerp(RandVecs3D[hash0 | 1], RandVecs3D[hash1 | 1], xs);
+ float lz0x = Lerp(RandVecs3D[hash0 | 2], RandVecs3D[hash1 | 2], xs);
+
+ hash0 = Hash(seed, x0, y1, z0) & (255 << 2);
+ hash1 = Hash(seed, x1, y1, z0) & (255 << 2);
+
+ float lx1x = Lerp(RandVecs3D[hash0], RandVecs3D[hash1], xs);
+ float ly1x = Lerp(RandVecs3D[hash0 | 1], RandVecs3D[hash1 | 1], xs);
+ float lz1x = Lerp(RandVecs3D[hash0 | 2], RandVecs3D[hash1 | 2], xs);
+
+ float lx0y = Lerp(lx0x, lx1x, ys);
+ float ly0y = Lerp(ly0x, ly1x, ys);
+ float lz0y = Lerp(lz0x, lz1x, ys);
+
+ hash0 = Hash(seed, x0, y0, z1) & (255 << 2);
+ hash1 = Hash(seed, x1, y0, z1) & (255 << 2);
+
+ lx0x = Lerp(RandVecs3D[hash0], RandVecs3D[hash1], xs);
+ ly0x = Lerp(RandVecs3D[hash0 | 1], RandVecs3D[hash1 | 1], xs);
+ lz0x = Lerp(RandVecs3D[hash0 | 2], RandVecs3D[hash1 | 2], xs);
+
+ hash0 = Hash(seed, x0, y1, z1) & (255 << 2);
+ hash1 = Hash(seed, x1, y1, z1) & (255 << 2);
+
+ lx1x = Lerp(RandVecs3D[hash0], RandVecs3D[hash1], xs);
+ ly1x = Lerp(RandVecs3D[hash0 | 1], RandVecs3D[hash1 | 1], xs);
+ lz1x = Lerp(RandVecs3D[hash0 | 2], RandVecs3D[hash1 | 2], xs);
+
+ coord.x += Lerp(lx0y, Lerp(lx0x, lx1x, ys), zs) * warpAmp;
+ coord.y += Lerp(ly0y, Lerp(ly0x, ly1x, ys), zs) * warpAmp;
+ coord.z += Lerp(lz0y, Lerp(lz0x, lz1x, ys), zs) * warpAmp;
+ }
+
+
+ // Domain Warp Simplex/OpenSimplex2
+ private void SingleDomainWarpSimplexGradient(int seed, float warpAmp, float frequency, /*FNLfloat*/ double x, /*FNLfloat*/ double y, Vector2 coord, boolean outGradOnly)
+ {
+ final float SQRT3 = 1.7320508075688772935274463415059f;
+ final float G2 = (3 - SQRT3) / 6;
+
+ x *= frequency;
+ y *= frequency;
+
+ /*
+ * --- Skew moved to switch statements before fractal evaluation ---
+ * final FNLfloat F2 = 0.5f * (SQRT3 - 1);
+ * FNLfloat s = (x + y) * F2;
+ * x += s; y += s;
+ */
+
+ int i = FastFloor(x);
+ int j = FastFloor(y);
+ float xi = (float)(x - i);
+ float yi = (float)(y - j);
+
+ float t = (xi + yi) * G2;
+ float x0 = (float)(xi - t);
+ float y0 = (float)(yi - t);
+
+ i *= PrimeX;
+ j *= PrimeY;
+
+ float vx, vy;
+ vx = vy = 0;
+
+ float a = 0.5f - x0 * x0 - y0 * y0;
+ if (a > 0)
+ {
+ float aaaa = (a * a) * (a * a);
+ float xo, yo;
+ if (outGradOnly)
+ {
+ int hash = Hash(seed, i, j) & (255 << 1);
+ xo = RandVecs2D[hash];
+ yo = RandVecs2D[hash | 1];
+ }
+ else
+ {
+ int hash = Hash(seed, i, j);
+ int index1 = hash & (127 << 1);
+ int index2 = (hash >> 7) & (255 << 1);
+ float xg = Gradients2D[index1];
+ float yg = Gradients2D[index1 | 1];
+ float value = x0 * xg + y0 * yg;
+ float xgo = RandVecs2D[index2];
+ float ygo = RandVecs2D[index2 | 1];
+ xo = value * xgo;
+ yo = value * ygo;
+ }
+ vx += aaaa * xo;
+ vy += aaaa * yo;
+ }
+
+ float c = (float)(2 * (1 - 2 * G2) * (1 / G2 - 2)) * t + ((float)(-2 * (1 - 2 * G2) * (1 - 2 * G2)) + a);
+ if (c > 0)
+ {
+ float x2 = x0 + (2 * (float)G2 - 1);
+ float y2 = y0 + (2 * (float)G2 - 1);
+ float cccc = (c * c) * (c * c);
+ float xo, yo;
+ if (outGradOnly)
+ {
+ int hash = Hash(seed, i + PrimeX, j + PrimeY) & (255 << 1);
+ xo = RandVecs2D[hash];
+ yo = RandVecs2D[hash | 1];
+ }
+ else
+ {
+ int hash = Hash(seed, i + PrimeX, j + PrimeY);
+ int index1 = hash & (127 << 1);
+ int index2 = (hash >> 7) & (255 << 1);
+ float xg = Gradients2D[index1];
+ float yg = Gradients2D[index1 | 1];
+ float value = x2 * xg + y2 * yg;
+ float xgo = RandVecs2D[index2];
+ float ygo = RandVecs2D[index2 | 1];
+ xo = value * xgo;
+ yo = value * ygo;
+ }
+ vx += cccc * xo;
+ vy += cccc * yo;
+ }
+
+ if (y0 > x0)
+ {
+ float x1 = x0 + (float)G2;
+ float y1 = y0 + ((float)G2 - 1);
+ float b = 0.5f - x1 * x1 - y1 * y1;
+ if (b > 0)
+ {
+ float bbbb = (b * b) * (b * b);
+ float xo, yo;
+ if (outGradOnly)
+ {
+ int hash = Hash(seed, i, j + PrimeY) & (255 << 1);
+ xo = RandVecs2D[hash];
+ yo = RandVecs2D[hash | 1];
+ }
+ else
+ {
+ int hash = Hash(seed, i, j + PrimeY);
+ int index1 = hash & (127 << 1);
+ int index2 = (hash >> 7) & (255 << 1);
+ float xg = Gradients2D[index1];
+ float yg = Gradients2D[index1 | 1];
+ float value = x1 * xg + y1 * yg;
+ float xgo = RandVecs2D[index2];
+ float ygo = RandVecs2D[index2 | 1];
+ xo = value * xgo;
+ yo = value * ygo;
+ }
+ vx += bbbb * xo;
+ vy += bbbb * yo;
+ }
+ }
+ else
+ {
+ float x1 = x0 + ((float)G2 - 1);
+ float y1 = y0 + (float)G2;
+ float b = 0.5f - x1 * x1 - y1 * y1;
+ if (b > 0)
+ {
+ float bbbb = (b * b) * (b * b);
+ float xo, yo;
+ if (outGradOnly)
+ {
+ int hash = Hash(seed, i + PrimeX, j) & (255 << 1);
+ xo = RandVecs2D[hash];
+ yo = RandVecs2D[hash | 1];
+ }
+ else
+ {
+ int hash = Hash(seed, i + PrimeX, j);
+ int index1 = hash & (127 << 1);
+ int index2 = (hash >> 7) & (255 << 1);
+ float xg = Gradients2D[index1];
+ float yg = Gradients2D[index1 | 1];
+ float value = x1 * xg + y1 * yg;
+ float xgo = RandVecs2D[index2];
+ float ygo = RandVecs2D[index2 | 1];
+ xo = value * xgo;
+ yo = value * ygo;
+ }
+ vx += bbbb * xo;
+ vy += bbbb * yo;
+ }
+ }
+
+ coord.x += vx * warpAmp;
+ coord.y += vy * warpAmp;
+ }
+
+ private void SingleDomainWarpOpenSimplex2Gradient(int seed, float warpAmp, float frequency, /*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z, Vector3 coord, boolean outGradOnly)
+ {
+ x *= frequency;
+ y *= frequency;
+ z *= frequency;
+
+ /*
+ * --- Rotation moved to switch statements before fractal evaluation ---
+ * final FNLfloat R3 = (FNLfloat)(2.0 / 3.0);
+ * FNLfloat r = (x + y + z) * R3; // Rotation, not skew
+ * x = r - x; y = r - y; z = r - z;
+ */
+
+ int i = FastRound(x);
+ int j = FastRound(y);
+ int k = FastRound(z);
+ float x0 = (float)x - i;
+ float y0 = (float)y - j;
+ float z0 = (float)z - k;
+
+ int xNSign = (int)(-x0 - 1.0f) | 1;
+ int yNSign = (int)(-y0 - 1.0f) | 1;
+ int zNSign = (int)(-z0 - 1.0f) | 1;
+
+ float ax0 = xNSign * -x0;
+ float ay0 = yNSign * -y0;
+ float az0 = zNSign * -z0;
+
+ i *= PrimeX;
+ j *= PrimeY;
+ k *= PrimeZ;
+
+ float vx, vy, vz;
+ vx = vy = vz = 0;
+
+ float a = (0.6f - x0 * x0) - (y0 * y0 + z0 * z0);
+ for (int l = 0; ; l++)
+ {
+ if (a > 0)
+ {
+ float aaaa = (a * a) * (a * a);
+ float xo, yo, zo;
+ if (outGradOnly)
+ {
+ int hash = Hash(seed, i, j, k) & (255 << 2);
+ xo = RandVecs3D[hash];
+ yo = RandVecs3D[hash | 1];
+ zo = RandVecs3D[hash | 2];
+ }
+ else
+ {
+ int hash = Hash(seed, i, j, k);
+ int index1 = hash & (63 << 2);
+ int index2 = (hash >> 6) & (255 << 2);
+ float xg = Gradients3D[index1];
+ float yg = Gradients3D[index1 | 1];
+ float zg = Gradients3D[index1 | 2];
+ float value = x0 * xg + y0 * yg + z0 * zg;
+ float xgo = RandVecs3D[index2];
+ float ygo = RandVecs3D[index2 | 1];
+ float zgo = RandVecs3D[index2 | 2];
+ xo = value * xgo;
+ yo = value * ygo;
+ zo = value * zgo;
+ }
+ vx += aaaa * xo;
+ vy += aaaa * yo;
+ vz += aaaa * zo;
+ }
+
+ float b = a;
+ int i1 = i;
+ int j1 = j;
+ int k1 = k;
+ float x1 = x0;
+ float y1 = y0;
+ float z1 = z0;
+
+ if (ax0 >= ay0 && ax0 >= az0)
+ {
+ x1 += xNSign;
+ b = b + ax0 + ax0;
+ i1 -= xNSign * PrimeX;
+ }
+ else if (ay0 > ax0 && ay0 >= az0)
+ {
+ y1 += yNSign;
+ b = b + ay0 + ay0;
+ j1 -= yNSign * PrimeY;
+ }
+ else
+ {
+ z1 += zNSign;
+ b = b + az0 + az0;
+ k1 -= zNSign * PrimeZ;
+ }
+
+ if (b > 1)
+ {
+ b -= 1;
+ float bbbb = (b * b) * (b * b);
+ float xo, yo, zo;
+ if (outGradOnly)
+ {
+ int hash = Hash(seed, i1, j1, k1) & (255 << 2);
+ xo = RandVecs3D[hash];
+ yo = RandVecs3D[hash | 1];
+ zo = RandVecs3D[hash | 2];
+ }
+ else
+ {
+ int hash = Hash(seed, i1, j1, k1);
+ int index1 = hash & (63 << 2);
+ int index2 = (hash >> 6) & (255 << 2);
+ float xg = Gradients3D[index1];
+ float yg = Gradients3D[index1 | 1];
+ float zg = Gradients3D[index1 | 2];
+ float value = x1 * xg + y1 * yg + z1 * zg;
+ float xgo = RandVecs3D[index2];
+ float ygo = RandVecs3D[index2 | 1];
+ float zgo = RandVecs3D[index2 | 2];
+ xo = value * xgo;
+ yo = value * ygo;
+ zo = value * zgo;
+ }
+ vx += bbbb * xo;
+ vy += bbbb * yo;
+ vz += bbbb * zo;
+ }
+
+ if (l == 1) break;
+
+ ax0 = 0.5f - ax0;
+ ay0 = 0.5f - ay0;
+ az0 = 0.5f - az0;
+
+ x0 = xNSign * ax0;
+ y0 = yNSign * ay0;
+ z0 = zNSign * az0;
+
+ a += (0.75f - ax0) - (ay0 + az0);
+
+ i += (xNSign >> 1) & PrimeX;
+ j += (yNSign >> 1) & PrimeY;
+ k += (zNSign >> 1) & PrimeZ;
+
+ xNSign = -xNSign;
+ yNSign = -yNSign;
+ zNSign = -zNSign;
+
+ seed += 1293373;
+ }
+
+ coord.x += vx * warpAmp;
+ coord.y += vy * warpAmp;
+ coord.z += vz * warpAmp;
+ }
+
+ public static class Vector2
+ {
+ public /*FNLfloat*/ double x;
+ public /*FNLfloat*/ double y;
+ public Vector2(/*FNLfloat*/ double x, /*FNLfloat*/ double y)
+ {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ public static class Vector3
+ {
+ public /*FNLfloat*/ double x;
+ public /*FNLfloat*/ double y;
+ public /*FNLfloat*/ double z;
+ public Vector3(/*FNLfloat*/ double x, /*FNLfloat*/ double y, /*FNLfloat*/ double z)
+ {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/credits.txt b/src/main/resources/assets/credits.txt
new file mode 100644
index 0000000..deb2af5
--- /dev/null
+++ b/src/main/resources/assets/credits.txt
@@ -0,0 +1,7 @@
+CodeStorm Team
+Leader:
+- Mai Thanh (@thnhmai06)
+Members:
+- Manh Tan (@ManhTanTran)
+- Anh Tuan (@huynhtuan372)
+Thank you for playing! <3
diff --git a/src/main/resources/assets/sounds/music/in_game.wav b/src/main/resources/assets/sounds/music/in_game.wav
new file mode 100644
index 0000000..916f273
Binary files /dev/null and b/src/main/resources/assets/sounds/music/in_game.wav differ
diff --git a/src/main/resources/assets/sounds/music/menu.ogg b/src/main/resources/assets/sounds/music/menu.ogg
new file mode 100644
index 0000000..d457631
Binary files /dev/null and b/src/main/resources/assets/sounds/music/menu.ogg differ
diff --git a/src/main/resources/assets/sounds/sfx/blip.wav b/src/main/resources/assets/sounds/sfx/blip.wav
new file mode 100644
index 0000000..9fdee0b
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/blip.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/brick-hit-1.wav b/src/main/resources/assets/sounds/sfx/brick-hit-1.wav
new file mode 100644
index 0000000..9ce943f
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/brick-hit-1.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/brick-hit-2.wav b/src/main/resources/assets/sounds/sfx/brick-hit-2.wav
new file mode 100644
index 0000000..ecdf4bb
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/brick-hit-2.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/buzz.wav b/src/main/resources/assets/sounds/sfx/buzz.wav
new file mode 100644
index 0000000..1978bfc
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/buzz.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/click_tiny.wav b/src/main/resources/assets/sounds/sfx/click_tiny.wav
new file mode 100644
index 0000000..2f7d044
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/click_tiny.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/confirm.wav b/src/main/resources/assets/sounds/sfx/confirm.wav
new file mode 100644
index 0000000..b0c5a71
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/confirm.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/electric.wav b/src/main/resources/assets/sounds/sfx/electric.wav
new file mode 100644
index 0000000..9e39104
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/electric.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/high_score.wav b/src/main/resources/assets/sounds/sfx/high_score.wav
new file mode 100644
index 0000000..9702f67
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/high_score.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/hurt.wav b/src/main/resources/assets/sounds/sfx/hurt.wav
new file mode 100644
index 0000000..78defe3
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/hurt.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/key_open.wav b/src/main/resources/assets/sounds/sfx/key_open.wav
new file mode 100644
index 0000000..4c3ffcb
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/key_open.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/lose.wav b/src/main/resources/assets/sounds/sfx/lose.wav
new file mode 100644
index 0000000..cc74e03
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/lose.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/no-select.wav b/src/main/resources/assets/sounds/sfx/no-select.wav
new file mode 100644
index 0000000..38bcf3e
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/no-select.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/paddle_hit.wav b/src/main/resources/assets/sounds/sfx/paddle_hit.wav
new file mode 100644
index 0000000..68483ba
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/paddle_hit.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/pause.wav b/src/main/resources/assets/sounds/sfx/pause.wav
new file mode 100644
index 0000000..1a99da0
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/pause.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/power_up.wav b/src/main/resources/assets/sounds/sfx/power_up.wav
new file mode 100644
index 0000000..ccbad18
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/power_up.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/recover.wav b/src/main/resources/assets/sounds/sfx/recover.wav
new file mode 100644
index 0000000..387db55
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/recover.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/score.wav b/src/main/resources/assets/sounds/sfx/score.wav
new file mode 100644
index 0000000..420d868
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/score.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/select.wav b/src/main/resources/assets/sounds/sfx/select.wav
new file mode 100644
index 0000000..ccfeda3
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/select.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/shrink.wav b/src/main/resources/assets/sounds/sfx/shrink.wav
new file mode 100644
index 0000000..ef11ed6
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/shrink.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/switch2.wav b/src/main/resources/assets/sounds/sfx/switch2.wav
new file mode 100644
index 0000000..c19e3d4
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/switch2.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/victory.wav b/src/main/resources/assets/sounds/sfx/victory.wav
new file mode 100644
index 0000000..11cbea7
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/victory.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/wall_hit.wav b/src/main/resources/assets/sounds/sfx/wall_hit.wav
new file mode 100644
index 0000000..b8eb985
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/wall_hit.wav differ
diff --git a/src/main/resources/assets/sounds/sfx/win3.wav b/src/main/resources/assets/sounds/sfx/win3.wav
new file mode 100644
index 0000000..e050c78
Binary files /dev/null and b/src/main/resources/assets/sounds/sfx/win3.wav differ
diff --git a/src/main/resources/assets/textures/ball/blue/Blue Ball.png b/src/main/resources/assets/textures/ball/blue/Blue Ball.png
new file mode 100644
index 0000000..6d33bdc
Binary files /dev/null and b/src/main/resources/assets/textures/ball/blue/Blue Ball.png differ
diff --git a/src/main/resources/assets/textures/ball/green/Green Ball.png b/src/main/resources/assets/textures/ball/green/Green Ball.png
new file mode 100644
index 0000000..2a75873
Binary files /dev/null and b/src/main/resources/assets/textures/ball/green/Green Ball.png differ
diff --git a/src/main/resources/assets/textures/ball/orange/Orange Ball.png b/src/main/resources/assets/textures/ball/orange/Orange Ball.png
new file mode 100644
index 0000000..c28e25a
Binary files /dev/null and b/src/main/resources/assets/textures/ball/orange/Orange Ball.png differ
diff --git a/src/main/resources/assets/textures/ball/pink/Pink Ball.png b/src/main/resources/assets/textures/ball/pink/Pink Ball.png
new file mode 100644
index 0000000..797a555
Binary files /dev/null and b/src/main/resources/assets/textures/ball/pink/Pink Ball.png differ
diff --git a/src/main/resources/assets/textures/ball/red/Red Ball.png b/src/main/resources/assets/textures/ball/red/Red Ball.png
new file mode 100644
index 0000000..4b40762
Binary files /dev/null and b/src/main/resources/assets/textures/ball/red/Red Ball.png differ
diff --git a/src/main/resources/assets/textures/ball/start ball/Start Ball.png b/src/main/resources/assets/textures/ball/start ball/Start Ball.png
new file mode 100644
index 0000000..be0e7c8
Binary files /dev/null and b/src/main/resources/assets/textures/ball/start ball/Start Ball.png differ
diff --git a/src/main/resources/assets/textures/ball/yellow/Yellow Ball.png b/src/main/resources/assets/textures/ball/yellow/Yellow Ball.png
new file mode 100644
index 0000000..fd3299d
Binary files /dev/null and b/src/main/resources/assets/textures/ball/yellow/Yellow Ball.png differ
diff --git a/src/main/resources/assets/textures/bricks/blue/explode.png b/src/main/resources/assets/textures/bricks/blue/explode.png
new file mode 100644
index 0000000..b7f7024
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/blue/explode.png differ
diff --git a/src/main/resources/assets/textures/bricks/blue/keybrick.png b/src/main/resources/assets/textures/bricks/blue/keybrick.png
new file mode 100644
index 0000000..d25f36e
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/blue/keybrick.png differ
diff --git a/src/main/resources/assets/textures/bricks/blue/normal.png b/src/main/resources/assets/textures/bricks/blue/normal.png
new file mode 100644
index 0000000..0840abd
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/blue/normal.png differ
diff --git a/src/main/resources/assets/textures/bricks/blue/shield.png b/src/main/resources/assets/textures/bricks/blue/shield.png
new file mode 100644
index 0000000..432eb10
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/blue/shield.png differ
diff --git a/src/main/resources/assets/textures/bricks/blue/strong.png b/src/main/resources/assets/textures/bricks/blue/strong.png
new file mode 100644
index 0000000..a8bf01d
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/blue/strong.png differ
diff --git a/src/main/resources/assets/textures/bricks/blue/strongFirstHit.png b/src/main/resources/assets/textures/bricks/blue/strongFirstHit.png
new file mode 100644
index 0000000..fd5ceb7
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/blue/strongFirstHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/blue/strongSecondHit.png b/src/main/resources/assets/textures/bricks/blue/strongSecondHit.png
new file mode 100644
index 0000000..174d81f
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/blue/strongSecondHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/blue/strongThirdHit.png b/src/main/resources/assets/textures/bricks/blue/strongThirdHit.png
new file mode 100644
index 0000000..7477f2d
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/blue/strongThirdHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/green/explode.png b/src/main/resources/assets/textures/bricks/green/explode.png
new file mode 100644
index 0000000..00108bc
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/green/explode.png differ
diff --git a/src/main/resources/assets/textures/bricks/green/keybrick.png b/src/main/resources/assets/textures/bricks/green/keybrick.png
new file mode 100644
index 0000000..9700512
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/green/keybrick.png differ
diff --git a/src/main/resources/assets/textures/bricks/green/normal.png b/src/main/resources/assets/textures/bricks/green/normal.png
new file mode 100644
index 0000000..85df743
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/green/normal.png differ
diff --git a/src/main/resources/assets/textures/bricks/green/shield.png b/src/main/resources/assets/textures/bricks/green/shield.png
new file mode 100644
index 0000000..557d354
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/green/shield.png differ
diff --git a/src/main/resources/assets/textures/bricks/green/strong.png b/src/main/resources/assets/textures/bricks/green/strong.png
new file mode 100644
index 0000000..a6774be
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/green/strong.png differ
diff --git a/src/main/resources/assets/textures/bricks/green/strongFirstHit.png b/src/main/resources/assets/textures/bricks/green/strongFirstHit.png
new file mode 100644
index 0000000..cbe4161
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/green/strongFirstHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/green/strongSecondHit.png b/src/main/resources/assets/textures/bricks/green/strongSecondHit.png
new file mode 100644
index 0000000..d179a35
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/green/strongSecondHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/green/strongThirdHit.png b/src/main/resources/assets/textures/bricks/green/strongThirdHit.png
new file mode 100644
index 0000000..3386dd2
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/green/strongThirdHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/orange/explode.png b/src/main/resources/assets/textures/bricks/orange/explode.png
new file mode 100644
index 0000000..4a48293
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/orange/explode.png differ
diff --git a/src/main/resources/assets/textures/bricks/orange/keybrick.png b/src/main/resources/assets/textures/bricks/orange/keybrick.png
new file mode 100644
index 0000000..0536665
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/orange/keybrick.png differ
diff --git a/src/main/resources/assets/textures/bricks/orange/normal.png b/src/main/resources/assets/textures/bricks/orange/normal.png
new file mode 100644
index 0000000..f1823f7
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/orange/normal.png differ
diff --git a/src/main/resources/assets/textures/bricks/orange/shield.png b/src/main/resources/assets/textures/bricks/orange/shield.png
new file mode 100644
index 0000000..58c91e2
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/orange/shield.png differ
diff --git a/src/main/resources/assets/textures/bricks/orange/strong.png b/src/main/resources/assets/textures/bricks/orange/strong.png
new file mode 100644
index 0000000..8c58ac7
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/orange/strong.png differ
diff --git a/src/main/resources/assets/textures/bricks/orange/strongFirstHit.png b/src/main/resources/assets/textures/bricks/orange/strongFirstHit.png
new file mode 100644
index 0000000..af4f120
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/orange/strongFirstHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/orange/strongSecondHit.png b/src/main/resources/assets/textures/bricks/orange/strongSecondHit.png
new file mode 100644
index 0000000..3d6c355
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/orange/strongSecondHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/orange/strongThirdHit.png b/src/main/resources/assets/textures/bricks/orange/strongThirdHit.png
new file mode 100644
index 0000000..4092668
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/orange/strongThirdHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/pink/explode.png b/src/main/resources/assets/textures/bricks/pink/explode.png
new file mode 100644
index 0000000..6c58af7
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/pink/explode.png differ
diff --git a/src/main/resources/assets/textures/bricks/pink/keybrick.png b/src/main/resources/assets/textures/bricks/pink/keybrick.png
new file mode 100644
index 0000000..f0078e9
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/pink/keybrick.png differ
diff --git a/src/main/resources/assets/textures/bricks/pink/normal.png b/src/main/resources/assets/textures/bricks/pink/normal.png
new file mode 100644
index 0000000..8ec0034
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/pink/normal.png differ
diff --git a/src/main/resources/assets/textures/bricks/pink/shield.png b/src/main/resources/assets/textures/bricks/pink/shield.png
new file mode 100644
index 0000000..690b632
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/pink/shield.png differ
diff --git a/src/main/resources/assets/textures/bricks/pink/strong.png b/src/main/resources/assets/textures/bricks/pink/strong.png
new file mode 100644
index 0000000..647c9a6
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/pink/strong.png differ
diff --git a/src/main/resources/assets/textures/bricks/pink/strongFirstHit.png b/src/main/resources/assets/textures/bricks/pink/strongFirstHit.png
new file mode 100644
index 0000000..4a7ca6b
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/pink/strongFirstHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/pink/strongSecondHit.png b/src/main/resources/assets/textures/bricks/pink/strongSecondHit.png
new file mode 100644
index 0000000..3a70a66
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/pink/strongSecondHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/pink/strongThirdHit.png b/src/main/resources/assets/textures/bricks/pink/strongThirdHit.png
new file mode 100644
index 0000000..9b1f0b5
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/pink/strongThirdHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/red/explode.png b/src/main/resources/assets/textures/bricks/red/explode.png
new file mode 100644
index 0000000..2442015
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/red/explode.png differ
diff --git a/src/main/resources/assets/textures/bricks/red/keybrick.png b/src/main/resources/assets/textures/bricks/red/keybrick.png
new file mode 100644
index 0000000..c59cc8e
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/red/keybrick.png differ
diff --git a/src/main/resources/assets/textures/bricks/red/normal.png b/src/main/resources/assets/textures/bricks/red/normal.png
new file mode 100644
index 0000000..137632c
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/red/normal.png differ
diff --git a/src/main/resources/assets/textures/bricks/red/shield.png b/src/main/resources/assets/textures/bricks/red/shield.png
new file mode 100644
index 0000000..83837ae
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/red/shield.png differ
diff --git a/src/main/resources/assets/textures/bricks/red/strong.png b/src/main/resources/assets/textures/bricks/red/strong.png
new file mode 100644
index 0000000..e00b227
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/red/strong.png differ
diff --git a/src/main/resources/assets/textures/bricks/red/strongFirstHit.png b/src/main/resources/assets/textures/bricks/red/strongFirstHit.png
new file mode 100644
index 0000000..8fd14ea
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/red/strongFirstHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/red/strongSecondHit.png b/src/main/resources/assets/textures/bricks/red/strongSecondHit.png
new file mode 100644
index 0000000..6ce8ebb
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/red/strongSecondHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/red/strongThirdHit.png b/src/main/resources/assets/textures/bricks/red/strongThirdHit.png
new file mode 100644
index 0000000..5fd3164
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/red/strongThirdHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/yellow/explode.png b/src/main/resources/assets/textures/bricks/yellow/explode.png
new file mode 100644
index 0000000..d141b47
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/yellow/explode.png differ
diff --git a/src/main/resources/assets/textures/bricks/yellow/keybrick.png b/src/main/resources/assets/textures/bricks/yellow/keybrick.png
new file mode 100644
index 0000000..e07fe93
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/yellow/keybrick.png differ
diff --git a/src/main/resources/assets/textures/bricks/yellow/normal.png b/src/main/resources/assets/textures/bricks/yellow/normal.png
new file mode 100644
index 0000000..5aeb023
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/yellow/normal.png differ
diff --git a/src/main/resources/assets/textures/bricks/yellow/shield.png b/src/main/resources/assets/textures/bricks/yellow/shield.png
new file mode 100644
index 0000000..6f6976d
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/yellow/shield.png differ
diff --git a/src/main/resources/assets/textures/bricks/yellow/strong.png b/src/main/resources/assets/textures/bricks/yellow/strong.png
new file mode 100644
index 0000000..b76bbd9
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/yellow/strong.png differ
diff --git a/src/main/resources/assets/textures/bricks/yellow/strongFirstHit.png b/src/main/resources/assets/textures/bricks/yellow/strongFirstHit.png
new file mode 100644
index 0000000..a372ff8
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/yellow/strongFirstHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/yellow/strongSecondHit.png b/src/main/resources/assets/textures/bricks/yellow/strongSecondHit.png
new file mode 100644
index 0000000..42cdb7d
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/yellow/strongSecondHit.png differ
diff --git a/src/main/resources/assets/textures/bricks/yellow/strongThirdHit.png b/src/main/resources/assets/textures/bricks/yellow/strongThirdHit.png
new file mode 100644
index 0000000..4528f77
Binary files /dev/null and b/src/main/resources/assets/textures/bricks/yellow/strongThirdHit.png differ
diff --git a/src/main/resources/assets/textures/heart.png b/src/main/resources/assets/textures/heart.png
new file mode 100644
index 0000000..461b398
Binary files /dev/null and b/src/main/resources/assets/textures/heart.png differ
diff --git a/src/main/resources/assets/textures/numbers/0.png b/src/main/resources/assets/textures/numbers/0.png
new file mode 100644
index 0000000..60d8e91
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/0.png differ
diff --git a/src/main/resources/assets/textures/numbers/1.png b/src/main/resources/assets/textures/numbers/1.png
new file mode 100644
index 0000000..23ab362
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/1.png differ
diff --git a/src/main/resources/assets/textures/numbers/2.png b/src/main/resources/assets/textures/numbers/2.png
new file mode 100644
index 0000000..db7a1ec
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/2.png differ
diff --git a/src/main/resources/assets/textures/numbers/3.png b/src/main/resources/assets/textures/numbers/3.png
new file mode 100644
index 0000000..a833c61
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/3.png differ
diff --git a/src/main/resources/assets/textures/numbers/4.png b/src/main/resources/assets/textures/numbers/4.png
new file mode 100644
index 0000000..c0579e0
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/4.png differ
diff --git a/src/main/resources/assets/textures/numbers/5.png b/src/main/resources/assets/textures/numbers/5.png
new file mode 100644
index 0000000..e983a21
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/5.png differ
diff --git a/src/main/resources/assets/textures/numbers/6.png b/src/main/resources/assets/textures/numbers/6.png
new file mode 100644
index 0000000..cbc1222
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/6.png differ
diff --git a/src/main/resources/assets/textures/numbers/7.png b/src/main/resources/assets/textures/numbers/7.png
new file mode 100644
index 0000000..3936b98
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/7.png differ
diff --git a/src/main/resources/assets/textures/numbers/8.png b/src/main/resources/assets/textures/numbers/8.png
new file mode 100644
index 0000000..ddd4039
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/8.png differ
diff --git a/src/main/resources/assets/textures/numbers/9.png b/src/main/resources/assets/textures/numbers/9.png
new file mode 100644
index 0000000..1f1a125
Binary files /dev/null and b/src/main/resources/assets/textures/numbers/9.png differ
diff --git a/src/main/resources/assets/textures/paddle/expand/blue/Blue Expand Paddle.png b/src/main/resources/assets/textures/paddle/expand/blue/Blue Expand Paddle.png
new file mode 100644
index 0000000..95c73d7
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/expand/blue/Blue Expand Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/expand/green/Green Expand Paddle.png b/src/main/resources/assets/textures/paddle/expand/green/Green Expand Paddle.png
new file mode 100644
index 0000000..b31181a
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/expand/green/Green Expand Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/expand/orange/Orange Expand Paddle.png b/src/main/resources/assets/textures/paddle/expand/orange/Orange Expand Paddle.png
new file mode 100644
index 0000000..734e461
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/expand/orange/Orange Expand Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/expand/pink/Pink Expand Paddle.png b/src/main/resources/assets/textures/paddle/expand/pink/Pink Expand Paddle.png
new file mode 100644
index 0000000..5d2b236
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/expand/pink/Pink Expand Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/expand/red/Red Expand Paddle.png b/src/main/resources/assets/textures/paddle/expand/red/Red Expand Paddle.png
new file mode 100644
index 0000000..747228a
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/expand/red/Red Expand Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/expand/yellow/Yellow Expand Paddle.png b/src/main/resources/assets/textures/paddle/expand/yellow/Yellow Expand Paddle.png
new file mode 100644
index 0000000..9cd4bb7
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/expand/yellow/Yellow Expand Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/normal/blue/Blue Paddle.png b/src/main/resources/assets/textures/paddle/normal/blue/Blue Paddle.png
new file mode 100644
index 0000000..fc5f96f
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/normal/blue/Blue Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/normal/green/Green Paddle.png b/src/main/resources/assets/textures/paddle/normal/green/Green Paddle.png
new file mode 100644
index 0000000..bd969ef
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/normal/green/Green Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/normal/orange/Orange Paddle.png b/src/main/resources/assets/textures/paddle/normal/orange/Orange Paddle.png
new file mode 100644
index 0000000..d941cff
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/normal/orange/Orange Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/normal/pink/Pink Paddle.png b/src/main/resources/assets/textures/paddle/normal/pink/Pink Paddle.png
new file mode 100644
index 0000000..866d779
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/normal/pink/Pink Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/normal/red/Red Paddle.png b/src/main/resources/assets/textures/paddle/normal/red/Red Paddle.png
new file mode 100644
index 0000000..8522ac3
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/normal/red/Red Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/normal/yellow/Yellow Paddle.png b/src/main/resources/assets/textures/paddle/normal/yellow/Yellow Paddle.png
new file mode 100644
index 0000000..86d691c
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/normal/yellow/Yellow Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/power/blue/Blue Power Paddle.png b/src/main/resources/assets/textures/paddle/power/blue/Blue Power Paddle.png
new file mode 100644
index 0000000..c5b0fef
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/power/blue/Blue Power Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/power/green/Green Power Paddle.png b/src/main/resources/assets/textures/paddle/power/green/Green Power Paddle.png
new file mode 100644
index 0000000..a057702
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/power/green/Green Power Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/power/orange/Orange Power Paddle.png b/src/main/resources/assets/textures/paddle/power/orange/Orange Power Paddle.png
new file mode 100644
index 0000000..48a66ec
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/power/orange/Orange Power Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/power/pink/Pink Power Paddle.png b/src/main/resources/assets/textures/paddle/power/pink/Pink Power Paddle.png
new file mode 100644
index 0000000..cf61c0d
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/power/pink/Pink Power Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/power/red/Red Power Paddle.png b/src/main/resources/assets/textures/paddle/power/red/Red Power Paddle.png
new file mode 100644
index 0000000..ef71ff5
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/power/red/Red Power Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/power/yellow/Yellow Power Paddle.png b/src/main/resources/assets/textures/paddle/power/yellow/Yellow Power Paddle.png
new file mode 100644
index 0000000..c265202
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/power/yellow/Yellow Power Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/shrink/blue/Blue Shrink Paddle.png b/src/main/resources/assets/textures/paddle/shrink/blue/Blue Shrink Paddle.png
new file mode 100644
index 0000000..063a65f
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/shrink/blue/Blue Shrink Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/shrink/green/Green Shrink Paddle.png b/src/main/resources/assets/textures/paddle/shrink/green/Green Shrink Paddle.png
new file mode 100644
index 0000000..7ae9ade
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/shrink/green/Green Shrink Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/shrink/orange/Orange Shrink Paddle.png b/src/main/resources/assets/textures/paddle/shrink/orange/Orange Shrink Paddle.png
new file mode 100644
index 0000000..e62d490
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/shrink/orange/Orange Shrink Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/shrink/pink/Pink Shrink Paddle.png b/src/main/resources/assets/textures/paddle/shrink/pink/Pink Shrink Paddle.png
new file mode 100644
index 0000000..26fe6ef
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/shrink/pink/Pink Shrink Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/shrink/red/Red Shrink Paddle.png b/src/main/resources/assets/textures/paddle/shrink/red/Red Shrink Paddle.png
new file mode 100644
index 0000000..efe1ccd
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/shrink/red/Red Shrink Paddle.png differ
diff --git a/src/main/resources/assets/textures/paddle/shrink/yellow/Yellow Shrink Paddle.png b/src/main/resources/assets/textures/paddle/shrink/yellow/Yellow Shrink Paddle.png
new file mode 100644
index 0000000..3db0b24
Binary files /dev/null and b/src/main/resources/assets/textures/paddle/shrink/yellow/Yellow Shrink Paddle.png differ
diff --git a/src/main/resources/assets/textures/power/ball/Fast Ball.png b/src/main/resources/assets/textures/power/ball/Fast Ball.png
new file mode 100644
index 0000000..c560b8f
Binary files /dev/null and b/src/main/resources/assets/textures/power/ball/Fast Ball.png differ
diff --git a/src/main/resources/assets/textures/power/ball/Slow Ball.png b/src/main/resources/assets/textures/power/ball/Slow Ball.png
new file mode 100644
index 0000000..7941ae0
Binary files /dev/null and b/src/main/resources/assets/textures/power/ball/Slow Ball.png differ
diff --git a/src/main/resources/assets/textures/power/ball/x2 Ball.png b/src/main/resources/assets/textures/power/ball/x2 Ball.png
new file mode 100644
index 0000000..697979c
Binary files /dev/null and b/src/main/resources/assets/textures/power/ball/x2 Ball.png differ
diff --git a/src/main/resources/assets/textures/power/misc/extra_life.png b/src/main/resources/assets/textures/power/misc/extra_life.png
new file mode 100644
index 0000000..a83a3d5
Binary files /dev/null and b/src/main/resources/assets/textures/power/misc/extra_life.png differ
diff --git a/src/main/resources/assets/textures/power/misc/score_boost.png b/src/main/resources/assets/textures/power/misc/score_boost.png
new file mode 100644
index 0000000..1e7f0d2
Binary files /dev/null and b/src/main/resources/assets/textures/power/misc/score_boost.png differ
diff --git a/src/main/resources/assets/textures/power/misc/shield.png b/src/main/resources/assets/textures/power/misc/shield.png
new file mode 100644
index 0000000..fbc95a0
Binary files /dev/null and b/src/main/resources/assets/textures/power/misc/shield.png differ
diff --git a/src/main/resources/assets/textures/power/paddle/Expand Paddle.png b/src/main/resources/assets/textures/power/paddle/Expand Paddle.png
new file mode 100644
index 0000000..09bb999
Binary files /dev/null and b/src/main/resources/assets/textures/power/paddle/Expand Paddle.png differ
diff --git a/src/main/resources/assets/textures/power/paddle/Gun.png b/src/main/resources/assets/textures/power/paddle/Gun.png
new file mode 100644
index 0000000..7c817db
Binary files /dev/null and b/src/main/resources/assets/textures/power/paddle/Gun.png differ
diff --git a/src/main/resources/assets/textures/power/paddle/Reverse Paddle.png b/src/main/resources/assets/textures/power/paddle/Reverse Paddle.png
new file mode 100644
index 0000000..f0be531
Binary files /dev/null and b/src/main/resources/assets/textures/power/paddle/Reverse Paddle.png differ
diff --git a/src/main/resources/assets/textures/power/paddle/Shrink Paddle.png b/src/main/resources/assets/textures/power/paddle/Shrink Paddle.png
new file mode 100644
index 0000000..1494b09
Binary files /dev/null and b/src/main/resources/assets/textures/power/paddle/Shrink Paddle.png differ
diff --git a/src/main/resources/assets/ui/powerup.css b/src/main/resources/assets/ui/powerup.css
new file mode 100644
index 0000000..83bd554
--- /dev/null
+++ b/src/main/resources/assets/ui/powerup.css
@@ -0,0 +1,6 @@
+.powerup-text {
+ -fx-fill: #F1C40F;
+ -fx-font-weight: bold;
+ -fx-font-size: 18px;
+ -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.7), 5, 0, 0, 1);
+}
diff --git a/src/main/resources/assets/videos/intro.mp4 b/src/main/resources/assets/videos/intro.mp4
new file mode 100644
index 0000000..75669b8
Binary files /dev/null and b/src/main/resources/assets/videos/intro.mp4 differ
diff --git a/src/main/resources/credits.txt b/src/main/resources/credits.txt
deleted file mode 100644
index 2c67d4f..0000000
--- a/src/main/resources/credits.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-CodeStorm
-
-Leader:
-Mai Thành (@thnhmai06)
-Members:
-Mạnh Tân (@ManhTanTran)
-Minh Ngọc (@minngoc1213)
-Anh Tuấn (@huynhtuan372)
-
--o0o-
-
-Thank you for playing! <3
\ No newline at end of file
diff --git a/src/main/resources/settings.properties b/src/main/resources/settings.properties
index 7c4b419..22ff206 100644
--- a/src/main/resources/settings.properties
+++ b/src/main/resources/settings.properties
@@ -3,6 +3,10 @@
#
# General
general.name=Bounceverse
-general.version=1.0.0
+general.version=1.1.0
general.devMode=true
+# Video
+video.width=1280
+video.height=720
+
diff --git a/system/Readme.txt b/system/Readme.txt
new file mode 100644
index 0000000..6578c70
--- /dev/null
+++ b/system/Readme.txt
@@ -0,0 +1 @@
+This directory contains FXGL system data files.
\ No newline at end of file
diff --git a/system/fxgl.bundle b/system/fxgl.bundle
new file mode 100644
index 0000000..a8ad53e
Binary files /dev/null and b/system/fxgl.bundle differ
|