diff --git a/.editorconfig b/.editorconfig index c13f780..179b03d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,17 +12,21 @@ trim_trailing_whitespace = true # 2-space indentation (Google Style Guide Section 4.2) indent_style = space -# Limit line length to 100 characters (Section 4.4) -max_line_length = 100 +# Even though Google Style Guide Section 2.3 recommends 100 characters, +# many modern IDEs default to 120 characters, so we follow that convention. +max_line_length = 120 -# Standard Java formatting rules +# Standard Java formatting rules. ij_java_use_single_class_imports = true ij_java_insert_inner_class_imports = false ij_java_class_count_to_use_import_on_demand = 999 ij_java_names_count_to_use_import_on_demand = 999 ij_java_packages_to_use_import_on_demand = unset -ij_java_blank_lines_before_class_end = 1 +ij_java_blank_lines_before_class_end = 0 ij_java_blank_lines_after_class_header = 1 +ij_java_doc_align_param_comments = false +ij_java_doc_do_not_wrap_if_one_line = true +ij_any_keep_simple_blocks_in_one_line = true # Block formatting (Section 4.1.2) ij_java_class_brace_style = end_of_line diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a5c556e..eafd405 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -3,7 +3,7 @@ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username -ko_fi: stringfromjava +ko_fi: stringdotjar tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 51db3bd..005304b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,25 +13,48 @@ jobs: os: [windows-latest, macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} - + steps: - uses: actions/checkout@v4 - - # LibGDX typically uses Java 11 or newer + + # LibGDX typically uses Java 11 or newer. - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - # Cache Gradle dependencies to speed up subsequent runs + # Cache Gradle dependencies to speed up subsequent runs. cache: 'gradle' - # Grant execute permission for gradlew (necessary for macOS and Linux) + # Grant execute permission for gradlew (necessary for macOS and Linux). - name: Grant execute permission for gradlew run: chmod +x gradlew - # Use an 'if' condition to only run this on non-Windows OS + # Use an 'if' condition to only run this on non-Windows OS. if: startsWith(runner.os, 'macOS') || startsWith(runner.os, 'Linux') - - # This task compiles all modules (core, desktop, etc.) + + # This task compiles all modules (core, desktop, etc.). - name: Build with Gradle run: ./gradlew build + + build-android: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + # LibGDX typically uses Java 11 or newer. + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + # Cache Gradle dependencies to speed up subsequent runs. + cache: 'gradle' + + # Grant execute permission for gradlew. + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Build Android module. + - name: Build Android with Gradle + run: ./gradlew android:build diff --git a/.gitignore b/.gitignore index 159c8cd..2096da3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,10 @@ .gradle/ gradle-app.setting /build/ +/funkin/build/ +/flixelgdx/build/ +/polyverse/build/ /android/build/ -/core/build/ -/lwjgl2/build/ /lwjgl3/build/ /html/build/ /teavm/build/ @@ -155,8 +156,8 @@ Thumbs.db *~ *.*# *#*# -/.kotlin/ /assets/assets.txt +/assets/sdk/ ## Special cases: diff --git a/COMPILING.md b/COMPILING.md index 7c5f7be..465f4d7 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -1,7 +1,7 @@ # How to Compile FNF:JE > [!TIP] -> If you want some helpful info about the Gradle tasks (or the framework FNF:JE uses), consider taking a look at [LIBGDX.md](LIBGDX.md)! +> If you want some helpful info about the Gradle tasks (or the framework FNF:JE uses), consider taking a look at [LIBGDX.md](PROJECT.md)! There are two main ways you can download and compile the game's code: with GitHub Desktop or the terminal. @@ -12,13 +12,13 @@ In this guide, we'll use the GitHub Desktop method, since it the most user-frien - A [Java Development Kit (JDK)](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) to compile the game's code. - An integrated development environment. - We recommend either one of the options listed below: - - [Intellij IDEA](https://www.jetbrains.com/idea/download/) + - [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) - [Eclipse](https://www.eclipse.org/downloads/) - [VS Code](https://code.visualstudio.com/) - Some basic knowledge of programming (especially Gradle) and how to navigate an IDE. > [!TIP] -> Although every IDE listed is great for Java, we STRONGLY recommend Intellij IDEA, due to how beginner friendly and integrated it is with FNF:JE! +> Although every IDE listed is great for Java, we STRONGLY recommend IntelliJ IDEA, due to how beginner-friendly and integrated it is with FNF:JE! # Step-by-Step Guide diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8df22cc..a67cc45 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ hard to understand in general, then they won't be any use. Even too many comments are unnecessary, since your code should be self documented and easily readable. -***Formatting MUST match the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html).*** +***Formatting MUST match the configurations set in the [editor config file](.editorconfig), with the formatting based on the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html).*** ### Example of GOOD Comments (With Good Formatting) diff --git a/LIBGDX.md b/PROJECT.md similarity index 80% rename from LIBGDX.md rename to PROJECT.md index d0e041a..6681816 100644 --- a/LIBGDX.md +++ b/PROJECT.md @@ -4,8 +4,10 @@ FNF:JE is designed to run on multiple different platforms. Below are the different modules that hold the code for each one. -- `core`: Main module with the game's logic shared by all platforms. This is where you want to write your source code! -- `lwjgl3`: Primary desktop platform using [LWJGL3](https://www.lwjgl.org/). This is what launches the desktop versions of the game! +- `funkin`: The core part of the game's code. This is where all the game logic is implemented. +- `flixelgdx`: Custom framework that bridges [HaxeFlixel](https://haxeflixel.com/) and is based on libGDX. This is where the HaxeFlixel-like API is implemented. +- `polyverse`: Custom library for adding modding capabilities to the game. +- `lwjgl3`: Primary desktop platform using [LWJGL3](https://www.lwjgl.org/). This is what launches the desktop versions of the game! - `android`: Android mobile platform. This requires the Android SDK, which can be downloaded and configured simply by running the universal [setup file](setup/android_setup.sh)! > [!IMPORTANT] @@ -32,6 +34,7 @@ The Gradle wrapper was included, so you can run Gradle tasks using `gradlew.bat` - `clean`: removes `build` folders, which store compiled classes and built archives. - `eclipse`: generates Eclipse project data. - `idea`: generates IntelliJ project data. +- `funkin:exportModSDK`: Exports the game's API and its dependencies as `.jar` files to the assets folder. - `lwjgl3:jar`: builds the game's runnable jar, which can be found at `lwjgl3/build/libs`. - `lwjgl3:run`: starts the desktop version of the game. - `test`: runs unit tests (if any). diff --git a/README.md b/README.md index f72641c..cdb21a4 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,21 @@ Welcome to the official GitHub repository for the Java edition of Friday Night Funkin'! -### Please note that this unofficial version of the game is NOT completed! This has a long way to go, but with YOUR help, we can make this project come alive! <3 +> [!NOTE] +> This is a fan-made project and is not affiliated with the original developers. You can play the original game [here](https://ninja-muffin24.itch.io/funkin). + +## Please note that this unofficial version of the game is NOT completed! This has a long way to go, but with YOUR help, we can make this project come alive! <3 + +# About the Project +Friday Night Funkin': Java Edition is an open-source project that aims to recreate the popular rhythm game [Friday Night Funkin'](https://github.com/FunkinCrew/Funkin) using Java. +It is built using its own custom framework based on libGDX, called FlixelGDX: a Java port of the HaxeFlixel framework used in the original game. + +The project is designed to have practically endless modding capabilities, empowering developers to use features for mods +which otherwise would be impossible to accomplish in the original game and HaxeFlixel. + +# Project Navigation + +- [Contributing](CONTRIBUTING.md) +- [Project Structure and Gradle Tasks](PROJECT.md) +- [Compiling the Game](COMPILING.md) +- [License](LICENSE.md) diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index f1a19ce..2bddab6 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -12,7 +12,7 @@ tools:ignore="UnusedAttribute" android:theme="@style/GdxTheme"> @@ -119,4 +119,4 @@ Created with Funkin Packer v0.1.3 https://neeeoo.github.io/funkin-packer/ - \ No newline at end of file + diff --git a/assets/shared/images/characters/Pico_Death.xml b/assets/shared/images/characters/Pico_Death.xml index 152c157..6a980c0 100644 --- a/assets/shared/images/characters/Pico_Death.xml +++ b/assets/shared/images/characters/Pico_Death.xml @@ -1,6 +1,6 @@ @@ -67,4 +67,4 @@ Created with Funkin Packer v0.1.3 https://neeeoo.github.io/funkin-packer/ - \ No newline at end of file + diff --git a/assets/test.groovy b/assets/test.groovy index a3adf17..0a291d2 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -1,12 +1,11 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.graphics.Texture -import com.badlogic.gdx.graphics.g2d.Sprite -import me.stringfromjava.funkin.Funkin -import me.stringfromjava.funkin.backend.display.FunkinScreen -import me.stringfromjava.funkin.backend.system.Paths -import me.stringfromjava.funkin.polyverse.script.type.SystemScript +import me.stringdotjar.flixelgdx.Flixel +import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen +import me.stringdotjar.flixelgdx.backend.FlixelPaths +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite +import me.stringdotjar.polyverse.script.type.SystemScript class TestScript extends SystemScript { @@ -17,7 +16,7 @@ class TestScript extends SystemScript { @Override void onCreate() { super.onCreate() - Funkin.info("TestScript", "Script has been created!") + Flixel.info("TestScript", "Script has been created!") } @Override @@ -25,31 +24,31 @@ class TestScript extends SystemScript { super.onRender(delta) if (Gdx.input.isKeyJustPressed(Input.Keys.Q)) { - Funkin.setScreen(new TestScreen()) + Flixel.setScreen(new TestScreen()) } } @Override void onDispose() { super.onDispose() - Funkin.info("TestClass", "Script has been disposed!") + Flixel.info("TestClass", "Script has been disposed!") } } -class TestScreen extends FunkinScreen { +class TestScreen extends FlixelScreen { - private Sprite test + private FlixelSprite test @Override void show() { super.show() - test = new Sprite(new Texture(Paths.image('NOTE_hold_assets'))) + test = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset('NOTE_hold_assets')) add(test) bgColor = new Color(0, 1, 0, 1) - Funkin.playMusic('songs/guns/Inst.ogg') + Flixel.playMusic('songs/darnell/Inst.ogg') } @Override diff --git a/build.gradle b/build.gradle index 199446c..a90df70 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,6 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:8.7.3" - } } diff --git a/core/src/main/java/me/stringfromjava/funkin/Funkin.java b/core/src/main/java/me/stringfromjava/funkin/Funkin.java deleted file mode 100644 index 8290389..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/Funkin.java +++ /dev/null @@ -1,258 +0,0 @@ -package me.stringfromjava.funkin; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Music; -import com.badlogic.gdx.audio.Sound; -import me.stringfromjava.funkin.audio.FunkinSound; -import me.stringfromjava.funkin.backend.display.FunkinScreen; -import me.stringfromjava.funkin.backend.system.FunkinSignal; -import me.stringfromjava.funkin.backend.system.Paths; -import me.stringfromjava.funkin.util.Constants; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; - -/** - * Global manager and utility class for the game. - * - *

This is where you want to do the main things, like switching screens, playing sounds/music, - * etc. - */ -public final class Funkin { - - /** - * The current {@code FunkinScreen} being displayed, stored in a static instance for global - * access. - * - *

Use this instead of {@code Funkin.game.getScreen()} for actually accessing all the custom - * functions and attributes that a regular {@code Screen} doesn't have! - */ - public static FunkinScreen screen = null; - - /** - * A map containing all sounds that are currently playing. - * - *

The key is the sound's ID (created by libGDX), and the value is the sound itself. Note that - * it's not recommended to access this unless you know what you're doing! - */ - public static Map soundPool = new HashMap<>(); - - /** The object where the current music being played is stored. */ - public static Music music = null; - - /** The global volume multiplier for all sounds and music. */ - public static float masterVolume = 1.0f; - - /** - * The static instance used to access the core elements of the game. This includes the loop, - * setting the current screen, and more. - */ - private static FunkinGame game; - - /** Has the global manager been initialized yet? */ - private static boolean initialized = false; - - /** - * Initializes the global manager. - * - *

This can only be called once. If attempted to be executed again, the game will throw an - * exception. - * - * @param gameInstance The instance of the game to use. - */ - public static void initialize(FunkinGame gameInstance) { - if (initialized) { - throw new IllegalStateException("FNF:JE has already been initialized!"); - } - game = gameInstance; - initialized = true; - } - - /** - * Sets the current screen to the provided screen. This is just a more direct version of {@code - * Funkin.game.setScreen(screen)} with some extra functionality put into it. - * - * @param screen The new {@code FunkinScreen} to set as the current screen. - */ - public static void setScreen(FunkinScreen screen) { - Signals.preScreenSwitch.dispatch(new Signals.ScreenSwitchSignalData(screen)); - if (!initialized) { - throw new IllegalStateException("FNF:JE has not been initialized yet!"); - } - if (screen == null) { - throw new IllegalArgumentException("Screen cannot be null!"); - } - if (Funkin.screen != null) { - Funkin.screen.hide(); - Funkin.screen.dispose(); - } - Funkin.screen = screen; - game.setScreen(screen); - Signals.postScreenSwitch.dispatch(new Signals.ScreenSwitchSignalData(screen)); - } - - /** - * Plays a sound. (Duh.) - * - * @param path The path to play the sound from. - * @return The sound instance itself, as a {@link FunkinSound}. - */ - public static FunkinSound playSound(String path) { - FunkinSound sound = new FunkinSound(path); - if (sound.ID != -1) { // libGDX will return -1 if the sound fails to play. - soundPool.put(sound.ID, sound); - } - sound.play(); - return sound; - } - - /** - * Plays new music. (Duh.) - * - * @param path The path to play the music from. - * @return The music instance itself. - */ - public static Music playMusic(String path) { - return playMusic(path, 1.0f, true); - } - - /** - * Plays new music. (Duh.) - * - * @param path The path to play the music from. - * @param volume The volume to play the music at. - * @return The music instance itself. - */ - public static Music playMusic(String path, float volume) { - return playMusic(path, volume, true); - } - - /** - * Plays new music. (Duh.) - * - * @param path The path to play the music from. - * @param volume The volume to play the music at. - * @param looped Should the music loop when it is finished playing? - * @return The music instance itself. - */ - public static Music playMusic(String path, float volume, boolean looped) { - Music music = Gdx.audio.newMusic(Paths.asset(path)); - if (Funkin.music != null && Funkin.music.isPlaying()) { - Funkin.music.stop(); - } - Funkin.music = music; - music.setVolume(volume); - music.setLooping(looped); - music.play(); - return music; - } - - public static void info(Object message) { - info("Funkin", message); - } - - public static void info(String tag, Object message) { - outputLog(tag, message, FunkinLogLevel.INFO); - } - - public static void warn(Object message) { - warn("Funkin", message); - } - - public static void warn(String tag, Object message) { - outputLog(tag, message, FunkinLogLevel.WARN); - } - - public static void error(String message) { - error("Funkin", message); - } - - public static void error(String tag, Object message) { - outputLog(tag, message, FunkinLogLevel.ERROR); - } - - public static FunkinGame getGame() { - return game; - } - - // ====================================== - // UTILITY FUNCTIONS, IGNORE BELOW - // ====================================== - - private static void outputLog(String tag, Object message, FunkinLogLevel level) { - String color = switch (level) { - case INFO -> Constants.Colors.WHITE; - case WARN -> Constants.Colors.YELLOW; - case ERROR -> Constants.Colors.RED; - }; - - boolean underline = (level == FunkinLogLevel.ERROR); - String timeAndDate = colorText( - LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " ", - color, true, false, underline); - String formattedTag = colorText("[" + tag + "] [" + level + "] ", color, true, false, underline); - String formattedMessage = colorText(message.toString(), color, false, true, underline); - - System.out.println(timeAndDate + formattedTag + formattedMessage); - } - - - private static String colorText( - String text, String color, boolean bold, boolean italic, boolean underline) { - StringBuilder sb = new StringBuilder(); - if (bold) { - sb.append(Constants.Colors.BOLD); - } - if (italic) { - sb.append(Constants.Colors.ITALIC); - } - if (underline) { - sb.append(Constants.Colors.UNDERLINE); - } - sb.append(color); - sb.append(text); - sb.append(Constants.Colors.RESET); - return sb.toString(); - } - - /** - * Contains all the global events that get dispatched when something happens in the game. - * - *

This includes anything from the screen being switched, the game updating every frame, and - * just about everything you can think of. - * - *

IMPORTANT DETAIL!: Anything with the {@code pre} and {@code post} prefixes always mean the - * same thing. If a signal has {@code pre}, then the signal gets ran BEFORE any functionality is - * executed, and {@code post} means AFTER all functionality was executed. - */ - public static class Signals { - - public static final FunkinSignal preRender = new FunkinSignal<>(); - public static final FunkinSignal postRender = new FunkinSignal<>(); - public static final FunkinSignal preScreenSwitch = new FunkinSignal<>(); - public static final FunkinSignal postScreenSwitch = - new FunkinSignal<>(); - public static final FunkinSignal preGameClose = new FunkinSignal<>(); - public static final FunkinSignal postGameClose = new FunkinSignal<>(); - public static final FunkinSignal preSoundPlayed = new FunkinSignal<>(); - public static final FunkinSignal postSoundPlayed = new FunkinSignal<>(); - - public record RenderSignalData(float delta) {} - - public record ScreenSwitchSignalData(FunkinScreen screen) {} - - public record SoundPlayedSignalData(FunkinSound sound) {} - - private Signals() {} - } - - private Funkin() {} -} - -enum FunkinLogLevel { - INFO, - WARN, - ERROR -} diff --git a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java b/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java deleted file mode 100644 index 0cac92f..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/FunkinGame.java +++ /dev/null @@ -1,123 +0,0 @@ -package me.stringfromjava.funkin; - -import com.badlogic.gdx.Game; -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Sound; -import me.stringfromjava.funkin.backend.system.Paths; -import me.stringfromjava.funkin.game.InitScreen; -import me.stringfromjava.funkin.polyverse.Polyverse; -import me.stringfromjava.funkin.polyverse.script.type.Script; -import me.stringfromjava.funkin.polyverse.script.type.SystemScript; -import me.stringfromjava.funkin.tween.FunkinTween; - -import java.util.Set; - -import static me.stringfromjava.funkin.Funkin.Signals.RenderSignalData; - -/** - * An enhanced version of libGDX's {@link Game} object. - * - *

If you want to change what happens to the pre and window configurations, you might want to see - * {@code Lwjgl3Launcher} in the {@code lwjgl3} folder. - */ -public class FunkinGame extends Game { - - /** Is the game's window currently minimized? */ - protected boolean isMinimized = false; - - @Override - public void create() { - configurePolyverse(); - - setScreen(new InitScreen()); - } - - @Override - public void render() { - super.render(); - float delta = Gdx.graphics.getDeltaTime(); - - Funkin.Signals.preRender.dispatch(new RenderSignalData(delta)); - - FunkinTween.globalManager.update(delta); - Polyverse.forAllScripts(script -> script.onRender(delta)); - - Funkin.Signals.postRender.dispatch(new RenderSignalData(delta)); - } - - /** Called when the user regains focus on the game's window. */ - public void onWindowFocused() { - Funkin.masterVolume = 1.0f; - Funkin.music.setVolume(1); - Funkin.info("Game window has regained focus."); - } - - /** Called when the user loses focus on the game's window, while also not being minimized. */ - public void onWindowUnfocused() { - if (isMinimized) { - return; - } - Funkin.masterVolume = 0.008f; - Funkin.music.setVolume(0.008f); - Funkin.info("Game window has lost focus."); - } - - /** - * Called when the user minimizes the game's window. - * - * @param iconified Whether the window is iconified (minimized) or not. This parameter is provided - * for compatibility with the window listener in the LWJGL3 (desktop) launcher. - */ - public void onWindowMinimized(boolean iconified) { - isMinimized = iconified; - if (!isMinimized) { - return; - } - Funkin.masterVolume = 0.0f; - Funkin.music.setVolume(0); - Funkin.info("Game window has been minimized."); - } - - @Override - public void dispose() { - Funkin.warn("SHUTTING DOWN GAME AND DISPOSING ALL RESOURCES."); - - Funkin.Signals.preGameClose.dispatch(); - - // Dispose of all sounds and the music (if there is any playing). - Funkin.info("Disposing music..."); - if (Funkin.music != null) { - Funkin.music.stop(); - Funkin.music.dispose(); - } - - Funkin.info("Disposing sound pool..."); - Set soundPoolKeys = Funkin.soundPool.keySet(); - for (long key : soundPoolKeys) { - Sound sound = Funkin.soundPool.get(key); - if (sound == null) { - continue; - } - sound.stop(); - sound.dispose(); - } - - Funkin.info("Disposing and shutting down scripts..."); - Polyverse.forAllScripts(Script::onDispose); - - Funkin.Signals.postGameClose.dispatch(); - } - - public boolean isMinimized() { - return isMinimized; - } - - private void configurePolyverse() { - // Register Polyverse script types. - Polyverse.registerScriptType(Script.class); // Master type, DO NOT REMOVE THIS! - Polyverse.registerScriptType(SystemScript.class); - - Polyverse.registerScript(Paths.asset("test.groovy")); - Polyverse.registerScript(Paths.asset("another_test.groovy")); - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java b/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java deleted file mode 100644 index 77c199e..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/audio/FunkinSound.java +++ /dev/null @@ -1,189 +0,0 @@ -package me.stringfromjava.funkin.audio; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.audio.Sound; -import com.badlogic.gdx.files.FileHandle; -import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.backend.system.Paths; - -/** - * An enhanced version of libGDX's {@link Sound}. - * - *

This is mostly for ensuring that a sound's volume changes when the users' global volume - * changes. - */ -public class FunkinSound implements Sound { - - /** The ID of {@code this} specific sound. */ - public final long ID; - - private Sound thisSound; - private float volume; - private float pitch; - private float pan; - private boolean looping; - private boolean isPaused; - - public FunkinSound(String path) { - this(Paths.asset(path)); - } - - public FunkinSound(FileHandle path) { - thisSound = Gdx.audio.newSound(path); - ID = thisSound.play(); - volume = 1.0f; - pitch = 1.0f; - pan = 0.0f; - looping = false; - isPaused = false; - thisSound.stop(); - Funkin.soundPool.put(ID, thisSound); - } - - @Override - public long play() { - return thisSound.play(volume, pitch, pan); - } - - @Override - public long play(float volume) { - return play(volume, 1.0f, 0.0f); - } - - public long play(float volume, float pitch) { - return play(volume, pitch, 0.0f); - } - - @Override - public long play(float volume, float pitch, float pan) { - Funkin.Signals.preSoundPlayed.dispatch(new Funkin.Signals.SoundPlayedSignalData(this)); - this.volume = volume * Funkin.masterVolume; - this.pitch = pitch; - this.pan = pan; - this.looping = false; - this.isPaused = false; - Funkin.Signals.postSoundPlayed.dispatch(new Funkin.Signals.SoundPlayedSignalData(this)); - return thisSound.play(volume, pitch, pan); - } - - @Override - public long loop() { - return loop(1.0f, 1.0f, 0.0f); - } - - @Override - public long loop(float volume) { - return loop(volume, 1.0f, 0.0f); - } - - public long loop(float volume, float pitch) { - return loop(volume, pitch, 0.0f); - } - - @Override - public long loop(float volume, float pitch, float pan) { - Funkin.Signals.preSoundPlayed.dispatch(new Funkin.Signals.SoundPlayedSignalData(this)); - this.volume = volume * Funkin.masterVolume; - this.pitch = pitch; - this.pan = pan; - this.looping = true; - this.isPaused = false; - Funkin.Signals.postSoundPlayed.dispatch(new Funkin.Signals.SoundPlayedSignalData(this)); - return thisSound.loop(volume, pitch, pan); - } - - @Override - public void stop() { - isPaused = false; - thisSound.stop(); - } - - @Override - public void pause() { - isPaused = true; - thisSound.pause(); - } - - @Override - public void resume() { - isPaused = false; - thisSound.resume(); - } - - @Override - public void dispose() { - volume = 1.0f; - pitch = 1.0f; - pan = 0.0f; - looping = false; - isPaused = false; - thisSound.dispose(); - Funkin.soundPool.remove(ID); - } - - @Override - public void stop(long soundId) { - isPaused = false; - thisSound.stop(); - } - - @Override - public void pause(long soundId) { - isPaused = true; - thisSound.pause(soundId); - } - - @Override - public void resume(long soundId) { - isPaused = false; - thisSound.resume(); - } - - @Override - public void setLooping(long soundId, boolean looping) { - this.looping = looping; - thisSound.setLooping(soundId, looping); - } - - @Override - public void setPitch(long soundId, float pitch) { - this.pitch = pitch; - thisSound.setPitch(soundId, pitch); - } - - @Override - public void setVolume(long soundId, float volume) { - float v = volume * Funkin.masterVolume; - this.volume = v; - thisSound.setVolume(soundId, v); - } - - public void setPan(long soundId, float pan) { - setPan(soundId, pan, 1.0f); - } - - @Override - public void setPan(long soundId, float pan, float volume) { - thisSound.setPan(soundId, pan, volume); - } - - public float getVolume() { - return volume; - } - - public float getPitch() { - return pitch; - } - - public float getPan() { - return pan; - } - - public boolean isLooping() { - return looping; - } - - public boolean isPaused() { - return isPaused; - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/Reflect.java b/core/src/main/java/me/stringfromjava/funkin/backend/Reflect.java deleted file mode 100644 index cd4ff4f..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/backend/Reflect.java +++ /dev/null @@ -1,18 +0,0 @@ -package me.stringfromjava.funkin.backend; - -import java.lang.reflect.Modifier; - -public class Reflect { - - public static boolean isClassFinal(String classPath) { - try { - Class clazz = Class.forName(classPath); - // Uses the java.lang.reflect.Modifier utility - return Modifier.isFinal(clazz.getModifiers()); - } catch (ClassNotFoundException e) { - // Treat non-existent class as non-final for safe binding, - // though the user will hit an error later. - return false; - } - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/display/FunkinScreen.java b/core/src/main/java/me/stringfromjava/funkin/backend/display/FunkinScreen.java deleted file mode 100644 index 3204807..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/backend/display/FunkinScreen.java +++ /dev/null @@ -1,100 +0,0 @@ -package me.stringfromjava.funkin.backend.display; - -import com.badlogic.gdx.Screen; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.OrthographicCamera; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.Sprite; -import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.utils.ScreenUtils; -import com.badlogic.gdx.utils.viewport.FitViewport; -import com.badlogic.gdx.utils.viewport.Viewport; -import me.stringfromjava.funkin.util.Constants; - -import java.util.ArrayList; - -/** - * Base class for creating a better screen display with more functionality than the default {@link - * com.badlogic.gdx.Screen} interface. - */ -public abstract class FunkinScreen implements Screen { - - // TODO: Create a way to add more than one camera! - /** The {@link OrthographicCamera} used to see the world. */ - public OrthographicCamera camera; - - /** The current {@link Viewport} of {@code this} current screen. */ - public Viewport viewport; - - /** The background color of {@code this} current screen. */ - public Color bgColor; - - /** All display objects that are shown in {@code this} screen. */ - public final ArrayList members = new ArrayList<>(); - - /** The {@link SpriteBatch} used to render sprites in the current screen. */ - protected SpriteBatch spriteBatch; - - @Override - public void show() { - spriteBatch = new SpriteBatch(); - camera = new OrthographicCamera(); - camera.setToOrtho(false, Constants.Display.WINDOW_WIDTH, Constants.Display.WINDOW_HEIGHT); - viewport = new FitViewport(Constants.Display.WINDOW_WIDTH, Constants.Display.WINDOW_HEIGHT, camera); - viewport.apply(); - bgColor = new Color(0, 0, 0, 1); - } - - @Override - public void render(float delta) { - // Refresh the screen display. - ScreenUtils.clear(bgColor); - - camera.update(); - spriteBatch.setProjectionMatrix(camera.combined); - - spriteBatch.begin(); - for (Sprite s : members) { - s.draw(spriteBatch); - } - - spriteBatch.end(); - } - - @Override - public void resize(int width, int height) { - viewport.update(width, height, true); - } - - @Override - public void pause() {} - - @Override - public void resume() {} - - @Override - public void hide() {} - - @Override - public void dispose() { - spriteBatch.dispose(); - for (Sprite s : members) { - Texture texture = s.getTexture(); - if (texture != null) { - texture.dispose(); - } - } - } - - /** - * Adds a new sprite to {@code this} screen. If it is {@code null}, it will not be added and - * simply ignored. - * - * @param s The sprite to add to the screen. - */ - public void add(Sprite s) { - if (s != null) { - members.add(s); - } - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/system/Paths.java b/core/src/main/java/me/stringfromjava/funkin/backend/system/Paths.java deleted file mode 100644 index 6a1f874..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/backend/system/Paths.java +++ /dev/null @@ -1,22 +0,0 @@ -package me.stringfromjava.funkin.backend.system; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.files.FileHandle; - -/** Utility class for simplifying asset paths. */ -public final class Paths { - - public static FileHandle asset(String path) { - return Gdx.files.internal(path); - } - - public static FileHandle shared(String path) { - return asset(String.format("shared/%s", path)); - } - - public static FileHandle image(String path) { - return shared(String.format("images/%s.png", path)); - } - - private Paths() {} -} diff --git a/core/src/main/java/me/stringfromjava/funkin/game/InitScreen.java b/core/src/main/java/me/stringfromjava/funkin/game/InitScreen.java deleted file mode 100644 index 27e5748..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/game/InitScreen.java +++ /dev/null @@ -1,14 +0,0 @@ -package me.stringfromjava.funkin.game; - -import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.backend.display.FunkinScreen; -import me.stringfromjava.funkin.game.menus.TitleScreen; - -public class InitScreen extends FunkinScreen { - - @Override - public void show() { - super.show(); - Funkin.setScreen(new TitleScreen()); - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java b/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java deleted file mode 100644 index 7e2e032..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/game/menus/TitleScreen.java +++ /dev/null @@ -1,83 +0,0 @@ -package me.stringfromjava.funkin.game.menus; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.Input; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.g2d.Sprite; -import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.audio.FunkinSound; -import me.stringfromjava.funkin.backend.display.FunkinScreen; -import me.stringfromjava.funkin.backend.system.Paths; -import me.stringfromjava.funkin.tween.FunkinTween; -import me.stringfromjava.funkin.tween.FunkinEase; -import me.stringfromjava.funkin.tween.settings.FunkinTweenSettings; - -public class TitleScreen extends FunkinScreen { - - private Sprite logo; - private FunkinSound tickleFight; - - private FunkinTween tween; - - @Override - public void show() { - super.show(); - - logo = new Sprite(new Texture(Paths.image("stage_light"))); - add(logo); - - tickleFight = new FunkinSound("shared/sounds/tickleFight.ogg"); - Funkin.playMusic("preload/music/freakyMenu/freakyMenu.ogg", 0.5f); - - FunkinTweenSettings settings = new FunkinTweenSettings() - .addGoal("x", 600) - .addGoal("y", 40) - .addGoal("rotation", 180) - .setDuration(0.7f) - .setEase(FunkinEase::circInOut); - tween = FunkinTween.tween(logo, settings, (values) -> { - logo.setX(values.get("x")); - logo.setY(values.get("y")); - logo.setRotation(values.get("rotation")); - }).stop(); - } - - @Override - public void render(float delta) { - super.render(delta); - - float speed = 200 * delta; - if (Gdx.input.isKeyPressed(Input.Keys.W)) { - logo.setY(logo.getY() + speed); - } - if (Gdx.input.isKeyPressed(Input.Keys.S)) { - logo.setY(logo.getY() - speed); - } - if (Gdx.input.isKeyPressed(Input.Keys.A)) { - logo.setX(logo.getX() - speed); - } - if (Gdx.input.isKeyPressed(Input.Keys.D)) { - logo.setX(logo.getX() + speed); - } - - if (Gdx.input.isKeyJustPressed(Input.Keys.T)) { - tween.start(); - } - - if (Gdx.input.isKeyJustPressed(Input.Keys.R)) { - tween.reset(); - } - - if (Gdx.input.isKeyJustPressed(Input.Keys.Y)) { - if (tween.paused) { - tween.resume(); - } else { - tween.pause(); - } - } - - if (Gdx.input.isKeyJustPressed(Input.Keys.Z)) { - tickleFight.play(1.0f); - } - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/polyverse/script/type/SystemScript.java b/core/src/main/java/me/stringfromjava/funkin/polyverse/script/type/SystemScript.java deleted file mode 100644 index de43ac7..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/polyverse/script/type/SystemScript.java +++ /dev/null @@ -1,8 +0,0 @@ -package me.stringfromjava.funkin.polyverse.script.type; - -public abstract class SystemScript extends Script { - - public SystemScript(String id) { - super(id); - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java deleted file mode 100644 index 6928584..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTween.java +++ /dev/null @@ -1,282 +0,0 @@ -package me.stringfromjava.funkin.tween; - -import me.stringfromjava.funkin.tween.settings.FunkinTweenSettings; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; - -/** - * Core class for creating new tweens to add nice and smooth animations to visual objects. - * - *

Note that this doesn't have to be used on sprites, it can be used on just about anything! - */ -public class FunkinTween { - - /** The global tween manager for the entire game. */ - public static FunkinTweenManager globalManager = new FunkinTweenManager(); - - /** The object to tween. */ - protected Object object; - - /** - * The settings used for how the tween is handled and calculated (aka how it looks and animates). - */ - protected FunkinTweenSettings tweenSettings; - - /** The parent manager that {@code this} tween gets updated in. */ - protected FunkinTweenManager manager; - - /** How far the tween is tweening itself. This is what's used to actually tween the object! */ - protected float scale = 0.0f; - - /** The update callback for {@code this} tween to change the objects values every update. */ - protected FunkinTween.FunkinTweenUpdateCallback updateCallback; - - /** How many seconds has elapsed since {@code this} tween started. */ - protected float secondsSinceStart = 0.0f; - - /** How many times {@code this} tween has updated. */ - protected int executions = 0; - - /** Is {@code this} tween currently paused? */ - public boolean paused = false; - - /** Is {@code this} tween currently active? */ - public boolean running = false; - - /** Is {@code this} tween finished tweening? */ - public boolean finished = false; - - /** Is {@code this} tween tweening backwards? */ - protected boolean backward = false; - - /** The initial values of the fields being tweened. */ - protected final Map initialValues = new HashMap<>(); - - /** - * @param object The object to tween values. - * @param settings The settings that configure and determine how the tween should animate and last - * for. - * @param updateCallback Callback function for updating the objects values when the tween updates. - */ - public FunkinTween( - Object object, - @NotNull FunkinTweenSettings settings, - FunkinTweenUpdateCallback updateCallback) { - this.object = object; - this.tweenSettings = settings; - this.updateCallback = updateCallback; - } - - /** - * Creates a new tween with the provided settings and starts it in the global tween manager. - * - * @param object The object to tween its values. - * @param tweenSettings The settings that configure and determine how the tween should animate. - * @param updateCallback Callback function for updating the objects values when the tween updates. - * @return The newly created and started tween. - */ - public static FunkinTween tween( - Object object, FunkinTweenSettings tweenSettings, FunkinTweenUpdateCallback updateCallback) { - return new FunkinTween(object, tweenSettings, updateCallback).setManager(globalManager).start(); - } - - public void update(float delta) { - if (paused || finished) { - return; - } - - var ease = tweenSettings.getEase(); - var duration = tweenSettings.getDuration(); - var onStart = tweenSettings.getOnStart(); - var onUpdate = tweenSettings.getOnUpdate(); - var framerate = tweenSettings.getFramerate(); - - float preTick = secondsSinceStart; - secondsSinceStart += delta; - float postTick = secondsSinceStart; - - float delay = (executions > 0) ? tweenSettings.getLoopDelay() : tweenSettings.getStartDelay(); - if (secondsSinceStart < delay) { - return; - } - - if (framerate > 0) { - preTick = Math.round(preTick * framerate) / framerate; - postTick = Math.round(postTick * framerate) / framerate; - } - - scale = Math.max((postTick - delay), 0.0f) / duration; - if (ease != null) { - scale = ease.compute(scale); - } - if (backward) { - scale = 1 - scale; - } - if (secondsSinceStart >= delay && !running) { - running = true; - if (onStart != null) { - onStart.run(this); - } - } - - if (secondsSinceStart > delay && !running) { - running = true; - if (onStart != null) { - onStart.run(this); - } - } - - // Update the object's fields based on the tween progress. - var newValues = new HashMap(); - for (String field : tweenSettings.getGoalFields()) { - FunkinTweenSettings.FunkinTweenGoal goal = tweenSettings.getGoal(field); - float startValue = initialValues.get(field); - float goalValue = goal.value(); - float newValue = startValue + (goalValue - startValue) * scale; - newValues.put(field, newValue); - } - updateCallback.update(newValues); - - // Check if the tween has finished. - if (secondsSinceStart >= duration + delay) { - scale = (backward) ? 0 : 1; - finished = true; - switch (tweenSettings.getType()) { - case ONESHOT -> { - manager.activeTweens.remove(this); - } - } - } else { - if (postTick > preTick && onUpdate != null) { - onUpdate.run(this); - } - } - } - - /** - * Starts {@code this} tween and resets every value to its initial state. - * - * @return {@code this} tween. - */ - public FunkinTween start() { - reset(); - running = true; - finished = false; - - // Ensure that the fields provided actually exist on the object and are floating point values. - var allFields = object.getClass().getDeclaredFields(); - var neededFields = tweenSettings.getGoalFields(); - for (Field field : allFields) { - try { - String fName = field.getName(); - if (!field.trySetAccessible()) { - continue; - } - if (!neededFields.contains(fName)) { - continue; - } - if (field.getType() != float.class) { - continue; - } - initialValues.put(fName, field.getFloat(object)); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - return this; - } - - /** - * Resumes {@code this} tween if it was previously paused. - * - * @return {@code this} tween. - */ - public FunkinTween resume() { - paused = false; - return this; - } - - /** - * Pauses {@code this} tween, stopping it from updating until resumed. - * - * @return {@code this} tween. - */ - public FunkinTween pause() { - paused = true; - running = false; - return this; - } - - /** - * Stops {@code this} tween. Note that this does not remove the tween from the active tweens in - * its manager. - * - * @return {@code this} tween. - */ - public FunkinTween stop() { - running = false; - finished = true; - return this; - } - - /** - * Cancels {@code this} tween and immediately removes it from the active tweens in its manager. - * - * @return {@code this} tween. - */ - public FunkinTween cancel() { - reset(); - manager.activeTweens.remove(this); - return this; - } - - /** - * Resets {@code this} tween back to its initial state without removing it from its manager. - * - * @return {@code this} tween. - */ - public FunkinTween reset() { - paused = false; - backward = false; - running = false; - finished = true; - scale = 0.0f; - secondsSinceStart = 0.0f; - executions = 0; - initialValues.clear(); - return this; - } - - public FunkinTweenSettings getTweenSettings() { - return tweenSettings; - } - - public FunkinTween setTweenSettings(@NotNull FunkinTweenSettings tweenSettings) { - this.tweenSettings = tweenSettings; - return this; - } - - public FunkinTween setManager(FunkinTweenManager manager) { - if (manager != null) { - this.manager = manager; - this.manager.activeTweens.add(this); - } - return this; - } - - /** Callback interface for changing an objects values when a tween updates its values. */ - @FunctionalInterface - public interface FunkinTweenUpdateCallback { - - /** - * A callback method that is called when the tween updates its values during its tweening - * (or animating) process. - * - * @param values The new current values of the fields being tweened during the animation. - */ - void update(Map values); - } -} diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java b/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java deleted file mode 100644 index 709be1d..0000000 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinTweenManager.java +++ /dev/null @@ -1,19 +0,0 @@ -package me.stringfromjava.funkin.tween; - -import java.util.concurrent.CopyOnWriteArrayList; - -/** Core manager class for handling all {@link FunkinTween}s that are currently active. */ -public final class FunkinTweenManager { - - /** A list where all current active tweens are stored. */ - public final CopyOnWriteArrayList activeTweens = new CopyOnWriteArrayList<>(); - - public void update(float delta) { - for (FunkinTween tween : activeTweens) { - if (tween == null) { - continue; - } - tween.update(delta); - } - } -} diff --git a/core/build.gradle b/flixelgdx/build.gradle similarity index 68% rename from core/build.gradle rename to flixelgdx/build.gradle index 9b3110a..474d702 100644 --- a/core/build.gradle +++ b/flixelgdx/build.gradle @@ -1,19 +1,17 @@ [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' -eclipse.project.name = appName + '-core' +eclipse.project.name = appName + '-flixelgdx' dependencies { - // libGDX. api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion" api "com.badlogicgames.gdx:gdx:$gdxVersion" api "com.github.tommyettinger:anim8-gdx:$anim8Version" api "com.github.tommyettinger:libgdx-utils:$utilsVersion" api "io.github.libktx:ktx-freetype:$ktxVersion" + api "games.rednblack.miniaudio:miniaudio:$miniaudioVersion" - // FNF:JE. - implementation "org.apache.groovy:groovy:4.0.12" - implementation "org.jetbrains:annotations:15.0" + implementation "org.jetbrains:annotations:26.0.2-1" - if(enableGraalNative == 'true') { + if (enableGraalNative == 'true') { implementation "io.github.berstanio:gdx-svmhelper-annotations:$graalHelperVersion" } } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java new file mode 100644 index 0000000..615053f --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java @@ -0,0 +1,487 @@ +package me.stringdotjar.flixelgdx; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.assets.AssetManager; +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.scenes.scene2d.Stage; +import games.rednblack.miniaudio.MAGroup; +import games.rednblack.miniaudio.MASound; +import games.rednblack.miniaudio.MiniAudio; +import games.rednblack.miniaudio.loader.MASoundLoader; +import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.util.FlixelConstants; +import me.stringdotjar.flixelgdx.signal.FlixelSignal; +import me.stringdotjar.flixelgdx.signal.FlixelSignalData.MusicPlayedSignalData; +import me.stringdotjar.flixelgdx.signal.FlixelSignalData.RenderSignalData; +import me.stringdotjar.flixelgdx.signal.FlixelSignalData.ScreenSwitchSignalData; +import me.stringdotjar.flixelgdx.signal.FlixelSignalData.SoundPlayedSignalData; +import org.jetbrains.annotations.NotNull; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Global manager and utility class for Flixel. + * + *

This is where you want to do the main things, like switching screens, playing sounds/music, etc. + */ +public final class Flixel { + + /** The current {@code FlixelScreen} being displayed. */ + private static FlixelScreen screen; + + /** The main audio object used to create, */ + private static MiniAudio engine; + + /** The global asset manager used to obtain preloaded assets. */ + private static AssetManager assetManager; + + /** The audio group for all sound effects, including the current music. */ + private static MAGroup soundsGroup; + + /** The sound for playing music throughout the game. */ + private static MASound music; + + /** The current master volume that is set. */ + private static float masterVolume = 1; + + /** The static instance used to access the core elements of the game. */ + private static FlixelGame game; + + /** Has the global manager been initialized yet? */ + private static boolean initialized = false; + + /** + * Initializes the global manager. + * + *

This can only be called once. If attempted to be executed again, the game will throw an + * exception. + * + * @param gameInstance The instance of the game to use. + */ + public static void initialize(FlixelGame gameInstance) { + if (initialized) { + throw new IllegalStateException("FNF:JE has already been initialized!"); + } + game = gameInstance; + + assetManager = new AssetManager(); + + // Set up the game's global audio system. + engine = new MiniAudio(); + soundsGroup = engine.createGroup(); + assetManager.setLoader(MASound.class, new MASoundLoader(engine, assetManager.getFileHandleResolver())); + + initialized = true; + } + + /** + * Sets the current screen to the provided screen. + * + * @param newScreen The new {@code FlixelScreen} to set as the current screen. + */ + public static void setScreen(FlixelScreen newScreen) { + Signals.preScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); + if (!initialized) { + throw new IllegalStateException("FNF:JE has not been initialized yet!"); + } + if (newScreen == null) { + throw new IllegalArgumentException("Screen cannot be null!"); + } + if (Flixel.screen != null) { + Flixel.screen.hide(); + Flixel.screen.dispose(); + } + Flixel.screen = newScreen; + Flixel.screen.show(); + Signals.postScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); + } + + /** + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * Flixel.playSound(FlixelPaths.external("your/path/here").path());
+   * }
+ * + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @return The new sound instance. + */ + public static MASound playSound(String path) { + return playSound(path, 1, false, null, false); + } + + /** + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * Flixel.playSound(FlixelPaths.external("your/path/here").path(), 1);
+   * }
+ * + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new sound with. + * @return The new sound instance. + */ + public static MASound playSound(String path, float volume) { + return playSound(path, volume, false, null, false); + } + + /** + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * Flixel.playSound(FlixelPaths.external("your/path/here").path(), 1, false);
+   * }
+ * + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new sound with. + * @param looping Should the new sound loop indefinitely? + * @return The new sound instance. + */ + public static MASound playSound(String path, float volume, boolean looping) { + return playSound(path, volume, looping, null, false); + } + + /** + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * // If null is passed down for the group, then the default sound group will be used.
+   * Flixel.playSound(FlixelPaths.external("your/path/here").path(), 1, false, null);
+   * }
+ * + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new sound with. + * @param looping Should the new sound loop indefinitely? + * @param group The sound group to add the new sound to. If {@code null} is passed down, then the + * default sound group will be used. + * @return The new sound instance. + */ + public static MASound playSound(String path, float volume, boolean looping, MAGroup group) { + return playSound(path, volume, looping, group, false); + } + + /** + * Plays a new sound effect. + * + *

When you want to play a sound externally, outside the assets folder, you can use a {@link + * FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * // If null is passed down for the group, then the default sound group will be used.
+   * // For the boolean attribuite "external", you only should make it true for mobile builds,
+   * // otherwise just simply leave it be or make it "false" for other platforms like desktop.
+   * Flixel.playSound(FlixelPaths.external("your/path/here").path(), 1, false, null, true);
+   * }
+ * + * @param path The path to load the sound from. Note that if you're loading an external sound + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new sound with. + * @param looping Should the new sound loop indefinitely? + * @param group The sound group to add the new sound to. If {@code null} is passed down, then the + * default sound group will be used. + * @param external Should this sound be loaded externally? (This is only for mobile platforms!) + * @return The new sound instance. + */ + public static MASound playSound(@NotNull String path, float volume, boolean looping, MAGroup group, boolean external) { + MASound sound = engine.createSound(path, (short) 0, (group != null) ? group : soundsGroup, external); + Signals.preSoundPlayed.dispatch(new SoundPlayedSignalData(sound)); + sound.setVolume(volume); + sound.setLooping(looping); + sound.play(); + Signals.postSoundPlayed.dispatch(new SoundPlayedSignalData(sound)); + return sound; + } + + /** + * Sets the current music playing for the entire game. + * + *

When you want to play music located externally, outside the assets folder, you can use a + * {@link FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * Flixel.playMusic(FlixelPaths.external("your/path/here").path());
+   * }
+ * + * @param path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + */ + public static void playMusic(String path) { + playMusic(path, 1, true, false); + } + + /** + * Sets the current music playing for the entire game. + * + *

When you want to play music located externally, outside the assets folder, you can use a + * {@link FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * Flixel.playMusic(FlixelPaths.external("your/path/here").path(), 1);
+   * }
+ * + * @param path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new music with. + */ + public static void playMusic(String path, float volume) { + playMusic(path, volume, true, false); + } + + /** + * Sets the current music playing for the entire game. + * + *

When you want to play music located externally, outside the assets folder, you can use a + * {@link FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * Flixel.playMusic(FlixelPaths.external("your/path/here").path(), 1, false);
+   * }
+ * + * @param path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new music with. + * @param looping Should the new music loop indefinitely? + */ + public static void playMusic(String path, float volume, boolean looping) { + playMusic(path, volume, looping, false); + } + + /** + * Sets the current music playing for the entire game. + * + *

When you want to play music located externally, outside the assets folder, you can use a + * {@link FileHandle} like so: + * + *

{@code
+   * // Notice how it uses the FlixelPaths class provided by Flixel'.
+   * // For the boolean attribuite "external", you only should make it true for mobile builds,
+   * // otherwise just simply leave it be or make it "false" for other platforms like desktop.
+   * Flixel.playMusic(FlixelPaths.external("your/path/here").path(), 1, false, true);
+   * }
+ * + * @param path The path to load the music from. Note that if you're loading an external sound file + * outside the game's assets, you should use {@link FileHandle}; otherwise, just pass down a + * regular string (without {@code assets/} at the beginning). + * @param volume The volume to play the new music with. + * @param looping Should the new music loop indefinitely? + * @param external Should this music be loaded externally? (This is only for mobile platforms!) + */ + public static void playMusic(String path, float volume, boolean looping, boolean external) { + Signals.preMusicPlayed.dispatch(new MusicPlayedSignalData(music)); + if (music != null) { + music.stop(); + } + music = engine.createSound(path, (short) 0, soundsGroup, external); + music.setVolume(volume); + music.setLooping(looping); + music.play(); + Signals.postMusicPlayed.dispatch(new MusicPlayedSignalData(music)); + } + + /** + * Sets the game master/global volume, which is automatically applied to all current sounds. + * + *

(This is just a helper method for creating a faster version of {@code + * Flixel.getAudioEngine().setMasterVolume(float)}). + * + * @param volume The new master volume to set. + */ + public static void setMasterVolume(float volume) { + engine.setMasterVolume(!(volume > 1.0) ? volume : 1.0f); + masterVolume = volume; + } + + public static boolean keyPressed(int key) { + return Gdx.input.isKeyPressed(key); + } + + public static boolean keyJustPressed(int key) { + return Gdx.input.isKeyJustPressed(key); + } + + public static void info(Object message) { + info(FlixelConstants.System.LOG_TAG, message); + } + + public static void info(String tag, Object message) { + outputLog(tag, message, FlixelLogLevel.INFO); + } + + public static void warn(Object message) { + warn(FlixelConstants.System.LOG_TAG, message); + } + + public static void warn(String tag, Object message) { + outputLog(tag, message, FlixelLogLevel.WARN); + } + + public static void error(String message) { + error(FlixelConstants.System.LOG_TAG, message, null); + } + + public static void error(String tag, Object message) { + error(tag, message, null); + } + + public static void error(String tag, Object message, Throwable throwable) { + String msg = (throwable != null) ? (message + " | Exception: " + throwable) : message.toString(); + outputLog(tag, msg, FlixelLogLevel.ERROR); + } + + public static FlixelGame getGame() { + return game; + } + + public static Stage getStage() { + return game.stage; + } + + public static FlixelScreen getScreen() { + return screen; + } + + public static MASound getMusic() { + return music; + } + + public static MiniAudio getAudioEngine() { + return engine; + } + + public static float getMasterVolume() { + return masterVolume; + } + + public static AssetManager getAssetManager() { + return assetManager; + } + + public static MAGroup getSoundsGroup() { + return soundsGroup; + } + + public static float getDelta() { + return Gdx.graphics.getDeltaTime(); + } + + /** + * Contains all the global events that get dispatched when something happens in the game. + * + *

This includes anything from the screen being switched, the game updating every frame, and + * just about everything you can think of. + * + *

IMPORTANT DETAIL!: Anything with the {@code pre} and {@code post} prefixes always mean the + * same thing. If a signal has {@code pre}, then the signal gets ran BEFORE any functionality is + * executed, and {@code post} means AFTER all functionality was executed. + */ + public static final class Signals { + + public static final FlixelSignal preRender = new FlixelSignal<>(); + public static final FlixelSignal postRender = new FlixelSignal<>(); + public static final FlixelSignal preScreenSwitch = new FlixelSignal<>(); + public static final FlixelSignal postScreenSwitch = new FlixelSignal<>(); + public static final FlixelSignal preGameClose = new FlixelSignal<>(); + public static final FlixelSignal postGameClose = new FlixelSignal<>(); + public static final FlixelSignal windowFocused = new FlixelSignal<>(); + public static final FlixelSignal windowUnfocused = new FlixelSignal<>(); + public static final FlixelSignal windowMinimized = new FlixelSignal<>(); + public static final FlixelSignal preSoundPlayed = new FlixelSignal<>(); + public static final FlixelSignal postSoundPlayed = new FlixelSignal<>(); + public static final FlixelSignal preMusicPlayed = new FlixelSignal<>(); + public static final FlixelSignal postMusicPlayed = new FlixelSignal<>(); + + private Signals() {} + } + + // ====================================== + // UTILITY FUNCTIONS, IGNORE BELOW + // ====================================== + + private static void outputLog(String tag, Object message, FlixelLogLevel level) { + StackWalker.StackFrame caller = StackWalker.getInstance() + .walk(s -> s.skip(3).findFirst()) + .orElse(null); + + String file = "UnknownFile.java:0"; + String method = "unknownMethod()"; + if (caller != null) { + file = caller.getFileName() + ":" + caller.getLineNumber(); + method = caller.getMethodName() + "()"; + } + + String color = switch (level) { + case INFO -> FlixelConstants.AsciiCodes.WHITE; + case WARN -> FlixelConstants.AsciiCodes.YELLOW; + case ERROR -> FlixelConstants.AsciiCodes.RED; + }; + + boolean underline = (level == FlixelLogLevel.ERROR); + String timeAndDate = colorText(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " ", + color, + true, + false, + underline); + String formattedTag = colorText("[" + level + "] [" + tag + "] [" + file + "] [" + method + "] ", color, true, false, underline); + String formattedMessage = colorText((message != null) ? message.toString() : null, color, false, true, underline); + + System.out.println(timeAndDate + formattedTag + formattedMessage); + } + + private static String colorText(String text, String color, boolean bold, boolean italic, boolean underline) { + StringBuilder sb = new StringBuilder(); + if (bold) { + sb.append(FlixelConstants.AsciiCodes.BOLD); + } + if (italic) { + sb.append(FlixelConstants.AsciiCodes.ITALIC); + } + if (underline) { + sb.append(FlixelConstants.AsciiCodes.UNDERLINE); + } + sb.append(color); + sb.append(text); + sb.append(FlixelConstants.AsciiCodes.RESET); + return sb.toString(); + } + + private Flixel() {} +} + +enum FlixelLogLevel { + INFO, + WARN, + ERROR +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java new file mode 100644 index 0000000..36b8078 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java @@ -0,0 +1,274 @@ +package me.stringdotjar.flixelgdx; + +import com.badlogic.gdx.Application; +import com.badlogic.gdx.ApplicationListener; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.utils.ScreenUtils; +import com.badlogic.gdx.utils.viewport.FitViewport; +import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelObject; +import me.stringdotjar.flixelgdx.tween.FlixelTween; +import me.stringdotjar.flixelgdx.util.FlixelConstants; +import me.stringdotjar.flixelgdx.util.FlixelRuntimeUtil; + +import static me.stringdotjar.flixelgdx.signal.FlixelSignalData.RenderSignalData; + +/** + * Flixel's enhanced game object used for containing the main loop and core elements of Flixel. + * + *

If you want to change what happens to the pre and window configurations, you might want to see + * {@code Lwjgl3Launcher} in the {@code lwjgl3} folder. + */ +public abstract class FlixelGame implements ApplicationListener { + + /** The title displayed on the game's window. */ + protected String title; + + /** The size of the game's starting window position and its viewport. */ + protected Vector2 windowSize; + + /** The entry point screen the game starts in (which becomes null after the game is done setting up!). */ + protected FlixelScreen initialScreen; + + /** Is the game's window currently focused? */ + protected boolean isFocused = true; + + /** Is the game's window currently minimized? */ + protected boolean isMinimized = false; + + /** The main stage used for rendering all screens and sprites on screen. */ + protected Stage stage; + + /** The main viewport used to fit the world no matter the screen size. */ + protected FitViewport viewport; + + /** The main camera used to see the world. */ + protected OrthographicCamera camera; + + /** The main sprite batch used for rendering all sprites on screen. */ + protected SpriteBatch batch; + + /** The 1x1 texture used to draw the background color of the current screen. */ + protected Texture bgTexture; + + /** + * Creates a new game instance with the specified title, window width/height, and initial screen. This configures + * the game's core parts, such as the viewport, stage, etc. + * + * @param title The title of the game's window. + * @param width The starting width of the game's window and how wide the viewport should be. + * @param height The starting height of the game's window and how tall the viewport should be. + * @param initialScreen The initial screen to load when the game starts. + */ + public FlixelGame(String title, int width, int height, FlixelScreen initialScreen) { + this.title = title; + this.windowSize = new Vector2(width, height); + this.initialScreen = initialScreen; + } + + @Override + public void create() { + configureCrashHandler(); // This should ALWAYS be called first no matter what! + + batch = new SpriteBatch(); + viewport = new FitViewport(windowSize.x, windowSize.y); + viewport.apply(); + + camera = new OrthographicCamera(); + camera.setToOrtho(false, windowSize.x, windowSize.y); + + stage = new Stage(viewport, batch); + + Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888); + pixmap.setColor(Color.WHITE); + pixmap.fill(); + bgTexture = new Texture(pixmap); + pixmap.dispose(); + + Flixel.setScreen(initialScreen); + initialScreen = null; + } + + @Override + public void resize(int width, int height) { + viewport.update(width, height, true); + } + + @Override + public void render() { + float delta = Gdx.graphics.getDeltaTime(); + FlixelScreen screen = Flixel.getScreen(); + + Flixel.Signals.preRender.dispatch(new RenderSignalData(delta)); + + if (Flixel.keyJustPressed(Input.Keys.F11)) { + toggleFullscreen(); + } + + // Update and render the current screen that's active. + ScreenUtils.clear(Color.BLACK); + viewport.apply(); + batch.setProjectionMatrix(camera.combined); + batch.begin(); + + if (screen != null) { + batch.setColor(screen.getBgColor()); + batch.draw(bgTexture, 0, 0, viewport.getWorldWidth(), viewport.getWorldHeight()); + batch.setColor(Color.WHITE); // Set color back to white so display objects aren't affected. + screen.render(delta); + var members = screen.members.begin(); + for (FlixelObject object : members) { + if (object == null) { + continue; + } + object.update(delta); + object.draw(batch); + } + screen.members.end(); + } + + batch.end(); + stage.act(delta); + stage.draw(); + + FlixelTween.getGlobalManager().update(delta); + + Flixel.Signals.postRender.dispatch(new RenderSignalData(delta)); + } + + @Override + public void pause() {} + + @Override + public void resume() {} + + /** Called when the user regains focus on the game's window. */ + public void onWindowFocused() { + isFocused = true; + Flixel.Signals.windowFocused.dispatch(); + Flixel.info("Game window has regained focus."); + } + + /** Called when the user loses focus on the game's window, while also not being minimized. */ + public void onWindowUnfocused() { + if (isMinimized) { + return; + } + isFocused = false; + Flixel.Signals.windowUnfocused.dispatch(); + Flixel.info("Game window has lost focus."); + } + + /** + * Called when the user minimizes the game's window. + * + * @param iconified Whether the window is iconified (minimized) or not. This parameter is provided + * for compatibility with the window listener in the LWJGL3 (desktop) launcher. + */ + public void onWindowMinimized(boolean iconified) { + isMinimized = iconified; + if (!isMinimized) { + return; + } + isFocused = false; + Flixel.Signals.windowMinimized.dispatch(); + Flixel.info("Game window has been minimized."); + } + + /** Toggles fullscreen mode on or off, depending on the current state. */ + public void toggleFullscreen() { + boolean isFullscreen = Gdx.graphics.isFullscreen(); + if (isFullscreen) { + Gdx.graphics.setWindowedMode((int) windowSize.x, (int) windowSize.y); + Flixel.info("Exiting fullscreen mode."); + } else { + Gdx.graphics.setFullscreenMode(Gdx.graphics.getDisplayMode()); + Flixel.info("Entering fullscreen mode."); + } + } + + @Override + public void dispose() { + Flixel.warn("SHUTTING DOWN GAME AND DISPOSING ALL RESOURCES."); + + Flixel.Signals.preGameClose.dispatch(); + + Flixel.info("Disposing the screen display..."); + Flixel.getScreen().hide(); + Flixel.getScreen().dispose(); + stage.dispose(); + batch.dispose(); + bgTexture.dispose(); + + Flixel.info("Disposing all sounds from sound group and music..."); + if (Flixel.getMusic() != null) { + Flixel.getMusic().dispose(); + } + Flixel.getSoundsGroup().dispose(); + Flixel.getAudioEngine().dispose(); + + Flixel.info("Disposing and shutting down scripts..."); + + Flixel.Signals.postGameClose.dispatch(); + } + + public String getTitle() { + return title; + } + + public Vector2 getWindowSize() { + return windowSize; + } + + public boolean isFocused() { + return isFocused; + } + + public Stage getStage() { + return stage; + } + + public FitViewport getViewport() { + return viewport; + } + + public OrthographicCamera getCamera() { + return camera; + } + + public SpriteBatch getBatch() { + return batch; + } + + public Texture getBgTexture() { + return bgTexture; + } + + public boolean isMinimized() { + return isMinimized; + } + + /** + * Configurers Flixel's crash handler to safely catch uncaught exceptions and gracefully close the game. + */ + protected void configureCrashHandler() { + Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + String logs = FlixelRuntimeUtil.getFullExceptionMessage(throwable); + String msg = "There was an uncaught exception on thread \"" + thread.getName() + "\"!\n" + logs; + Flixel.error(FlixelConstants.System.LOG_TAG, msg); + dispose(); + // Only use Gdx.app.exit() on non-iOS platforms to avoid App Store guideline violations! + if (Gdx.app.getType() != Application.ApplicationType.iOS) { + Gdx.app.exit(); + } + }); + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/backend/FlixelPaths.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/backend/FlixelPaths.java new file mode 100644 index 0000000..f349622 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/backend/FlixelPaths.java @@ -0,0 +1,30 @@ +package me.stringdotjar.flixelgdx.backend; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.files.FileHandle; + +/** Utility class for simplifying asset paths and libGDX {@link FileHandle}s. */ +public final class FlixelPaths { + + public static FileHandle asset(String path) { + return Gdx.files.internal(path); + } + + public static FileHandle shared(String path) { + return asset(String.format("shared/%s", path)); + } + + public static FileHandle xmlAsset(String path) { + return asset(String.format("%s.xml", path)); + } + + public static FileHandle sharedImageAsset(String path) { + return shared(String.format("images/%s.png", path)); + } + + public static FileHandle external(String path) { + return Gdx.files.external(path); + } + + private FlixelPaths() {} +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/screen/FlixelScreen.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/screen/FlixelScreen.java new file mode 100644 index 0000000..8f00693 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/screen/FlixelScreen.java @@ -0,0 +1,56 @@ +package me.stringdotjar.flixelgdx.graphics.screen; + +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.utils.SnapshotArray; +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelObject; + +/** + * Base class for creating a better screen display with more functionality than the default {@link + * com.badlogic.gdx.Screen} interface. + */ +public abstract class FlixelScreen implements Screen { + + /** The background color of {@code this} current screen. */ + protected Color bgColor; + + /** All display objects that are shown in {@code this} screen. */ + public final SnapshotArray members = new SnapshotArray<>(FlixelObject.class); + + @Override + public void show() {} + + @Override + public void render(float delta) {} + + @Override + public void resize(int width, int height) {} + + @Override + public void pause() {} + + @Override + public void resume() {} + + @Override + public void hide() {} + + @Override + public void dispose() {} + + /** + * Adds a new sprite to {@code this} screen. If it is {@code null}, it will not be added and + * simply ignored. + * + * @param object The sprite to add to the screen. + */ + public void add(FlixelObject object) { + if (object != null) { + members.add(object); + } + } + + public Color getBgColor() { + return (bgColor != null) ? bgColor : Color.BLACK; + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java new file mode 100644 index 0000000..6ebd5c2 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java @@ -0,0 +1,10 @@ +package me.stringdotjar.flixelgdx.graphics.sprite; + +import com.badlogic.gdx.graphics.g2d.Batch; +import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; + +/** An interface which allows any class that implements it to be added to a {@link FlixelScreen}. */ +public interface FlixelObject { + void update(float delta); + void draw(Batch batch); +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java new file mode 100644 index 0000000..ca82924 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java @@ -0,0 +1,371 @@ +package me.stringdotjar.flixelgdx.graphics.sprite; + +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Animation; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Pool; +import com.badlogic.gdx.utils.XmlReader; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +/** + * An enhanced version of libGDX's {@link Sprite} class. + * + *

It allows you to load animations, textures, and do much more with simplicity and ease. + */ +public class FlixelSprite extends Sprite implements FlixelObject, Pool.Poolable { + + /** The texture image that {@code this} sprite uses. */ + protected Texture texture; + + /** The hitbox used for collision detection and angling. */ + protected Rectangle hitbox; + + /** The atlas regions used in this sprite (used for animations). */ + protected Array atlasRegions; + + /** A map that animations that stored and registered in. */ + protected final Map> animations; + + /** The current frame that {@code this} sprite is on in its animation (if one is playing). */ + protected TextureAtlas.AtlasRegion currentFrame; + + /** Used for updating {@code this} sprite's current animation. */ + protected float stateTime = 0; + + /** The name of the current animation playing. */ + protected String currentAnim = ""; + + /** Is {@code this} sprites current animation looping indefinitely? */ + protected boolean looping = true; + + /** + * Where all the image frames are stored. This is also where the main image is stored when using + * {@link #loadGraphic(FileHandle)}. + */ + protected TextureRegion[][] frames; + + public FlixelSprite() { + animations = new HashMap<>(); + } + + /** + * Updates {@code this} sprite. + * + * @param delta The amount of time that has passed since the last frame update. + */ + @Override + public void update(float delta) { + if (animations.containsKey(currentAnim)) { + stateTime += delta; + currentFrame = (TextureAtlas.AtlasRegion) animations.get(currentAnim).getKeyFrame(stateTime, looping); + setRegion(currentFrame); + } + } + + /** + * Load's a texture and automatically resizes the size of {@code this} sprite. + * + * @param path The directory of the {@code .png} to load onto {@code this} sprite. + * @return {@code this} sprite for chaining. + */ + public FlixelSprite loadGraphic(FileHandle path) { + Texture texture = new Texture(path); + return loadGraphic(texture, texture.getWidth(), texture.getHeight()); + } + + /** + * Load's a texture and automatically resizes the size of {@code this} sprite. + * + * @param path The directory of the {@code .png} to load onto {@code this} sprite. + * @param frameWidth How wide the sprite should be. + * @return {@code this} sprite for chaining. + */ + public FlixelSprite loadGraphic(FileHandle path, int frameWidth) { + Texture texture = new Texture(path); + return loadGraphic(texture, frameWidth, texture.getHeight()); + } + + /** + * Load's a texture and automatically resizes the size of {@code this} sprite. + * + * @param path The directory of the {@code .png} to load onto {@code this} sprite. + * @param frameWidth How wide the sprite should be. + * @param frameHeight How tall the sprite should be. + * @return {@code this} sprite for chaining. + */ + public FlixelSprite loadGraphic(FileHandle path, int frameWidth, int frameHeight) { + return loadGraphic(new Texture(path), frameWidth, frameHeight); + } + + public FlixelSprite loadGraphic(Texture texture, int frameWidth, int frameHeight) { + frames = TextureRegion.split(texture, frameWidth, frameHeight); + // Set default visual to the first frame. + setRegion(frames[0][0]); + setSize(frameWidth, frameHeight); + setOriginCenter(); + return this; + } + + /** + * Loads an {@code .xml} spritesheet with {@code SubTexture} data inside of it. + * + * @param texture The path to the {@code .png} texture file for slicing and extracting the + * different frames from. + * @param xmlFile The path to the {@code .xml} file which contains the data for each subtexture of + * the sparrow atlas. + * @return {@code this} sprite for chaining. + */ + public FlixelSprite loadSparrowFrames(FileHandle texture, FileHandle xmlFile) { + return loadSparrowFrames(new Texture(texture), new XmlReader().parse(xmlFile)); + } + + /** + * Loads an {@code .xml} spritesheet with {@code SubTexture} data inside of it. + * + * @param texture The {@code .png} texture file for slicing and extracting the different frames + * from. + * @param xmlFile The {@link XmlReader.Element} data which contains the data for each subtexture + * of the sparrow atlas. + * @return {@code this} sprite for chaining. + */ + public FlixelSprite loadSparrowFrames(Texture texture, XmlReader.Element xmlFile) { + // We store regions in a list so we can filter them by prefix later. + // TextureAtlas.AtlasRegion is used because it supports offsets. + atlasRegions = new Array<>(); + + for (XmlReader.Element subTexture : xmlFile.getChildrenByName("SubTexture")) { + String name = subTexture.getAttribute("name"); + int x = subTexture.getInt("x"); + int y = subTexture.getInt("y"); + int width = subTexture.getInt("width"); + int height = subTexture.getInt("height"); + + this.texture = texture; + TextureAtlas.AtlasRegion region = new TextureAtlas.AtlasRegion(texture, x, y, width, height); + region.name = name; + + // Handle Trimming/Offsets (frameX, frameY, frameWidth, frameHeight). + // These are crucial for centering notes/characters correctly. + if (subTexture.hasAttribute("frameX")) { + region.offsetX = Math.abs(subTexture.getInt("frameX")); + region.offsetY = Math.abs(subTexture.getInt("frameY")); + region.originalWidth = subTexture.getInt("frameWidth"); + region.originalHeight = subTexture.getInt("frameHeight"); + } else { + region.offsetX = 0; + region.offsetY = 0; + region.originalWidth = width; + region.originalHeight = height; + } + + atlasRegions.add(region); + } + + if (atlasRegions.size > 0) { + currentFrame = atlasRegions.first(); + setRegion(currentFrame); + + // This ensures the Sprite's internal width/height matches + // the frame before the first update(). + setSize(currentFrame.getRegionWidth(), currentFrame.getRegionHeight()); + } + + // Set the default visual. + setRegion(atlasRegions.first()); + setSize(getRegionWidth(), getRegionHeight()); + return this; + } + + /** + * Adds an animation by looking for sub textures that start with the prefix passed down. + * + * @param name The name of the animation (e.g., "confirm"). + * @param prefix The prefix in the {@code .xml} file (e.g., "left confirm"). + * @param frameRate How fast the animation should play in frames-per-second. Standard is 24. + * @param loop Should the new animation loop indefinitely? + */ + public void addAnimationByPrefix(String name, String prefix, int frameRate, boolean loop) { + Array frames = new Array<>(); + + for (TextureAtlas.AtlasRegion region : atlasRegions) { + if (region.name.startsWith(prefix)) { + frames.add(region); + } + } + + if (frames.size > 0) { + // Ensure frames are sorted alphabetically (e.g., confirm0000, confirm0001). + frames.sort(Comparator.comparing(o -> o.name)); + + animations.put( + name, + new Animation<>( + 1f / frameRate, frames, loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL)); + } + } + + /** + * Adds a new animation to the animations list, if it doesn't exist already. + * + * @param name The name of the animation. This is what you'll use every time you use {@code + * playAnimation()}. + * @param frameIndices An array of integers used for animation frame indices. + * @param frameDuration How long each frame lasts for in seconds. + */ + public void addAnimation(String name, int[] frameIndices, float frameDuration) { + Array animFrames = new Array<>(); + + // Convert 1D indices (0, 1, 2...) to 2D grid coordinates. + int cols = frames[0].length; + for (int index : frameIndices) { + int row = index / cols; + int col = index % cols; + animFrames.add(frames[row][col]); + } + + animations.put(name, new Animation<>(frameDuration, animFrames)); + } + + /** + * Plays an animation {@code this} sprite has, with looping enabled by default. + * + * @param name The name of the animation to play. + */ + public void playAnimation(String name) { + playAnimation(name, true); + } + + /** + * Plays an animation {@code this} sprite has, if it exists. + * + * @param name The name of the animation to play. + * @param loop Should this animation loop indefinitely? + */ + public void playAnimation(String name, boolean loop) { + playAnimation(name, loop, true); + } + + /** + * Plays an animation {@code this} sprite has, if it exists. + * + * @param name The name of the animation to play. + * @param loop Should this animation loop indefinitely? + * @param forceRestart Should the animation automatically restart regardless if its playing? + */ + public void playAnimation(String name, boolean loop, boolean forceRestart) { + if (currentAnim.equals(name) && !forceRestart) { + return; + } + if (isAnimationFinished() || forceRestart) { + this.currentAnim = name; + this.looping = loop; + this.stateTime = 0; + } + } + + @Override + public void draw(Batch batch) { + if (currentFrame == null) { + super.draw(batch); + return; + } + + // Calculate the center based on the ORIGINAL frame size. + // This ensures the rotation point stays consistent across frames. + float originX = currentFrame.originalWidth / 2f; + float originY = currentFrame.originalHeight / 2f; + + // Adjust drawing position by the Sparrow offsets + float drawX = getX() + currentFrame.offsetX; + float drawY = getY() + (currentFrame.originalHeight - currentFrame.getRegionHeight() - currentFrame.offsetY); + + batch.draw( + currentFrame, + drawX, + drawY, + originX - currentFrame.offsetX, + originY - (currentFrame.originalHeight - currentFrame.getRegionHeight() - currentFrame.offsetY), + currentFrame.getRegionWidth(), + currentFrame.getRegionHeight(), + getScaleX(), + getScaleY(), + getRotation()); + } + + public boolean isAnimationFinished() { + Animation anim = animations.get(currentAnim); + if (anim == null) return true; + return anim.isAnimationFinished(stateTime); + } + + @Override + public void reset() { + setPosition(0f, 0f); + stateTime = 0; + currentAnim = null; + looping = true; + texture.dispose(); + texture = null; + currentFrame.getTexture().dispose(); + currentFrame = null; + for (int i = atlasRegions.size; i >= 0; i--) { + var region = atlasRegions.items[i]; + region.getTexture().dispose(); + } + atlasRegions.setSize(0); + atlasRegions = null; + for (int i = frames.length - 1; i >= 0; i--) { + var frame = frames[i]; + for (TextureRegion region : frame) { + region.getTexture().dispose(); + } + } + frames = null; + } + + public void changeX(float x) { + setX(getX() + x); + } + + public void changeY(float y) { + setY(getY() + y); + } + + public Map> getAnimations() { + return animations; + } + + public Array getAtlasRegions() { + return atlasRegions; + } + + public TextureAtlas.AtlasRegion getCurrentFrame() { + return currentFrame; + } + + public float getStateTime() { + return stateTime; + } + + public String getCurrentAnim() { + return currentAnim; + } + + public boolean isLooping() { + return looping; + } + + public TextureRegion[][] getFrames() { + return frames; + } +} diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/display/text/DisplayText.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/text/FlixelText.java similarity index 77% rename from core/src/main/java/me/stringfromjava/funkin/backend/display/text/DisplayText.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/text/FlixelText.java index f1b628c..cd72e43 100644 --- a/core/src/main/java/me/stringfromjava/funkin/backend/display/text/DisplayText.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/text/FlixelText.java @@ -1,7 +1,7 @@ -package me.stringfromjava.funkin.backend.display.text; +package me.stringdotjar.flixelgdx.graphics.text; /** A display object for creating a piece of text to show on the screen. */ -public class DisplayText { +public class FlixelText { /** The text to be written onto the screen. */ public String text; @@ -15,7 +15,7 @@ public class DisplayText { /** * @param text The string to be displayed. */ - public DisplayText(String text) { + public FlixelText(String text) { this.text = text; x = 0; y = 0; diff --git a/core/src/main/java/me/stringfromjava/funkin/backend/system/FunkinSignal.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignal.java similarity index 85% rename from core/src/main/java/me/stringfromjava/funkin/backend/system/FunkinSignal.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignal.java index 59efa42..3fe63b8 100644 --- a/core/src/main/java/me/stringfromjava/funkin/backend/system/FunkinSignal.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignal.java @@ -1,4 +1,4 @@ -package me.stringfromjava.funkin.backend.system; +package me.stringdotjar.flixelgdx.signal; import java.util.concurrent.CopyOnWriteArrayList; @@ -6,12 +6,12 @@ * Utility class for creating objects that can execute multiple callbacks when it is dispatched (or * triggered). */ -public class FunkinSignal { +public class FlixelSignal { private final CopyOnWriteArrayList> callbacks; private final CopyOnWriteArrayList> tempCallbacks; // Callbacks that are added with addOnce(). - public FunkinSignal() { + public FlixelSignal() { callbacks = new CopyOnWriteArrayList<>(); tempCallbacks = new CopyOnWriteArrayList<>(); } @@ -40,6 +40,16 @@ public void addOnce(SignalHandler callback) { } } + /** + * Removes a specific callback from {@code this} signal. + * + * @param callback The callback to remove. + */ + public void remove(SignalHandler callback) { + callbacks.remove(callback); + tempCallbacks.remove(callback); + } + /** Removes all callbacks from {@code this} signal. */ public void clear() { callbacks.clear(); diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java new file mode 100644 index 0000000..84f010b --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java @@ -0,0 +1,22 @@ +package me.stringdotjar.flixelgdx.signal; + +import games.rednblack.miniaudio.MASound; +import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.Flixel; + +/** + * Convenience class for holding all signal data records used in the default signals stored in + * the global {@link Flixel} manager class. + */ +public final class FlixelSignalData { + + public record RenderSignalData(float delta) {} + + public record ScreenSwitchSignalData(FlixelScreen screen) {} + + public record SoundPlayedSignalData(MASound sound) {} + + public record MusicPlayedSignalData(MASound music) {} + + private FlixelSignalData() {} +} diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinEase.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelEase.java similarity index 78% rename from core/src/main/java/me/stringfromjava/funkin/tween/FunkinEase.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelEase.java index f6cfc5c..7bed09f 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/FunkinEase.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelEase.java @@ -1,7 +1,7 @@ -package me.stringfromjava.funkin.tween; +package me.stringdotjar.flixelgdx.tween; /** Class where all easer functions are stored, mostly used for tweening. */ -public final class FunkinEase { +public final class FlixelEase { // Easing constants for specific functions. private static final float PI2 = (float) Math.PI / 2; @@ -15,7 +15,7 @@ public final class FunkinEase { private static final float ELASTIC_AMPLITUDE = 1; private static final float ELASTIC_PERIOD = 0.4f; - private FunkinEase() {} + private FlixelEase() {} public static float linear(float t) { return t; @@ -130,9 +130,9 @@ public static float circOut(float t) { public static float circInOut(float t) { return (float) - (t <= .5 - ? (Math.sqrt(1 - t * t * 4) - 1) / -2 - : (Math.sqrt(1 - (t * 2 - 2) * (t * 2 - 2)) + 1) / 2); + (t <= .5 + ? (Math.sqrt(1 - t * t * 4) - 1) / -2 + : (Math.sqrt(1 - (t * 2 - 2) * (t * 2 - 2)) + 1) / 2); } public static float expoIn(float t) { @@ -145,7 +145,7 @@ public static float expoOut(float t) { public static float expoInOut(float t) { return (float) - (t < .5 ? Math.pow(2, 10 * (t * 2 - 1)) / 2 : (-Math.pow(2, -10 * (t * 2 - 1)) + 2) / 2); + (t < .5 ? Math.pow(2, 10 * (t * 2 - 1)) / 2 : (-Math.pow(2, -10 * (t * 2 - 1)) + 2) / 2); } public static float backIn(float t) { @@ -165,37 +165,37 @@ public static float backInOut(float t) { public static float elasticIn(float t) { return (float) - -(ELASTIC_AMPLITUDE - * Math.pow(2, 10 * (t -= 1)) - * Math.sin( - (t - (ELASTIC_PERIOD / (2 * Math.PI) * Math.asin(1 / ELASTIC_AMPLITUDE))) - * (2 * Math.PI) - / ELASTIC_PERIOD)); + -(ELASTIC_AMPLITUDE + * Math.pow(2, 10 * (t -= 1)) + * Math.sin( + (t - (ELASTIC_PERIOD / (2 * Math.PI) * Math.asin(1 / ELASTIC_AMPLITUDE))) + * (2 * Math.PI) + / ELASTIC_PERIOD)); } public static float elasticOut(float t) { return (float) - (ELASTIC_AMPLITUDE - * Math.pow(2, -10 * t) - * Math.sin( - (t - (ELASTIC_PERIOD / (2 * Math.PI) * Math.asin(1 / ELASTIC_AMPLITUDE))) - * (2 * Math.PI) - / ELASTIC_PERIOD) - + 1); + (ELASTIC_AMPLITUDE + * Math.pow(2, -10 * t) + * Math.sin( + (t - (ELASTIC_PERIOD / (2 * Math.PI) * Math.asin(1 / ELASTIC_AMPLITUDE))) + * (2 * Math.PI) + / ELASTIC_PERIOD) + + 1); } public static float elasticInOut(float t) { if (t < 0.5) { return (float) - (-0.5 - * (Math.pow(2, 10 * (t -= 0.5f)) - * Math.sin((t - (ELASTIC_PERIOD / 4)) * (2 * Math.PI) / ELASTIC_PERIOD))); + (-0.5 + * (Math.pow(2, 10 * (t -= 0.5f)) + * Math.sin((t - (ELASTIC_PERIOD / 4)) * (2 * Math.PI) / ELASTIC_PERIOD))); } return (float) - (Math.pow(2, -10 * (t -= 0.5f)) - * Math.sin((t - (ELASTIC_PERIOD / 4)) * (2 * Math.PI) / ELASTIC_PERIOD) - * 0.5 - + 1); + (Math.pow(2, -10 * (t -= 0.5f)) + * Math.sin((t - (ELASTIC_PERIOD / 4)) * (2 * Math.PI) / ELASTIC_PERIOD) + * 0.5 + + 1); } @FunctionalInterface @@ -205,16 +205,17 @@ public interface FunkinEaseFunction { @FunctionalInterface public interface FunkinEaseStartCallback { - void run(FunkinTween tween); + void run(FlixelTween tween); } @FunctionalInterface public interface FunkinEaseUpdateCallback { - void run(FunkinTween tween); + void run(FlixelTween tween); } @FunctionalInterface public interface FunkinEaseCompleteCallback { - void run(FunkinTween tween); + void run(FlixelTween tween); } + } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelTween.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelTween.java new file mode 100644 index 0000000..36e0430 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelTween.java @@ -0,0 +1,255 @@ +package me.stringdotjar.flixelgdx.tween; + +import com.badlogic.gdx.utils.Pool; +import me.stringdotjar.flixelgdx.tween.settings.FlixelTweenSettings; +import me.stringdotjar.flixelgdx.tween.type.FlixelNumTween; +import me.stringdotjar.flixelgdx.tween.type.FlixelVarTween; +import org.jetbrains.annotations.NotNull; + +/** + * Core class for creating new tweens to add nice and smooth animations. + * + *

Please note that while this isn't an abstract class, it is advised to NOT create instances + * of this class, since it does not implement the actual tweening logic. Instead, you should use one of + * its subclasses, such as {@link FlixelVarTween}, or create your own subclass and add your own functionality. + * + *

The only reason this class is not abstract is to allow pooling of generic tweens when needed to save memory. + */ +public class FlixelTween implements Pool.Poolable { + + /** The global tween manager for the entire game. */ + protected static FlixelTweenManager globalManager = new FlixelTweenManager(); + + /** The settings used for how the tween is handled and calculated (aka how it looks and animates). */ + protected FlixelTweenSettings tweenSettings; + + /** The parent manager that {@code this} tween gets updated in. */ + protected FlixelTweenManager manager; + + /** How far the tween is tweening itself. This is what's used to actually tween the object! */ + protected float scale = 0.0f; + + /** How many seconds has elapsed since {@code this} tween started. */ + protected float secondsSinceStart = 0.0f; + + /** How many times {@code this} tween has updated. */ + protected int executions = 0; + + /** Is {@code this} tween currently paused? */ + public boolean paused = false; + + /** Is {@code this} tween currently active? */ + public boolean running = false; + + /** Is {@code this} tween finished tweening? */ + public boolean finished = false; + + /** Is {@code this} tween tweening backwards? */ + protected boolean backward = false; + + /** Default constructor for pooling purposes. */ + protected FlixelTween() {} + + protected FlixelTween(FlixelTweenSettings tweenSettings) { + this.tweenSettings = tweenSettings; + } + + /** + * Creates a new tween with the provided settings and starts it in the global tween manager. + * + * @param object The object to tween its values. + * @param tweenSettings The settings that configure and determine how the tween should animate. + * @param updateCallback Callback function for updating the objects values when the tween updates. + * @return The newly created and started tween. + */ + public static FlixelTween tween(Object object, FlixelTweenSettings tweenSettings, FlixelVarTween.FunkinVarTweenUpdateCallback updateCallback) { + return new FlixelVarTween(object, tweenSettings, updateCallback) + .setManager(globalManager) + .start(); + } + + public static FlixelTween num(float from, float to, FlixelTweenSettings tweenSettings, FlixelNumTween.FlixelNumTweenUpdateCallback updateCallback) { + return new FlixelNumTween(from, to, tweenSettings, updateCallback) + .setManager(globalManager) + .start(); + } + + /** + * Starts {@code this} tween and resets every value to its initial state. + * + * @return {@code this} tween. + */ + public FlixelTween start() { + resetBasic(); + running = true; + finished = false; + return this; + } + + /** + * Updates {@code this} tween by the given delta time. + * + * @param delta How much time has passed since the last update. + */ + public void update(float delta) { + if (paused || finished || !running) { + return; + } + if (tweenSettings == null) { + return; + } + + var ease = tweenSettings.getEase(); + var duration = tweenSettings.getDuration(); + var onStart = tweenSettings.getOnStart(); + var onUpdate = tweenSettings.getOnUpdate(); + var onComplete = tweenSettings.getOnComplete(); + var framerate = tweenSettings.getFramerate(); + + float preTick = secondsSinceStart; + secondsSinceStart += delta; + float postTick = secondsSinceStart; + + float delay = (executions > 0) ? tweenSettings.getLoopDelay() : tweenSettings.getStartDelay(); + if (secondsSinceStart < delay) { + return; + } + + if (framerate > 0) { + preTick = Math.round(preTick * framerate) / framerate; + postTick = Math.round(postTick * framerate) / framerate; + } + + scale = Math.max((postTick - delay), 0.0f) / duration; + if (ease != null) { + scale = ease.compute(scale); + } + if (backward) { + scale = 1 - scale; + } + if (secondsSinceStart >= delay && !running) { + running = true; + if (onStart != null) { + onStart.run(this); + } + } + + // Check if the tween has finished. + if (secondsSinceStart >= duration + delay) { + scale = (backward) ? 0 : 1; + finished = true; + if (onComplete != null) { + onComplete.run(this); + } + } else { + if (postTick > preTick && onUpdate != null) { + onUpdate.run(this); + } + } + } + + /** + * Resumes {@code this} tween if it was previously paused. + * + * @return {@code this} tween. + */ + public FlixelTween resume() { + paused = false; + return this; + } + + /** + * Pauses {@code this} tween, stopping it from updating until resumed. + * + * @return {@code this} tween. + */ + public FlixelTween pause() { + paused = true; + running = false; + return this; + } + + /** + * Stops {@code this} tween. Note that this does not remove the tween from the active tweens in + * its manager. + * + * @return {@code this} tween. + */ + public FlixelTween stop() { + running = false; + return this; + } + + /** + * Cancels {@code this} tween and automatically defaults its values, removing it from its manager by default. + * + * @return {@code this} tween. + */ + public FlixelTween cancel() { + return cancel(true); + } + + /** + * Cancels {@code this} tween and automatically defaults its values. + * + * @param remove Should {@code this} tween be removed from its manager? + * @return {@code this} tween. + */ + public FlixelTween cancel(boolean remove) { + reset(); + if (remove) { + manager.getTweenPool().free(this); + } + return this; + } + + @Override + public void reset() { + resetBasic(); + manager = null; + } + + /** + * Resets only the basic values of {@code this} tween without removing any references to the + * object, its settings or its callback function. + */ + public void resetBasic() { + scale = 0.0f; + secondsSinceStart = 0.0f; + executions = 0; + paused = false; + running = false; + finished = false; + backward = false; + } + + public FlixelTweenSettings getTweenSettings() { + return tweenSettings; + } + + public FlixelTween setTweenSettings(@NotNull FlixelTweenSettings tweenSettings) { + this.tweenSettings = tweenSettings; + return this; + } + + public static FlixelTweenManager getGlobalManager() { + return globalManager; + } + + public FlixelTweenManager getManager() { + return manager; + } + + public FlixelTween setManager(FlixelTweenManager newManager) { + if (newManager != null) { + if (manager != null) { + int index = manager.getActiveTweens().indexOf(this, true); + manager.getActiveTweens().removeIndex(index); + manager.getTweenPool().free(this); + } + manager = newManager; + manager.getActiveTweens().add(this); + } + return this; + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelTweenManager.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelTweenManager.java new file mode 100644 index 0000000..813ca42 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/FlixelTweenManager.java @@ -0,0 +1,60 @@ +package me.stringdotjar.flixelgdx.tween; + +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Pool; + +/** Manager class for handling a list of active {@link FlixelTween}s. */ +public class FlixelTweenManager { + + /** Array where all current active tweens are stored. */ + protected final Array activeTweens = new Array<>(FlixelTween.class); + + /** A pool where all unused tweens are stored to preserve memory. */ + protected final Pool tweenPool = new Pool<>() { + @Override + protected FlixelTween newObject() { + return new FlixelTween(); + } + }; + + /** + * Updates all active tweens that are stored and updated in {@code this} manager. + * + * @param delta The amount of time that has passed since the last frame. + */ + public void update(float delta) { + for (int i = activeTweens.size - 1; i >= 0; i--) { + var tween = activeTweens.items[i]; + if (tween == null) { + continue; + } + tween.update(delta); + + if (tween.finished) { + if (tween.manager != this) { + continue; + } + var settings = tween.getTweenSettings(); + if (settings == null) { + continue; + } + + switch (settings.getType()) { + case ONESHOT -> { + activeTweens.removeIndex(i); + tweenPool.free(tween); + } + case PERSIST -> {} // Do nothing, let it be. + } + } + } + } + + public Array getActiveTweens() { + return activeTweens; + } + + public Pool getTweenPool() { + return tweenPool; + } +} diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenSettings.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/settings/FlixelTweenSettings.java similarity index 58% rename from core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenSettings.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/settings/FlixelTweenSettings.java index 2f2ccaf..1f202bb 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenSettings.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/settings/FlixelTweenSettings.java @@ -1,6 +1,7 @@ -package me.stringfromjava.funkin.tween.settings; +package me.stringdotjar.flixelgdx.tween.settings; -import me.stringfromjava.funkin.tween.FunkinEase; +import me.stringdotjar.flixelgdx.tween.FlixelTween; +import me.stringdotjar.flixelgdx.tween.FlixelEase; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -10,39 +11,39 @@ /** * Class for holding basic data, containing configurations to be used on a {@link - * me.stringfromjava.funkin.tween.FunkinTween}. + * FlixelTween}. */ -public class FunkinTweenSettings { +public class FlixelTweenSettings { private float duration; private float startDelay; private float loopDelay; private float framerate; - private FunkinTweenType type; - private FunkinEase.FunkinEaseFunction ease; - private FunkinEase.FunkinEaseStartCallback onStart; - private FunkinEase.FunkinEaseUpdateCallback onUpdate; - private FunkinEase.FunkinEaseCompleteCallback onComplete; + private FlixelTweenType type; + private FlixelEase.FunkinEaseFunction ease; + private FlixelEase.FunkinEaseStartCallback onStart; + private FlixelEase.FunkinEaseUpdateCallback onUpdate; + private FlixelEase.FunkinEaseCompleteCallback onComplete; private ArrayList goals; - public FunkinTweenSettings() { - this(FunkinTweenType.ONESHOT, FunkinEase::linear); + public FlixelTweenSettings() { + this(FlixelTweenType.ONESHOT, FlixelEase::linear); } /** * @param type The type of tween it should be. */ - public FunkinTweenSettings(@NotNull FunkinTweenType type) { - this(type, FunkinEase::linear); + public FlixelTweenSettings(@NotNull FlixelTweenType type) { + this(type, FlixelEase::linear); } /** * @param type The type of tween it should be. * @param ease The easer function the tween should use (aka how it should be animated). */ - public FunkinTweenSettings( - @NotNull FunkinTweenType type, - @Nullable FunkinEase.FunkinEaseFunction ease) { + public FlixelTweenSettings( + @NotNull FlixelTweenType type, + @Nullable FlixelEase.FunkinEaseFunction ease) { this.duration = 1.0f; this.startDelay = 0.0f; this.loopDelay = 0.0f; @@ -58,11 +59,13 @@ public FunkinTweenSettings( /** * Adds a new goal to tween an objects value to. * + *

Note that this is only used on a {@link me.stringdotjar.flixelgdx.tween.type.FlixelVarTween}. + * * @param field The field to tween. * @param value The value to tween the field to. * @return {@code this} tween settings object for chaining. */ - public FunkinTweenSettings addGoal(String field, float value) { + public FlixelTweenSettings addGoal(String field, float value) { goals.add(new FunkinTweenGoal(field, value)); return this; } @@ -73,7 +76,7 @@ public FunkinTweenSettings addGoal(String field, float value) { * @param duration The new value to set. * @return {@code this} tween settings object for chaining. */ - public FunkinTweenSettings setDuration(float duration) { + public FlixelTweenSettings setDuration(float duration) { this.duration = duration; return this; } @@ -82,23 +85,23 @@ public float getDuration() { return duration; } - public FunkinTweenType getType() { + public FlixelTweenType getType() { return type; } - public FunkinEase.FunkinEaseFunction getEase() { + public FlixelEase.FunkinEaseFunction getEase() { return ease; } - public FunkinEase.FunkinEaseStartCallback getOnStart() { + public FlixelEase.FunkinEaseStartCallback getOnStart() { return onStart; } - public FunkinEase.FunkinEaseUpdateCallback getOnUpdate() { + public FlixelEase.FunkinEaseUpdateCallback getOnUpdate() { return onUpdate; } - public FunkinEase.FunkinEaseCompleteCallback getOnComplete() { + public FlixelEase.FunkinEaseCompleteCallback getOnComplete() { return onComplete; } @@ -135,7 +138,7 @@ public float getFramerate() { return framerate; } - public FunkinTweenSettings setEase(FunkinEase.FunkinEaseFunction ease) { + public FlixelTweenSettings setEase(FlixelEase.FunkinEaseFunction ease) { this.ease = ease; return this; } @@ -144,21 +147,41 @@ public void clearGoals() { goals.clear(); } - public FunkinTweenSettings setStartDelay(float startDelay) { + public FlixelTweenSettings setStartDelay(float startDelay) { this.startDelay = startDelay; return this; } - public FunkinTweenSettings setLoopDelay(float loopDelay) { + public FlixelTweenSettings setLoopDelay(float loopDelay) { this.loopDelay = loopDelay; return this; } - public FunkinTweenSettings setFramerate(float framerate) { + public FlixelTweenSettings setFramerate(float framerate) { this.framerate = framerate; return this; } + public FlixelTweenSettings setType(@NotNull FlixelTweenType type) { + this.type = type; + return this; + } + + public FlixelTweenSettings setOnStart(FlixelEase.FunkinEaseStartCallback onStart) { + this.onStart = onStart; + return this; + } + + public FlixelTweenSettings setOnUpdate(FlixelEase.FunkinEaseUpdateCallback onUpdate) { + this.onUpdate = onUpdate; + return this; + } + + public FlixelTweenSettings setOnComplete(FlixelEase.FunkinEaseCompleteCallback onComplete) { + this.onComplete = onComplete; + return this; + } + /** * A record containing basic info for a tween goal (aka a field to tween a numeric value to). * diff --git a/core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenType.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/settings/FlixelTweenType.java similarity index 84% rename from core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenType.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/settings/FlixelTweenType.java index e5bf801..e2a2012 100644 --- a/core/src/main/java/me/stringfromjava/funkin/tween/settings/FunkinTweenType.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/settings/FlixelTweenType.java @@ -1,7 +1,7 @@ -package me.stringfromjava.funkin.tween.settings; +package me.stringdotjar.flixelgdx.tween.settings; /** Enum containing all different tween types that can determine */ -public enum FunkinTweenType { +public enum FlixelTweenType { /** Will stop and remove itself from the manager when it finishes. */ ONESHOT, diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/type/FlixelNumTween.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/type/FlixelNumTween.java new file mode 100644 index 0000000..40cc7d1 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/type/FlixelNumTween.java @@ -0,0 +1,73 @@ +package me.stringdotjar.flixelgdx.tween.type; + +import me.stringdotjar.flixelgdx.tween.FlixelTween; +import me.stringdotjar.flixelgdx.tween.settings.FlixelTweenSettings; + +/** + * Tween type that tweens one numerical value to another. + */ +public class FlixelNumTween extends FlixelTween { + + /** The starting value of the tween. */ + protected float start; + + /** The target value of the tween. */ + protected float end; + + /** The current value of the tween. */ + protected float value; + + /** The range between the start and end values. */ + protected float range; + + /** Callback function for updating the value when the tween updates. */ + protected FlixelNumTweenUpdateCallback updateCallback; + + /** + * Constructs a new numerical tween, which will tween a simple starting number to an ending value. + * + * @param start The starting value. + * @param end The ending value. + * @param settings The settings that configure and determine how the tween should animate. + * @param updateCallback Callback function for updating any variable that needs the current value when the tween updates. + */ + public FlixelNumTween(float start, float end, FlixelTweenSettings settings, FlixelNumTweenUpdateCallback updateCallback) { + super(settings); + this.start = start; + this.end = end; + this.value = start; + this.range = end - start; + this.updateCallback = updateCallback; + } + + @Override + public void update(float delta) { + super.update(delta); + + if (paused || finished || !running) { + return; + } + if (updateCallback == null) { + return; + } + + value = start + range * scale; + + updateCallback.update(value); + } + + /** + * Functional interface for updating the numerical value when the tween updates. This is for updating any + * variable that needs the current value of the tween. + */ + @FunctionalInterface + public interface FlixelNumTweenUpdateCallback { + + /** + * A callback method that is called when the tween updates its value during its tweening (or animating) process. + * + * @param value The new current value of the numerical tween during the animation. + */ + void update(float value); + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/type/FlixelVarTween.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/type/FlixelVarTween.java new file mode 100644 index 0000000..3f14829 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/tween/type/FlixelVarTween.java @@ -0,0 +1,165 @@ +package me.stringdotjar.flixelgdx.tween.type; + +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.flixelgdx.tween.FlixelTween; +import me.stringdotjar.flixelgdx.tween.settings.FlixelTweenSettings; +import me.stringdotjar.flixelgdx.util.FlixelConstants; +import me.stringdotjar.flixelgdx.util.FlixelReflectUtil; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Tween type for tweening specific fields on an object using reflection. + */ +public class FlixelVarTween extends FlixelTween { + + /** The object to tween. */ + protected Object object; + + /** The initial values of the fields being tweened. */ + protected final Map initialValues = new HashMap<>(); + + /** + * Cache of the fields being tweened for faster access so they aren't accessed every time the + * {@link #start()} method is called. + */ + protected Field[] fieldsCache = null; + + /** The update callback for {@code this} tween to change the objects values every update. */ + protected FlixelVarTween.FunkinVarTweenUpdateCallback updateCallback; + + /** + * Constructs a new object tween using reflection. + * + *

Note that this does NOT add the tween to the global manager, it just assigns its main + * values. That's it. If you wish to create a tween to automatically start working, you might want + * to see {@link FlixelTween#tween(Object object, FlixelTweenSettings tweenSettings, + * FunkinVarTweenUpdateCallback updateCallback)}. + * + * @param object The object to tween values. + * @param settings The settings that configure and determine how the tween should animate and last for. + * @param updateCallback Callback function for updating the objects values when the tween updates. + */ + public FlixelVarTween(Object object, FlixelTweenSettings settings, FunkinVarTweenUpdateCallback updateCallback) { + super(settings); + this.object = object; + this.updateCallback = updateCallback; + } + + @Override + public FlixelTween start() { + super.start(); + + if (tweenSettings == null) { + Flixel.warn("FlixelTween", "No tween settings were provided for the tween."); + return this; + } + var neededFields = tweenSettings.getGoalFields(); + if (neededFields == null || neededFields.isEmpty()) { + Flixel.warn("FlixelTween", "No fields were provided to tween on the object."); + return this; + } + + if (fieldsCache == null) { + fieldsCache = FlixelReflectUtil.getAllFieldsAsArray(object.getClass()); + } + + // Ensure that the fields provided actually exist on the object and are floating point values. + // If there are fields that the tween is trying to tween that don't exist, then throw an error. + Set fieldIds = Arrays.stream(fieldsCache).map(Field::getName).collect(Collectors.toSet()); + for (String neededField : neededFields) { + if (!fieldIds.contains(neededField)) { + String message = "Field \"" + neededField + "\" does not exist on the given object."; + Flixel.error("FlixelTween", message); + throw new RuntimeException(message); + } + } + + for (Field field : fieldsCache) { + try { + String fName = field.getName(); + if (!field.trySetAccessible()) { + continue; + } + if (!neededFields.contains(fName)) { + continue; + } + if (field.getType() != float.class) { + continue; + } + initialValues.put(fName, field.getFloat(object)); + } catch (IllegalAccessException e) { + Flixel.error(FlixelConstants.System.LOG_TAG, "Could not access field \"" + field.getName() + "\".", e); + } + } + return this; + } + + @Override + public void update(float delta) { + super.update(delta); + + if (object == null) { + return; + } + if (paused || finished) { + return; + } + if (updateCallback == null) { + return; + } + + // Update the object's fields based on the tween progress. + var newValues = new HashMap(); + var goals = tweenSettings.getGoalFields(); + for (String field : goals) { + FlixelTweenSettings.FunkinTweenGoal goal = tweenSettings.getGoal(field); + if (goal == null) { + continue; + } + if (initialValues.isEmpty()) { + continue; + } + if (!initialValues.containsKey(field)) { + continue; + } + float startValue = initialValues.get(field); + float goalValue = goal.value(); + float newValue = startValue + (goalValue - startValue) * scale; + newValues.put(field, newValue); + } + if (!newValues.isEmpty() && initialValues.keySet().containsAll(newValues.keySet())) { + updateCallback.update(newValues); + } + } + + @Override + public void reset() { + super.reset(); + fieldsCache = null; + } + + @Override + public void resetBasic() { + super.resetBasic(); + initialValues.clear(); + } + + /** Callback interface for changing an objects values when a var tween updates its values. */ + @FunctionalInterface + public interface FunkinVarTweenUpdateCallback { + + /** + * A callback method that is called when the tween updates its values during its tweening (or + * animating) process. + * + * @param values The new current values of the fields being tweened during the animation. + */ + void update(Map values); + } +} diff --git a/core/src/main/java/me/stringfromjava/funkin/util/Constants.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java similarity index 56% rename from core/src/main/java/me/stringfromjava/funkin/util/Constants.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java index 7eade3f..149dda7 100644 --- a/core/src/main/java/me/stringfromjava/funkin/util/Constants.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java @@ -1,31 +1,31 @@ -package me.stringfromjava.funkin.util; +package me.stringdotjar.flixelgdx.util; /** * Core class for holding static classes with values which do not change. It is NOT RECOMMENDED to * store things like {@link java.util.ArrayList}'s, as they can still be modified even if they are * initialized as {@code final}. */ -public final class Constants { +public final class FlixelConstants { - /** Static subclass for holding values for components such as the window's width and height. */ - public static final class Display { + /** + * Stores constants for things related to the backend of Flixel. This includes components like + * logging, folder paths, etc. + */ + public static final class System { /** - * How wide the window's viewport is in pixels. This also affects how wide the window is when - * the game first starts up. + * The default and globally recognized default tag for logs that are outputted inside the + * console. */ - public static final int WINDOW_WIDTH = 1280; + public static final String LOG_TAG = "Flixel"; - /** - * How tall the window's viewport is in pixels. This also affects how tall the window is when - * the game first starts up. - */ - public static final int WINDOW_HEIGHT = 720; - - private Display() {} + private System() {} } - public static final class Colors { + /** + * Holds ASCII color code constants for text in the console. + */ + public static final class AsciiCodes { public static final String RESET = "\u001B[0m"; public static final String BOLD = "\033[0;1m"; @@ -40,8 +40,6 @@ public static final class Colors { public static final String CYAN = "\u001B[36m"; public static final String WHITE = "\u001B[37m"; - private Colors() {} + private AsciiCodes() {} } - - private Constants() {} } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelMathUtil.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelMathUtil.java new file mode 100644 index 0000000..bd28994 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelMathUtil.java @@ -0,0 +1,21 @@ +package me.stringdotjar.flixelgdx.util; + +/** + * Utility class for various math related functions used in FlixelGDX. + */ +public final class FlixelMathUtil { + + /** + * Rounds a float value to a specified number of decimal places. + * + * @param value The float value to round. + * @param decimalPlaces The number of decimal places to round to. + * @return The rounded float value. + */ + public static float round(float value, int decimalPlaces) { + float scale = (float) Math.pow(10, decimalPlaces); + return Math.round(value * scale) / scale; + } + + private FlixelMathUtil() {} +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java new file mode 100644 index 0000000..2c0ec5c --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java @@ -0,0 +1,84 @@ +package me.stringdotjar.flixelgdx.util; + +import me.stringdotjar.flixelgdx.Flixel; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Backend utility class for obtaining and manipulating fields on objects through the usage of Java + * reflection. + */ +public class FlixelReflectUtil { + + /** + * Checks if a field exists on a given object. + * + * @param target The object to verify the existence of the given field to check. + * @param fieldName The name of the + * @return If the field exists on the given object. + */ + public static boolean hasField(Object target, String fieldName) { + AtomicBoolean exists = new AtomicBoolean(); + getAllFields(target.getClass()) + .forEach(field -> { + if (field.getName().equals(fieldName)) { + exists.set(true); + } + exists.set(false); + }); + return exists.get(); + } + + /** + * Obtains all fields of a class, including the master types above it all the way to {@link + * Object}. + * + * @param type A class literal to obtain the fields from. + * @return All fields from itself and its master classes above it. + */ + public static List getAllFields(Class type) { + List fields = new ArrayList<>(); + for (Class c = type; c != null; c = c.getSuperclass()) { + fields.addAll(Arrays.asList(c.getDeclaredFields())); + } + return fields; + } + + /** + * Obtains all fields of a class, including the master types above it all the way to {@link + * Object}. Obvious to the name, this version returns an array instead of a list unlike its + * similar function . + * + * @param type A class literal to obtain the fields from. + * @return All fields from itself and its master classes above it. + */ + public static Field[] getAllFieldsAsArray(Class type) { + return getAllFields(type).toArray(new Field[0]); + } + + /** + * Checks if a class of a certain package is final. + * + * @param classPath The package definition of the class to check if final. An example could be + * {@code "me.stringdotjar.flixelgdx.Flixel"}. + * @return If the class provided is final. If there was an exception caught, then {@code false} is + * automatically returned. + */ + public static boolean isClassFinal(String classPath) { + try { + Class clazz = Class.forName(classPath); + // Uses the java.lang.reflect.Modifier utility. + return Modifier.isFinal(clazz.getModifiers()); + } catch (ClassNotFoundException e) { + // Treat non-existent class as non-final for safe binding, + // though the user will hit an error later. + Flixel.error(FlixelConstants.System.LOG_TAG, "Failed to check if a class was final.", e); + return false; + } + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java new file mode 100644 index 0000000..4d8a48c --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java @@ -0,0 +1,56 @@ +package me.stringdotjar.flixelgdx.util; + +/** + * Utility class for handling operation related to the runtime environment, including OS detection, + * extracting runtime information, obtaining information from exceptions, and other related tasks. + */ +public final class FlixelRuntimeUtil { + + /** + * Obtains a string representation of where an exception was thrown from, including the class, + * method, file, and line number. + * + * @param exception The exception to obtain the location from. + * @return A string representation of where the exception was thrown from. + */ + public static String getExceptionLocation(Throwable exception) { + if (exception == null) { + return "Unknown Location"; + } + StackTraceElement[] stackTrace = exception.getStackTrace(); + if (stackTrace.length == 0) { + return "Unknown Location"; + } + StackTraceElement element = stackTrace[0]; + return "FILE=" + + element.getFileName() + + ", CLASS=" + + element.getClassName() + + ", METHOD=" + + element.getMethodName() + + "(), LINE=" + + element.getLineNumber(); + } + + /** + * Obtains a full detailed message from an exception, including its type, location, and stack trace. + * + * @param exception The exception to obtain the message from. + * @return A full detailed message from the exception. + */ + public static String getFullExceptionMessage(Throwable exception) { + if (exception == null) { + return "No exception provided."; + } + StringBuilder messageBuilder = new StringBuilder(); + messageBuilder.append("Exception: ").append(exception).append("\n"); + messageBuilder.append("Location: ").append(getExceptionLocation(exception)).append("\n"); + messageBuilder.append("Stack Trace:\n"); + for (StackTraceElement element : exception.getStackTrace()) { + messageBuilder.append("\tat ").append(element.toString()).append("\n"); + } + return messageBuilder.toString(); + } + + private FlixelRuntimeUtil() {} +} diff --git a/funkin/build.gradle b/funkin/build.gradle new file mode 100644 index 0000000..b1991df --- /dev/null +++ b/funkin/build.gradle @@ -0,0 +1,43 @@ +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' +eclipse.project.name = appName + '-funkin' + +dependencies { + api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion" + api "com.badlogicgames.gdx:gdx:$gdxVersion" + api "com.github.tommyettinger:anim8-gdx:$anim8Version" + api "com.github.tommyettinger:libgdx-utils:$utilsVersion" + api "io.github.libktx:ktx-freetype:$ktxVersion" + api "games.rednblack.miniaudio:miniaudio:$miniaudioVersion" + api project(":flixelgdx") + api project(":polyverse") + + implementation "org.jetbrains:annotations:26.0.2-1" + + if (enableGraalNative == 'true') { + implementation "io.github.berstanio:gdx-svmhelper-annotations:$graalHelperVersion" + } +} + +tasks.register('exportModSDK', Copy) { + group = "modding" + description = "Copies all API JARs needed for modding into the assets folder." + + into "$rootProject.projectDir/assets/sdk/api" + + from(tasks.named('jar')) { + rename { "funkin-api.jar" } + } + + // Include every external dependency. + from(configurations.runtimeClasspath) { + // Exclude native files, since modders only need the Java interfaces. + exclude "**/*.dll", "**/*.so", "**/*.dylib", "**/*.jnilib" + } +} + +// Make sure the assets are ready before the game runs. +if (project.name == 'lwjgl3') { + tasks.named('run') { + dependsOn(rootProject.tasks.named('processAssets')) + } +} diff --git a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java new file mode 100644 index 0000000..cfdce50 --- /dev/null +++ b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java @@ -0,0 +1,71 @@ +package me.stringdotjar.funkin; + +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.flixelgdx.FlixelGame; +import me.stringdotjar.flixelgdx.backend.FlixelPaths; +import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.polyverse.Polyverse; +import me.stringdotjar.polyverse.script.type.Script; +import me.stringdotjar.polyverse.script.type.SystemScript; + +/** + * The main Funkin' game class that initializes and runs the game. + */ +public class FunkinGame extends FlixelGame { + + private float lastVolume = 1.0f; + + public FunkinGame(String title, int width, int height, FlixelScreen initialScreen) { + super(title, width, height, initialScreen); + } + + @Override + public void create() { + super.create(); + configurePolyverse(); // Scripting and modding support. + } + + @Override + public void render() { + super.render(); + Polyverse.forAllScripts(script -> script.onRender(Flixel.getDelta())); + } + + @Override + public void dispose() { + super.dispose(); + Polyverse.forAllScripts(Script::onDispose); + } + + @Override + public void onWindowFocused() { + super.onWindowFocused(); + Flixel.setMasterVolume(lastVolume); + Polyverse.forEachScript(SystemScript.class, SystemScript::onWindowFocused); + } + + @Override + public void onWindowUnfocused() { + super.onWindowUnfocused(); + lastVolume = Flixel.getMasterVolume(); + Flixel.setMasterVolume(0.008f); + Polyverse.forEachScript(SystemScript.class, SystemScript::onWindowUnfocused); + } + + @Override + public void onWindowMinimized(boolean iconified) { + super.onWindowMinimized(iconified); + lastVolume = Flixel.getMasterVolume(); + Flixel.setMasterVolume(0); + Polyverse.forEachScript(SystemScript.class, script -> script.onWindowMinimized(iconified)); + } + + private void configurePolyverse() { + // Register Polyverse script types. + Polyverse.registerScriptType(Script.class); // Master type, DO NOT REMOVE THIS! + Polyverse.registerScriptType(SystemScript.class); + + Polyverse.registerScript(FlixelPaths.asset("test.groovy")); + Polyverse.registerScript(FlixelPaths.asset("another_test.groovy")); + } +} diff --git a/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java b/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java new file mode 100644 index 0000000..e90d619 --- /dev/null +++ b/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java @@ -0,0 +1,14 @@ +package me.stringdotjar.funkin; + +import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.funkin.menus.TitleScreen; + +public class InitScreen extends FlixelScreen { + + @Override + public void show() { + super.show(); + Flixel.setScreen(new TitleScreen()); + } +} diff --git a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java new file mode 100644 index 0000000..99c78e2 --- /dev/null +++ b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java @@ -0,0 +1,90 @@ +package me.stringdotjar.funkin.menus; + +import com.badlogic.gdx.Input; +import games.rednblack.miniaudio.MASound; +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.flixelgdx.backend.FlixelPaths; +import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; +import me.stringdotjar.flixelgdx.tween.FlixelEase; +import me.stringdotjar.flixelgdx.tween.FlixelTween; +import me.stringdotjar.flixelgdx.tween.settings.FlixelTweenSettings; +import me.stringdotjar.flixelgdx.tween.settings.FlixelTweenType; + +public class TitleScreen extends FlixelScreen { + + private FlixelSprite logo; + + private FlixelTween tween; + private MASound tickleFight; + + @Override + public void show() { + super.show(); + + var t = FlixelPaths.sharedImageAsset("noteStrumline"); + var xml = FlixelPaths.shared("images/noteStrumline.xml"); + logo = new FlixelSprite().loadSparrowFrames(t, xml); + logo.addAnimationByPrefix("test", "confirmDown", 24, false); + add(logo); + + tickleFight = Flixel.playSound("shared/sounds/tickleFight.ogg"); +// Flixel.playMusic("preload/music/freakyMenu/freakyMenu.ogg", 0.5f); + + FlixelTweenSettings settings = new FlixelTweenSettings() + .addGoal("x", 600) + .addGoal("y", 40) + .addGoal("rotation", 180) + .setDuration(0.7f) + .setEase(FlixelEase::circInOut) + .setType(FlixelTweenType.PERSIST); + tween = FlixelTween.tween(logo, settings, values -> { + logo.setX(values.get("x")); + logo.setY(values.get("y")); + logo.setRotation(values.get("rotation")); + }).stop(); + } + + @Override + public void render(float elapsed) { + super.render(elapsed); + + float speed = 500 * elapsed; + if (Flixel.keyPressed(Input.Keys.W)) { + logo.changeY(speed); + } + if (Flixel.keyPressed(Input.Keys.S)) { + logo.changeY(-speed); + } + if (Flixel.keyPressed(Input.Keys.A)) { + logo.changeX(-speed); + } + if (Flixel.keyPressed(Input.Keys.D)) { + logo.changeX(speed); + } + + if (Flixel.keyJustPressed(Input.Keys.SPACE)) { + logo.playAnimation("test", true); + } + + if (Flixel.keyJustPressed(Input.Keys.T)) { + tween.start(); + } + + if (Flixel.keyJustPressed(Input.Keys.R)) { + tween.reset(); + } + + if (Flixel.keyJustPressed(Input.Keys.Y)) { + if (tween.paused) { + tween.resume(); + } else { + tween.pause(); + } + } + + if (Flixel.keyJustPressed(Input.Keys.Z)) { + tickleFight.play(); + } + } +} diff --git a/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java b/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java new file mode 100644 index 0000000..8fd5196 --- /dev/null +++ b/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java @@ -0,0 +1,26 @@ +package me.stringdotjar.funkin.util; + +/** + * Constants used throughout Funkin'. + */ +public final class FunkinConstants { + + /** + * The default title for the game's window. + */ + public static final String WINDOW_TITLE = "Friday Night Funkin': Java Edition"; + + /** + * How wide the window's viewport is in pixels. This also affects how wide the window is when + * the game first starts up. + */ + public static final int WINDOW_WIDTH = 1280; + + /** + * How tall the window's viewport is in pixels. This also affects how tall the window is when + * the game first starts up. + */ + public static final int WINDOW_HEIGHT = 720; + + private FunkinConstants() {} +} diff --git a/gradle.properties b/gradle.properties index 5f61b4a..d3f3f53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,10 +14,10 @@ org.gradle.logging.level=quiet anim8Version=0.5.3 ktxVersion=1.13.1-rc1 utilsVersion=0.13.7 -universalTweenVersion=6.3.3 graalHelperVersion=2.0.1 android.useAndroidX=true android.enableR8.fullMode=false enableGraalNative=false gdxVersion=1.13.1 -projectVersion=1.0.0-ALPHA +miniaudioVersion=0.7 +projectVersion=0.1.0-ALPHA diff --git a/lwjgl3/build.gradle b/lwjgl3/build.gradle index 37d3d2e..2038bf9 100644 --- a/lwjgl3/build.gradle +++ b/lwjgl3/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { classpath "io.github.fourlastor:construo:1.7.1" - if(enableGraalNative == 'true') { + if (enableGraalNative == 'true') { classpath "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.28" } } @@ -14,29 +14,27 @@ plugins { } apply plugin: 'io.github.fourlastor.construo' - -import io.github.fourlastor.construo.Target - -sourceSets.main.resources.srcDirs += [ rootProject.file('assets').path ] -mainClassName = 'me.stringfromjava.funkin.lwjgl3.Lwjgl3Launcher' +sourceSets.main.resources.srcDirs += [rootProject.file('assets').path] +mainClassName = 'me.stringdotjar.funkin.lwjgl3.Lwjgl3Launcher' application.setMainClass(mainClassName) eclipse.project.name = appName + '-lwjgl3' java.sourceCompatibility = 17 java.targetCompatibility = 17 if (JavaVersion.current().isJava9Compatible()) { - compileJava.options.release.set(17) + compileJava.options.release.set(17) } dependencies { + api "games.rednblack.miniaudio:gdx-miniaudio-platform:$miniaudioVersion:natives-desktop" implementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion" implementation "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop" implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" - implementation project(':core') + implementation project(':funkin') + implementation project(":flixelgdx") - if(enableGraalNative == 'true') { + if (enableGraalNative == 'true') { implementation "io.github.berstanio:gdx-svmhelper-backend-lwjgl3:$graalHelperVersion" - - } + } } def os = System.properties['os.name'].toLowerCase() @@ -113,47 +111,6 @@ tasks.register("jarWin") { } } -construo { - // name of the executable - name.set(appName) - // human-readable name, used for example in the `.app` name for macOS - humanName.set(appName) - // Optional, defaults to project version property - version.set("$projectVersion") - - targets.configure { - register("linuxX64", Target.Linux) { - architecture.set(Target.Architecture.X86_64) - jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_linux_hotspot_17.0.15_6.tar.gz") - // Linux does not currently have a way to set the icon on the executable - } - register("macM1", Target.MacOs) { - architecture.set(Target.Architecture.AARCH64) - jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.15_6.tar.gz") - // macOS needs an identifier - identifier.set("me.stringfromjava.funkin." + appName) - // Optional: icon for macOS, as an ICNS file - macIcon.set(project.file("icons/logo.icns")) - } - register("macX64", Target.MacOs) { - architecture.set(Target.Architecture.X86_64) - jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_mac_hotspot_17.0.15_6.tar.gz") - // macOS needs an identifier - identifier.set("me.stringfromjava.funkin." + appName) - // Optional: icon for macOS, as an ICNS file - macIcon.set(project.file("icons/logo.icns")) - } - register("winX64", Target.Windows) { - architecture.set(Target.Architecture.X86_64) - // Optional: icon for Windows, as a PNG - icon.set(project.file("icons/logo.png")) - jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_windows_hotspot_17.0.15_6.zip") - // Uncomment the next line to show a console when the game runs, to print messages. - //useConsole.set(true) - } - } -} - // Equivalent to the jar task; here for compatibility with gdx-setup. tasks.register('dist') { dependsOn 'jar' @@ -176,6 +133,6 @@ distributions { startScripts.dependsOn(':lwjgl3:jar') startScripts.classpath = project.tasks.jar.outputs.files -if(enableGraalNative == 'true') { +if (enableGraalNative == 'true') { apply from: file("nativeimage.gradle") } diff --git a/lwjgl3/src/main/java/me/stringfromjava/funkin/lwjgl3/Lwjgl3Launcher.java b/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java similarity index 68% rename from lwjgl3/src/main/java/me/stringfromjava/funkin/lwjgl3/Lwjgl3Launcher.java rename to lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java index a5968d9..7222c22 100644 --- a/lwjgl3/src/main/java/me/stringfromjava/funkin/lwjgl3/Lwjgl3Launcher.java +++ b/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java @@ -1,11 +1,12 @@ -package me.stringfromjava.funkin.lwjgl3; +package me.stringdotjar.funkin.lwjgl3; import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; import com.badlogic.gdx.backends.lwjgl3.Lwjgl3WindowAdapter; -import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.FunkinGame; -import me.stringfromjava.funkin.util.Constants; +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.funkin.FunkinGame; +import me.stringdotjar.funkin.InitScreen; +import me.stringdotjar.funkin.util.FunkinConstants; /** Launches the desktop (LWJGL3) application. */ public class Lwjgl3Launcher { @@ -18,26 +19,30 @@ public static void main(String[] args) { } private static void createApplication() { - FunkinGame game = new FunkinGame(); - Funkin.initialize(game); // This is VERY important to do before creating the application! - new Lwjgl3Application(game, createWindowConfiguration()); + FunkinGame game = new FunkinGame( + FunkinConstants.WINDOW_TITLE, + FunkinConstants.WINDOW_WIDTH, + FunkinConstants.WINDOW_HEIGHT, + new InitScreen() + ); + Flixel.initialize(game); // This is VERY important to do before creating the application! + var size = game.getWindowSize(); + new Lwjgl3Application(game, createWindowConfiguration(game.getTitle(), (int) size.x, (int) size.y)); } - private static Lwjgl3ApplicationConfiguration createWindowConfiguration() { + private static Lwjgl3ApplicationConfiguration createWindowConfiguration(String title, int width, int height) { Lwjgl3ApplicationConfiguration configuration = new Lwjgl3ApplicationConfiguration(); - configuration.setTitle("Friday Night Funkin': Java Edition"); + configuration.setTitle(title); // Vsync limits the frames per second to what your hardware can display, and helps eliminate // screen tearing. This setting doesn't always work on Linux, so the line after is a safeguard. configuration.useVsync(true); // Limits FPS to the refresh rate of the currently active monitor, plus 1 to try to match - // fractional - // refresh rates. The Vsync setting above should limit the actual FPS to match the monitor. + // fractional refresh rates. The Vsync setting above should limit the actual FPS to match the monitor. configuration.setForegroundFPS(Lwjgl3ApplicationConfiguration.getDisplayMode().refreshRate + 1); // If you remove the above line and set Vsync to false, you can get unlimited FPS, which can be // useful for testing performance, but can also be very stressful to some hardware. - // You may also need to configure GPU drivers to fully disable Vsync; this can cause screen - // tearing. - configuration.setWindowedMode(Constants.Display.WINDOW_WIDTH, Constants.Display.WINDOW_HEIGHT); + // You may also need to configure GPU drivers to fully disable Vsync; this can cause screen tearing. + configuration.setWindowedMode(width, height); // You can change these files; they are in lwjgl3/src/main/resources/ . // They can also be loaded from the root of assets/ . configuration.setWindowIcon("icon128.png", "icon64.png", "icon32.png", "icon16.png"); @@ -49,19 +54,19 @@ private static Lwjgl3ApplicationConfiguration createWindowConfiguration() { @Override public void focusGained() { super.focusGained(); - Funkin.getGame().onWindowFocused(); + Flixel.getGame().onWindowFocused(); } @Override public void focusLost() { super.focusLost(); - Funkin.getGame().onWindowUnfocused(); + Flixel.getGame().onWindowUnfocused(); } @Override public void iconified(boolean isIconified) { super.iconified(isIconified); - Funkin.getGame().onWindowMinimized(isIconified); + Flixel.getGame().onWindowMinimized(isIconified); } }); diff --git a/lwjgl3/src/main/java/me/stringfromjava/funkin/lwjgl3/StartupHelper.java b/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/StartupHelper.java similarity index 99% rename from lwjgl3/src/main/java/me/stringfromjava/funkin/lwjgl3/StartupHelper.java rename to lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/StartupHelper.java index 0a33663..b6ae7f1 100644 --- a/lwjgl3/src/main/java/me/stringfromjava/funkin/lwjgl3/StartupHelper.java +++ b/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/StartupHelper.java @@ -13,7 +13,7 @@ * limitations under the License. */ // Note, the above license and copyright applies to this file only. -package me.stringfromjava.funkin.lwjgl3; +package me.stringdotjar.funkin.lwjgl3; import com.badlogic.gdx.Version; import com.badlogic.gdx.backends.lwjgl3.Lwjgl3NativesLoader; diff --git a/polyverse/build.gradle b/polyverse/build.gradle new file mode 100644 index 0000000..cc873dd --- /dev/null +++ b/polyverse/build.gradle @@ -0,0 +1,9 @@ +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' +eclipse.project.name = appName + '-polyverse' + +dependencies { + api project(":flixelgdx") + + implementation "org.apache.groovy:groovy:5.0.3" + implementation "org.jetbrains:annotations:26.0.2-1" +} diff --git a/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java b/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java similarity index 72% rename from core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java rename to polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java index e9759fe..a8383af 100644 --- a/core/src/main/java/me/stringfromjava/funkin/polyverse/Polyverse.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java @@ -1,9 +1,9 @@ -package me.stringfromjava.funkin.polyverse; +package me.stringdotjar.polyverse; import com.badlogic.gdx.files.FileHandle; import groovy.lang.GroovyClassLoader; -import me.stringfromjava.funkin.Funkin; -import me.stringfromjava.funkin.polyverse.script.type.Script; +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.polyverse.script.type.Script; import java.util.ArrayList; import java.util.Collections; @@ -12,7 +12,7 @@ import java.util.Map; import java.util.function.Consumer; -/** Core manager class for managing the Polyverse mod engine for FNF:JE. */ +/** Core manager class for managing the Polyverse scripting and modding engine. */ public final class Polyverse { /** A map that stores lists of scripts based on their parent class type. */ @@ -46,7 +46,8 @@ public static List getScripts(Class type) { /** * Registers a new script instance into the Polyverse system, auto-detecting its type accordingly. - * Note that this automatically calls the script's {@link Script#onCreate()} method after registration. + * Note that this automatically calls the script's {@link Script#onCreate()} method after + * registration. * * @param handle The {@link FileHandle} that holds where the script is located. */ @@ -80,19 +81,19 @@ public static void registerScript(FileHandle handle) { var typeScripts = scripts.get(mostSpecificType); if (!typeScripts.contains(script)) { typeScripts.add(script); - Funkin.info( - "Polyverse", - "Registered Polyverse script \"" - + script.getClass().getSimpleName() - + "\" of script type \"" - + mostSpecificType.getSimpleName() - + "\"."); + Flixel.info( + "Polyverse", + "Registered Polyverse script \"" + + script.getClass().getSimpleName() + + "\" of script type \"" + + mostSpecificType.getSimpleName() + + "\"."); } } script.onCreate(); } } catch (Exception e) { - Funkin.error("Polyverse", "Failed to load script: " + handle.path()); + Flixel.error("Polyverse", "Failed to load script: " + handle.path(), e); } } @@ -103,43 +104,35 @@ public static void registerScript(FileHandle handle) { * @param action The action to perform on each script. */ public static void forEachScript(Class type, Consumer action) { - List scriptList = getScripts(type); - - // Use a standard for-loop to prevent ConcurrentModificationException - // and ensure we are iterating over the current snapshot of scripts. - for (int i = 0; i < scriptList.size(); i++) { - T script = scriptList.get(i); - if (script != null) { - try { - action.accept(script); - } catch (Exception e) { - Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName()); - } - } - } + executeScriptList(getScripts(type), action); } /** - * Executes an action for all scripts of all types, with error handling. - * Note that this function is NOT recommended to be used frequently due to how generic it is. It's - * mainly intended for cleanup and disposal tasks. If you know the specific type of scripts you - * want to operate on, use {@link #forEachScript(Class, Consumer)} instead. + * Executes an action for all scripts of all types, with error handling. Note that this function + * is NOT recommended to be used frequently due to how generic it is. It's mainly intended for + * cleanup and disposal tasks. If you know the specific type of scripts you want to operate on, + * use {@link #forEachScript(Class, Consumer)} instead. * * @param action The action to perform on each script. */ public static void forAllScripts(Consumer action) { for (Class type : scripts.keySet()) { + executeScriptList(scripts.get(type), action); + } + } + + private static void executeScriptList( + List scriptList, Consumer action) { + // Use a standard for-loop to prevent ConcurrentModificationException + // and ensure we are iterating over the current snapshot of scripts. + for (int i = 0; i < scriptList.size(); i++) { @SuppressWarnings("unchecked") - List scriptList = (List) scripts.get(type); - - for (int i = 0; i < scriptList.size(); i++) { - T script = scriptList.get(i); - if (script != null) { - try { - action.accept(script); - } catch (Exception e) { - Funkin.error("Polyverse", "Error in " + script.getClass().getSimpleName()); - } + T script = (T) scriptList.get(i); + if (script != null) { + try { + action.accept(script); + } catch (Exception e) { + Flixel.error("Polyverse", "Error in " + script.getClass().getSimpleName(), e); } } } diff --git a/core/src/main/java/me/stringfromjava/funkin/polyverse/script/type/Script.java b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java similarity index 91% rename from core/src/main/java/me/stringfromjava/funkin/polyverse/script/type/Script.java rename to polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java index 8effbbb..6b41e28 100644 --- a/core/src/main/java/me/stringfromjava/funkin/polyverse/script/type/Script.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java @@ -1,4 +1,4 @@ -package me.stringfromjava.funkin.polyverse.script.type; +package me.stringdotjar.polyverse.script.type; /** Base class for all Polyverse scripts to extend to. */ public abstract class Script { diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java new file mode 100644 index 0000000..1024e0e --- /dev/null +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java @@ -0,0 +1,17 @@ +package me.stringdotjar.polyverse.script.type; + +public abstract class SystemScript extends Script { + + public SystemScript(String id) { + super(id); + } + + /** Called when the game window gains focus. */ + public void onWindowFocused() {} + + /** Called when the game window loses focus. */ + public void onWindowUnfocused() {} + + /** Called when the game window is minimized. */ + public void onWindowMinimized(boolean iconified) {} +} diff --git a/settings.gradle b/settings.gradle index bf5968d..b3a82d8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,4 +5,4 @@ plugins { // A list of which subprojects to load as part of the same larger project. // You can remove Strings from the list and reload the Gradle project // if you want to temporarily disable a subproject. -include 'lwjgl3', 'android', 'core' +include 'funkin', 'flixelgdx', 'polyverse', 'lwjgl3', 'android'