diff --git a/COMPILING.md b/COMPILING.md index 465f4d7..f29f9d6 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -1,28 +1,23 @@ -# How to Compile FNF:JE +# How to Compile the Polyverse Environment > [!TIP] -> 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)! +> If you want some helpful info about the Gradle tasks (or the frameworks Polyverse uses), consider taking a look at [PROJECT.md](PROJECT.md)! -There are two main ways you can download and compile the game's code: with GitHub Desktop or the terminal. - -In this guide, we'll use the GitHub Desktop method, since it the most user-friendly experience, which provides you a nice UI and does all the hard stuff for you! - -# Prerequisites (for all methods) -- A [GitHub](https://github.com) account to download the game's GitHub repository. +# Prerequisites (*REQUIRED*) +- A [GitHub](https://github.com) account to download the game's GitHub repository (only if you're using GitHub Desktop!). - 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/) - - [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. +- The [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) IDE. +- Some basic knowledge of programming (especially with using 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 Eclipse is great for Java as well, we STRONGLY recommend IntelliJ IDEA, due to how beginner-friendly and integrated it is with Polyverse! # Step-by-Step Guide -1. Visit the official [GitHub Desktop website](https://desktop.github.com/download/) and download the app. +1. Download the JDK and run the installer accordingly. You generally don't need to change any settings + +2. Visit the official [GitHub Desktop website](https://desktop.github.com/download/) and download the app. Make sure to run the installer and + sign in with your GitHub account! > [!TIP] > If you're on Linux, don't worry! You can install a community made version by running the following commands in the terminal: @@ -35,19 +30,29 @@ In this guide, we'll use the GitHub Desktop method, since it the most user-frien > sudo apt update && sudo apt install github-desktop > ``` -2. When GitHub Desktop is done installing, sign in with your GitHub account when prompted to do so. +3. When GitHub Desktop is done installing, sign in with your GitHub account when prompted to do so. + +4. Go back to your browser and (on the official home page for Polyverse's repository), click the green `<> Code` button and select `Open with GitHub Desktop`. + You should see a prompt asking if you want to clone the game's code. + +> [!NOTE] +> If you're on Linux, cloning through the GitHub website and desktop app won't work. Although most Linux distros already have Git installed, +> you can check if you have it by running the command `git --version`. If you get an error saying the command doesn't exist, then you will have to install it onto your system. +> You can install Polyverse through Git with the following command (make sure you're running it in the folder you want it to be located in!): -3. Go back to your browser and (on the official home page for FNF:JE's repository), click the green `<> Code` button and select `Open with GitHub Desktop`. You should see a prompt asking if you want to clone the game's code. +```shell +git clone https://github.com/stringdotjar/Polyverse-Funkin.git optional/path/to/clone/into/ +``` -4. Click the blue `Clone` button and wait for the game's code to download. +5. Click the blue `Clone` button and wait for the game's code to download. > [!TIP] > We recommend putting the game's code in a place where you'll easily remember where it's at, such as your `Documents\GitHub` folder if you're on Windows! -5. Launch your IDE and open the game's folder when prompted to do so. +6. Open IntelliJ IDEA and open the game's folder when prompted to do so. > [!IMPORTANT] > When you open the game's folder, your IDE will most likely start running Gradle tasks. Don't worry, this is normal and expected! -6. When the tasks are finished, you can now run the game's code! You can do so by running the applicable task depending on the platform. +7. When the tasks are finished, you can now run the game's code! You can do so by running the applicable task depending on the platform. - For example, you can run the desktop version by executing the task `lwjgl3:run`. diff --git a/LICENSE.md b/LICENSE.md index 4a3c97d..a986765 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,8 +1,8 @@ -# Friday Night Funkin': Java Edition License +# Polyverse Funkin' License The original game (created by Cameron Taylor and the Funkin' Crew) uses a license called the *Apache License*, which is a permissive, free software license that allows users to freely use, modify, and distribute the software for any purpose, including commercially. -Because Friday Night Funkin': Java Edition (FNF:JE) is based off of the base game, we also adapt the same license they have to maintain consistency. +Because Polyverse Funkin' is an environment made for the base game, we also adapt the same license they have to maintain consistency. ## License diff --git a/PROJECT.md b/PROJECT.md index 6681816..50ad7ee 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -1,11 +1,11 @@ ## This is a [libGDX](https://libgdx.com/) project generated with [gdx-liftoff](https://github.com/libgdx/gdx-liftoff). -# Platforms +# Project Modules -FNF:JE is designed to run on multiple different platforms. Below are the different modules that hold the code for each one. +Polyverse has many different submodules to organize the environment. - `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. +- `flixelgdx`: Custom framework that bridges [HaxeFlixel](https://haxeflixel.com/) to Java 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)! @@ -34,7 +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. +- `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 cdb21a4..36e498d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Friday Night Funkin': Java Edition +# Polyverse Funkin' -Welcome to the official GitHub repository for the Java edition of Friday Night Funkin'! +Welcome to the official GitHub repository for the Java modding environment of Friday Night Funkin'! > [!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). @@ -8,7 +8,7 @@ Welcome to the official GitHub repository for the Java edition of Friday Night F ## 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. +Polyverse Funkin' is an open-source project that aims to create 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 diff --git a/android/src/main/java/me/stringdotjar/funkin/android/AndroidLauncher.java b/android/src/main/java/me/stringdotjar/funkin/android/AndroidLauncher.java index 999621a..a1f2eb8 100644 --- a/android/src/main/java/me/stringdotjar/funkin/android/AndroidLauncher.java +++ b/android/src/main/java/me/stringdotjar/funkin/android/AndroidLauncher.java @@ -4,7 +4,7 @@ import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import me.stringdotjar.funkin.FunkinGame; -import me.stringdotjar.funkin.InitScreen; +import me.stringdotjar.funkin.InitState; import me.stringdotjar.funkin.util.FunkinConstants; /** Launches the Android application. */ @@ -17,7 +17,7 @@ protected void onCreate(Bundle savedInstanceState) { FunkinConstants.WINDOW_TITLE, FunkinConstants.WINDOW_WIDTH, FunkinConstants.WINDOW_HEIGHT, - new InitScreen() + new InitState() ); AndroidApplicationConfiguration configuration = new AndroidApplicationConfiguration(); configuration.useImmersiveMode = true; // Recommended, but not required. diff --git a/assets/another_test.groovy b/assets/another_test.groovy index 243bdcb..e7bed07 100644 --- a/assets/another_test.groovy +++ b/assets/another_test.groovy @@ -32,7 +32,7 @@ class AnotherTestClass extends Script { sprite.setPosition(randomPosX, randomPosY) - Flixel.screen.add(sprite) + Flixel.state.add(sprite) } } diff --git a/assets/oml.groovy b/assets/oml.groovy new file mode 100644 index 0000000..3bcee2c --- /dev/null +++ b/assets/oml.groovy @@ -0,0 +1,15 @@ +import me.stringdotjar.flixelgdx.Flixel +import me.stringdotjar.polyverse.script.type.AnotherType + +class Sob extends AnotherType { + + Sob() { + super('Sob') + } + + @Override + void onWindowFocused() { + super.onWindowFocused() + Flixel.info("i hate [insert particular ethnicity here]") + } +} diff --git a/assets/test.groovy b/assets/test.groovy index 0a291d2..78e664b 100644 --- a/assets/test.groovy +++ b/assets/test.groovy @@ -2,7 +2,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Color import me.stringdotjar.flixelgdx.Flixel -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen +import me.stringdotjar.flixelgdx.graphics.FlixelState import me.stringdotjar.flixelgdx.backend.FlixelPaths import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite import me.stringdotjar.polyverse.script.type.SystemScript @@ -24,7 +24,7 @@ class TestScript extends SystemScript { super.onRender(delta) if (Gdx.input.isKeyJustPressed(Input.Keys.Q)) { - Flixel.setScreen(new TestScreen()) + Flixel.switchState(new TestState()) } } @@ -33,26 +33,34 @@ class TestScript extends SystemScript { super.onDispose() Flixel.info("TestClass", "Script has been disposed!") } + + @Override + void onWindowFocused() { + super.onWindowFocused() + Flixel.info("i like em young") + } } -class TestScreen extends FlixelScreen { +class TestState extends FlixelState { private FlixelSprite test @Override - void show() { - super.show() - - test = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset('NOTE_hold_assets')) - add(test) + void create() { + super.create() bgColor = new Color(0, 1, 0, 1) Flixel.playMusic('songs/darnell/Inst.ogg') + +// test.changeX(-30) + + test = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset('NOTE_hold_assets')) + add(test) } @Override - void render(float delta) { - super.render(delta) + void update(float delta) { + super.update(delta) } } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java index 615053f..59c8328 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/Flixel.java @@ -3,18 +3,20 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.assets.AssetManager; import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.math.Vector2; 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.graphics.FlixelState; +import me.stringdotjar.flixelgdx.graphics.FlixelViewport; 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.UpdateSignalData; +import me.stringdotjar.flixelgdx.signal.FlixelSignalData.StateSwitchSignalData; import me.stringdotjar.flixelgdx.signal.FlixelSignalData.SoundPlayedSignalData; +import me.stringdotjar.flixelgdx.util.FlixelConstants; import org.jetbrains.annotations.NotNull; import java.time.LocalDateTime; @@ -27,8 +29,8 @@ */ public final class Flixel { - /** The current {@code FlixelScreen} being displayed. */ - private static FlixelScreen screen; + /** The current {@code FlixelState} being displayed. */ + private static FlixelState state; /** The main audio object used to create, */ private static MiniAudio engine; @@ -61,7 +63,7 @@ public final class Flixel { */ public static void initialize(FlixelGame gameInstance) { if (initialized) { - throw new IllegalStateException("FNF:JE has already been initialized!"); + throw new IllegalStateException("FlixelGDX has already been initialized!"); } game = gameInstance; @@ -78,23 +80,24 @@ public static void initialize(FlixelGame gameInstance) { /** * Sets the current screen to the provided screen. * - * @param newScreen The new {@code FlixelScreen} to set as the current screen. + * @param newState The new {@code FlixelState} to set as the current screen. */ - public static void setScreen(FlixelScreen newScreen) { - Signals.preScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); + public static void switchState(FlixelState newState) { + Signals.preStateSwitch.dispatch(new StateSwitchSignalData(newState)); if (!initialized) { - throw new IllegalStateException("FNF:JE has not been initialized yet!"); + throw new IllegalStateException("Polyverse has not been initialized yet!"); } - if (newScreen == null) { - throw new IllegalArgumentException("Screen cannot be null!"); + if (newState == null) { + throw new IllegalArgumentException("New state cannot be null!"); } - if (Flixel.screen != null) { - Flixel.screen.hide(); - Flixel.screen.dispose(); + if (state != null) { + state.hide(); + state.dispose(); } - Flixel.screen = newScreen; - Flixel.screen.show(); - Signals.postScreenSwitch.dispatch(new ScreenSwitchSignalData(newScreen)); + game.resetViewports(); + state = newState; + state.create(); + Signals.postStateSwitch.dispatch(new StateSwitchSignalData(newState)); } /** @@ -109,8 +112,8 @@ public static void setScreen(FlixelScreen newScreen) { * } * * @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). + * 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) { @@ -128,9 +131,9 @@ public static MASound playSound(String path) { * 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 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. */ @@ -149,10 +152,10 @@ public static MASound playSound(String path, float volume) { * 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 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. */ @@ -172,13 +175,13 @@ public static MASound playSound(String path, float volume, boolean looping) { * 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 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 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) { @@ -199,13 +202,13 @@ public static MASound playSound(String path, float volume, boolean looping, MAGr * 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 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. */ @@ -231,8 +234,8 @@ public static MASound playSound(@NotNull String path, float volume, boolean loop * } * * @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). + * 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); @@ -249,9 +252,9 @@ public static void playMusic(String path) { * 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 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) { @@ -269,10 +272,10 @@ public static void playMusic(String path, float volume) { * 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 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) { @@ -292,11 +295,11 @@ public static void playMusic(String path, float volume, boolean looping) { * 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 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) { @@ -314,9 +317,6 @@ public static void playMusic(String path, float volume, boolean looping, boolean /** * 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) { @@ -369,14 +369,18 @@ public static Stage getStage() { return game.stage; } - public static FlixelScreen getScreen() { - return screen; + public static FlixelState getState() { + return state; } public static MASound getMusic() { return music; } + public static Vector2 getWindowSize() { + return game.viewSize; + } + public static MiniAudio getAudioEngine() { return engine; } @@ -393,10 +397,18 @@ public static MAGroup getSoundsGroup() { return soundsGroup; } - public static float getDelta() { + public static float getElapsed() { return Gdx.graphics.getDeltaTime(); } + public static FlixelViewport getViewport() { + return game.getViewport(); + } + + public static boolean isFullscreen() { + return Gdx.graphics.isFullscreen(); + } + /** * Contains all the global events that get dispatched when something happens in the game. * @@ -409,10 +421,12 @@ public static float getDelta() { */ 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 preUpdate = new FlixelSignal<>(); + public static final FlixelSignal postUpdate = new FlixelSignal<>(); + public static final FlixelSignal preDraw = new FlixelSignal<>(); + public static final FlixelSignal postDraw = new FlixelSignal<>(); + public static final FlixelSignal preStateSwitch = new FlixelSignal<>(); + public static final FlixelSignal postStateSwitch = new FlixelSignal<>(); public static final FlixelSignal preGameClose = new FlixelSignal<>(); public static final FlixelSignal postGameClose = new FlixelSignal<>(); public static final FlixelSignal windowFocused = new FlixelSignal<>(); diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java index 36b8078..0d30fc5 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/FlixelGame.java @@ -3,23 +3,22 @@ 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 com.badlogic.gdx.utils.SnapshotArray; +import me.stringdotjar.flixelgdx.graphics.FlixelObject; +import me.stringdotjar.flixelgdx.graphics.FlixelState; +import me.stringdotjar.flixelgdx.graphics.FlixelViewport; +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; 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; +import static me.stringdotjar.flixelgdx.signal.FlixelSignalData.UpdateSignalData; /** * Flixel's enhanced game object used for containing the main loop and core elements of Flixel. @@ -33,32 +32,35 @@ public abstract class FlixelGame implements ApplicationListener { protected String title; /** The size of the game's starting window position and its viewport. */ + protected Vector2 viewSize; + + /** The current window size stored in a vector object. */ protected Vector2 windowSize; /** The entry point screen the game starts in (which becomes null after the game is done setting up!). */ - protected FlixelScreen initialScreen; + protected FlixelState initialScreen; /** Is the game's window currently focused? */ - protected boolean isFocused = true; + private boolean isFocused = true; /** Is the game's window currently minimized? */ - protected boolean isMinimized = false; + private 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; + /** Where all the global viewports are stored. */ + protected SnapshotArray viewports; + + /** Is the game currently closing? */ + private boolean isClosing = false; + /** * 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. @@ -68,8 +70,9 @@ public abstract class FlixelGame implements ApplicationListener { * @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) { + public FlixelGame(String title, int width, int height, FlixelState initialScreen) { this.title = title; + this.viewSize = new Vector2(width, height); this.windowSize = new Vector2(width, height); this.initialScreen = initialScreen; } @@ -79,13 +82,10 @@ 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); + viewports = new SnapshotArray<>(FlixelViewport.class); + viewports.add(new FlixelViewport((int) viewSize.x, (int) viewSize.y)); - stage = new Stage(viewport, batch); + stage = new Stage(getViewport(), batch); Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888); pixmap.setColor(Color.WHITE); @@ -93,55 +93,110 @@ public void create() { bgTexture = new Texture(pixmap); pixmap.dispose(); - Flixel.setScreen(initialScreen); + Flixel.switchState(initialScreen); initialScreen = null; } @Override public void resize(int width, int height) { - viewport.update(width, height, true); + FlixelViewport[] viewportsArray = viewports.begin(); + for (int i = 0; i < viewports.size; i++) { + FlixelViewport viewport = viewportsArray[i]; + if (viewport != null) { + viewport.update(width, height, true); + } + } + viewports.end(); } - @Override - public void render() { - float delta = Gdx.graphics.getDeltaTime(); - FlixelScreen screen = Flixel.getScreen(); + /** + * Updates the logic of the game's loop. + * + * @param elapsed The amount of time that occurred in the last frame. + */ + public void update(float elapsed) { + Flixel.Signals.preUpdate.dispatch(new UpdateSignalData(elapsed)); - Flixel.Signals.preRender.dispatch(new RenderSignalData(delta)); + stage.act(elapsed); + FlixelTween.getGlobalManager().update(elapsed); - if (Flixel.keyJustPressed(Input.Keys.F11)) { - toggleFullscreen(); + FlixelState state = Flixel.getState(); + state.update(elapsed); + + // Update all members contained in the current state. + SnapshotArray members = state.getMembers(); + FlixelObject[] mbrs = members.begin(); + for (int i = 0; i < members.size; i++) { + mbrs[i].update(elapsed); } + members.end(); + + Flixel.Signals.postUpdate.dispatch(new UpdateSignalData(elapsed)); + } - // Update and render the current screen that's active. + /** + * Updates the graphics and display of the game. + */ + public void draw() { + Flixel.Signals.preDraw.dispatch(); 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) { + + // Loop through all viewports and draw the current state's members onto their set viewports. + FlixelViewport[] vps = viewports.begin(); + FlixelState state = Flixel.getState(); + SnapshotArray members = state.getMembers(); + FlixelObject[] mbrs = members.begin(); + for (int i = 0; i < viewports.size; i++) { + FlixelViewport viewport = vps[i]; + if (viewport == null) { + continue; + } + + var camera = viewport.getCamera(); + viewport.apply(); + camera.update(); + batch.setProjectionMatrix(camera.combined); + batch.begin(); + + // Draw the background color first. + batch.setColor(state.getBgColor()); + batch.draw(bgTexture, 0, 0, getViewport().getWorldWidth(), getViewport().getWorldHeight()); + batch.setColor(Color.WHITE); // Set the batch color back to white so display objects aren't affected. + + // Draw all the current screens members. + for (int j = 0; j < members.size; j++) { + FlixelObject member = mbrs[j]; + if (member == null) { continue; } - object.update(delta); - object.draw(batch); + if (member instanceof FlixelSprite s) { + // Check if the current sprite is in the visible part of the viewport. + // If it cannot be seen, then we don't draw the sprite to save performance. + if (camera.frustum.boundsInFrustum(s.getX(), s.getY(), 0, s.getWidth(), s.getHeight(), 0)) { + s.draw(batch); + } + } else { + member.draw(batch); + } } - screen.members.end(); + batch.end(); } - - batch.end(); - stage.act(delta); + viewports.end(); + members.end(); + state.draw(batch); stage.draw(); + Flixel.Signals.postDraw.dispatch(); + } + + @Override + public void render() { + float delta = Gdx.graphics.getDeltaTime(); - FlixelTween.getGlobalManager().update(delta); + windowSize.x = Gdx.graphics.getWidth(); + windowSize.y = Gdx.graphics.getHeight(); - Flixel.Signals.postRender.dispatch(new RenderSignalData(delta)); + update(delta); + draw(); } @Override @@ -171,7 +226,7 @@ public void onWindowUnfocused() { * 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. + * for compatibility with the window listener in the LWJGL3 (desktop) launcher. */ public void onWindowMinimized(boolean iconified) { isMinimized = iconified; @@ -183,27 +238,41 @@ public void onWindowMinimized(boolean iconified) { 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 { + /** + * Sets fullscreen mode for the game's window. + * + * @param enabled If the game's window should be in fullscreen mode. + */ + public void setFullscreen(boolean enabled) { + if (enabled) { Gdx.graphics.setFullscreenMode(Gdx.graphics.getDisplayMode()); - Flixel.info("Entering fullscreen mode."); + Flixel.info("Entered fullscreen mode."); + } else { + Gdx.graphics.setWindowedMode((int) viewSize.x, (int) viewSize.y); + Flixel.info("Exited fullscreen mode."); } } + /** Toggles fullscreen mode on or off, depending on the current state. */ + public void toggleFullscreen() { + setFullscreen(!Flixel.isFullscreen()); + } + @Override public void dispose() { + if (isClosing) { + Flixel.warn("Game is already closing. Skipping dispose..."); + return; + } + isClosing = true; + 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(); + Flixel.getState().hide(); + Flixel.getState().dispose(); stage.dispose(); batch.dispose(); bgTexture.dispose(); @@ -215,15 +284,34 @@ public void dispose() { Flixel.getSoundsGroup().dispose(); Flixel.getAudioEngine().dispose(); - Flixel.info("Disposing and shutting down scripts..."); - Flixel.Signals.postGameClose.dispatch(); } + /** + * 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(msg); + FlixelRuntimeUtil.showErrorAlert("Uncaught Exception", 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(); + } + }); + } + public String getTitle() { return title; } + public Vector2 getViewSize() { + return viewSize; + } + public Vector2 getWindowSize() { return windowSize; } @@ -236,12 +324,34 @@ public Stage getStage() { return stage; } - public FitViewport getViewport() { - return viewport; + /** + * Gets the first viewport that is part of the list. If the list is {@code null} or empty, then a new list (with a + * default viewport accordingly). + * + * @return The first viewport in the list. + */ + public FlixelViewport getViewport() { + Vector2 windowSize = Flixel.getWindowSize(); + if (viewports == null) { + Flixel.warn("Viewport list is null. Resigning with fresh array..."); + viewports = new SnapshotArray<>(); + } + if (viewports.isEmpty()) { + Flixel.warn("Viewport list is empty. Adding new fresh default viewport..."); + viewports.add(new FlixelViewport((int) windowSize.x, (int) windowSize.y)); + } + return viewports.first(); } - public OrthographicCamera getCamera() { - return camera; + public void resetViewports() { + FlixelViewport viewport = new FlixelViewport((int) viewSize.x, (int) viewSize.y); + viewport.update((int) windowSize.x, (int) windowSize.y, true); + viewports.clear(); + viewports.add(viewport); + } + + public SnapshotArray getViewports() { + return viewports; } public SpriteBatch getBatch() { @@ -256,19 +366,9 @@ 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(); - } - }); + public void setWindowSize(Vector2 newSize) { + viewSize = newSize; + Gdx.graphics.setWindowedMode((int) newSize.x, (int) newSize.y); + Flixel.info("Set window to new size. (WIDTH=" + newSize.x + ", HEIGHT=" + newSize.y + ")"); } } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java similarity index 56% rename from flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java rename to flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java index 6ebd5c2..c806248 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelObject.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelObject.java @@ -1,10 +1,10 @@ -package me.stringdotjar.flixelgdx.graphics.sprite; +package me.stringdotjar.flixelgdx.graphics; 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}. */ +/** An interface which allows any class that implements it to be added to a {@link FlixelState}. */ public interface FlixelObject { void update(float delta); void draw(Batch batch); + void destroy(); } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java new file mode 100644 index 0000000..dde2799 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelState.java @@ -0,0 +1,110 @@ +package me.stringdotjar.flixelgdx.graphics; + +import com.badlogic.gdx.Screen; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.utils.SnapshotArray; +import me.stringdotjar.flixelgdx.group.FlixelGroup; + +/** + * Base class for creating a better screen display with more functionality than the default {@link + * com.badlogic.gdx.Screen} interface. + */ +public abstract class FlixelState extends FlixelGroup implements Screen { + + /** Should {@code this} screen update its logic even when a substate is currently opened? */ + public boolean persistentUpdate = false; + + /** Should {@code this} screen draw its members even when a substate is currently opened? */ + public boolean persistentDraw = true; + + /** The background color of {@code this} current screen. */ + protected Color bgColor; + + public FlixelState() { + members = new SnapshotArray<>(FlixelObject.class); + } + + @Override + public final void show() {} + + @Override + public final void render(float delta) {} + + /** + * Called when the screen is first created. This is where you want to assign your + * sprites and setup everything your screen uses! + */ + public void create() {} + + /** + * Updates the logic of {@code this} screen. + * + * @param delta The amount of time that occurred since the last frame. + */ + public void update(float delta) { + if (!persistentUpdate) { + return; + } + } + + /** + * Draws {@code this} state's members onto the screen. + * + * @param batch The batch that's used to draw {@code this} state's members. + */ + public void draw(Batch batch) { + if (!persistentDraw) { + return; + } + } + + @Override + public void resize(int width, int height) {} + + @Override + public void pause() {} + + @Override + public void resume() {} + + @Override + public void hide() {} + + /** + * Disposes {@code this} state and all of its members. Called automatically when {@link + * me.stringdotjar.flixelgdx.Flixel#switchState(FlixelState)} is used, so that sprites and other + * {@link FlixelObject}s release its resources. + */ + @Override + public void dispose() { + if (members == null) { + return; + } + Object[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject obj = (FlixelObject) items[i]; + if (obj != null) { + obj.destroy(); + } + } + members.end(); + members.clear(); + } + + /** + * 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/FlixelViewport.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelViewport.java new file mode 100644 index 0000000..be9b225 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/FlixelViewport.java @@ -0,0 +1,22 @@ +package me.stringdotjar.flixelgdx.graphics; + +import com.badlogic.gdx.graphics.Camera; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.utils.viewport.FitViewport; + +import static me.stringdotjar.flixelgdx.Flixel.getWindowSize; + +public class FlixelViewport extends FitViewport { + + public FlixelViewport() { + this((int)getWindowSize().x, (int)getWindowSize().y, new OrthographicCamera(getWindowSize().x, getWindowSize().y)); + } + + public FlixelViewport(int width, int height) { + this(width, height, new OrthographicCamera(width, height)); + } + + public FlixelViewport(int width, int height, Camera camera) { + super(width, height, camera); + } +} 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 deleted file mode 100644 index 8f00693..0000000 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/screen/FlixelScreen.java +++ /dev/null @@ -1,56 +0,0 @@ -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/FlixelSprite.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java index ca82924..5f5a58a 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/graphics/sprite/FlixelSprite.java @@ -5,13 +5,16 @@ 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.graphics.g2d.TextureAtlas.AtlasRegion; 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 me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.flixelgdx.graphics.FlixelObject; +import me.stringdotjar.flixelgdx.graphics.FlixelViewport; import java.util.Comparator; import java.util.HashMap; @@ -30,6 +33,9 @@ public class FlixelSprite extends Sprite implements FlixelObject, Pool.Poolable /** The hitbox used for collision detection and angling. */ protected Rectangle hitbox; + /** The viewports that {@code this} sprite is projected onto. */ + protected FlixelViewport[] viewports; + /** The atlas regions used in this sprite (used for animations). */ protected Array atlasRegions; @@ -43,10 +49,10 @@ public class FlixelSprite extends Sprite implements FlixelObject, Pool.Poolable protected float stateTime = 0; /** The name of the current animation playing. */ - protected String currentAnim = ""; + private String currentAnim = ""; /** Is {@code this} sprites current animation looping indefinitely? */ - protected boolean looping = true; + private boolean looping = true; /** * Where all the image frames are stored. This is also where the main image is stored when using @@ -56,6 +62,7 @@ public class FlixelSprite extends Sprite implements FlixelObject, Pool.Poolable public FlixelSprite() { animations = new HashMap<>(); + viewports = new FlixelViewport[]{Flixel.getViewport()}; } /** @@ -86,7 +93,7 @@ public FlixelSprite loadGraphic(FileHandle path) { /** * 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 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. */ @@ -98,8 +105,8 @@ public FlixelSprite loadGraphic(FileHandle path, int frameWidth) { /** * 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 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. */ @@ -120,9 +127,9 @@ public FlixelSprite loadGraphic(Texture texture, int frameWidth, int frameHeight * 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. + * different frames from. * @param xmlFile The path to the {@code .xml} file which contains the data for each subtexture of - * the sparrow atlas. + * the sparrow atlas. * @return {@code this} sprite for chaining. */ public FlixelSprite loadSparrowFrames(FileHandle texture, FileHandle xmlFile) { @@ -133,15 +140,15 @@ public FlixelSprite loadSparrowFrames(FileHandle texture, FileHandle 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. + * from. * @param xmlFile The {@link XmlReader.Element} data which contains the data for each subtexture - * of the sparrow atlas. + * 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<>(); + atlasRegions = new Array<>(AtlasRegion.class); for (XmlReader.Element subTexture : xmlFile.getChildrenByName("SubTexture")) { String name = subTexture.getAttribute("name"); @@ -189,10 +196,10 @@ public FlixelSprite loadSparrowFrames(Texture texture, XmlReader.Element xmlFile /** * 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 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? + * @param loop Should the new animation loop indefinitely? */ public void addAnimationByPrefix(String name, String prefix, int frameRate, boolean loop) { Array frames = new Array<>(); @@ -209,17 +216,17 @@ public void addAnimationByPrefix(String name, String prefix, int frameRate, bool animations.put( name, - new Animation<>( - 1f / frameRate, frames, loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL)); + 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 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) { @@ -258,8 +265,8 @@ public void playAnimation(String name, boolean loop) { /** * 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 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) { @@ -302,6 +309,11 @@ public void draw(Batch batch) { getRotation()); } + @Override + public void destroy() { + reset(); + } + public boolean isAnimationFinished() { Animation anim = animations.get(currentAnim); if (anim == null) return true; @@ -314,23 +326,37 @@ public void reset() { 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(); + if (texture != null) { + texture.dispose(); + texture = null; + } + if (currentFrame != null) { + currentFrame.getTexture().dispose(); + currentFrame = null; + } + if (atlasRegions != null) { + for (int i = atlasRegions.size - 1; i >= 0; i--) { + var region = atlasRegions.items[i]; + if (region != null) { + region.getTexture().dispose(); + } + } + atlasRegions.setSize(0); + atlasRegions = null; } - 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(); + if (frames != null) { + for (int i = frames.length - 1; i >= 0; i--) { + var frame = frames[i]; + if (frame != null) { + for (TextureRegion region : frame) { + if (region != null) { + region.getTexture().dispose(); + } + } + } } + frames = null; } - frames = null; } public void changeX(float x) { @@ -341,6 +367,10 @@ public void changeY(float y) { setY(getY() + y); } + public void changeRotation(float rotation) { + setRotation(getRotation() + rotation); + } + public Map> getAnimations() { return animations; } @@ -349,6 +379,10 @@ public Array getAtlasRegions() { return atlasRegions; } + public FlixelViewport[] getViewports() { + return viewports; + } + public TextureAtlas.AtlasRegion getCurrentFrame() { return currentFrame; } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java new file mode 100644 index 0000000..1aac9ab --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroup.java @@ -0,0 +1,97 @@ +package me.stringdotjar.flixelgdx.group; + +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.utils.SnapshotArray; + +import me.stringdotjar.flixelgdx.graphics.FlixelObject; + +import java.util.function.Consumer; + +/** + * Base class for creating groups with a list of members inside of it. + */ +public abstract class FlixelGroup implements FlixelGroupable, FlixelObject { + + /** + * The list of members that {@code this} group contains. + */ + protected SnapshotArray members; + + public FlixelGroup() { + members = new SnapshotArray<>(FlixelObject.class); + } + + @Override + public void add(T member) { + members.add(member); + } + + @Override + public void update(float elapsed) { + FlixelObject[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = items[i]; + if (member == null) { + continue; + } + member.update(elapsed); + } + members.end(); + } + + @Override + public void draw(Batch batch) { + FlixelObject[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = items[i]; + if (member == null) { + continue; + } + member.draw(batch); + } + members.end(); + } + + @Override + public void remove(T member) { + members.removeValue(member, true); + } + + @Override + public void destroy() { + members.forEach(FlixelObject::destroy); + members.clear(); + } + + @Override + public void clear() { + members.clear(); + } + + public void forEachMember(Consumer callback) { + FlixelObject[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = items[i]; + if (member == null) { + continue; + } + callback.accept(member); + } + members.end(); + } + + public void forEachMemberType(Class type, Consumer callback) { + FlixelObject[] items = members.begin(); + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = items[i]; + if (type.isInstance(member)) { + callback.accept(type.cast(member)); + } + } + members.end(); + } + + public SnapshotArray getMembers() { + return members; + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java new file mode 100644 index 0000000..b6340a0 --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelGroupable.java @@ -0,0 +1,10 @@ +package me.stringdotjar.flixelgdx.group; + +/** + * Interface for creating new groups with members inside of them. + */ +public interface FlixelGroupable { + void add(FlixelObject member); + void remove(FlixelObject member); + void clear(); +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java new file mode 100644 index 0000000..324eb5a --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/group/FlixelSpriteGroup.java @@ -0,0 +1,555 @@ +package me.stringdotjar.flixelgdx.group; + +import com.badlogic.gdx.math.MathUtils; +import me.stringdotjar.flixelgdx.graphics.FlixelObject; +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; + +import java.util.Comparator; +import java.util.Random; +import java.util.function.Consumer; + +/** + * A group of sprites that can be used to group and manage multiple sprites at once. + *

+ * Setting the group's position ({@link #setX}, {@link #setY}, {@link #setPosition}) moves all + * member sprites by the same delta, mirroring how HaxeFlixel propagates position changes + * (except in {@link RotationMode#WHEEL} mode where positions are set absolutely each frame). + * Sprites added to the group are automatically offset by the group's current position. + *

+ * Rotation behaviour is controlled by {@link #rotationMode}: + *

+ */ +public class FlixelSpriteGroup extends FlixelGroup { + + /** + * Maximum number of members allowed. When {@code 0}, the group can grow without limit (default). + * When {@code > 0}, {@link #add(FlixelSprite)} will not add if at capacity. + */ + private int maxSize = 0; + + /** + * The X position of the group. When changed, all member sprites are moved by the delta + * (unless {@link #rotationMode} is {@link RotationMode#WHEEL}, in which case positions are + * set absolutely each frame). + */ + private float x; + + /** + * The Y position of the group. When changed, all member sprites are moved by the delta + * (unless {@link #rotationMode} is {@link RotationMode#WHEEL}, in which case positions are + * set absolutely each frame). + */ + private float y; + + /** + * The current rotation mode. See {@link RotationMode} for details. + */ + private RotationMode rotationMode = RotationMode.INDIVIDUAL; + + /** + * Distance of each sprite from the center when {@link #rotationMode} is + * {@link RotationMode#WHEEL}. + */ + private float rotationRadius = 100f; + + /** + * The group's rotation in degrees. How this value is used depends on {@link #rotationMode}: + * + */ + private float rotation; + + private static final Random RANDOM = new Random(); + + /** + * Creates a new FlixelSpriteGroup with default parameters. + */ + public FlixelSpriteGroup() { + this(0, 100f, 0f); + } + + /** + * Creates a new FlixelSpriteGroup with the given max size. + * + * @param maxSize Maximum members allowed; 0 for unlimited. + */ + public FlixelSpriteGroup(int maxSize) { + this(maxSize, 100f, 0f); + } + + /** + * Creates a new FlixelSpriteGroup with the given parameters. + * + * @param maxSize Maximum members allowed; 0 for unlimited. + * @param rotationRadius Distance of each sprite from the center when in {@link RotationMode#WHEEL}. + * @param rotation Initial rotation in degrees. + */ + public FlixelSpriteGroup(int maxSize, float rotationRadius, float rotation) { + super(); + this.maxSize = Math.max(0, maxSize); + this.rotationRadius = rotationRadius; + this.rotation = rotation; + } + + public float getX() { + return x; + } + + /** + * Sets the group's X position. Every member sprite is moved by the delta ({@code newX - oldX}), + * unless the rotation mode is {@link RotationMode#WHEEL} (positions are set absolutely each frame). + */ + public void setX(float x) { + float dx = x - this.x; + this.x = x; + + if (rotationMode != RotationMode.WHEEL) { + transformMembersX(dx); + } + } + + /** + * Adds the given amount to the group's X position. Equivalent to {@code setX(getX() + x)}. + */ + public void changeX(float x) { + setX(this.x + x); + } + + public float getY() { + return y; + } + + /** + * Sets the group's Y position. Every member sprite is moved by the delta ({@code newY - oldY}), + * unless the rotation mode is {@link RotationMode#WHEEL} (positions are set absolutely each frame). + */ + public void setY(float y) { + float dy = y - this.y; + this.y = y; + + if (rotationMode != RotationMode.WHEEL) { + transformMembersY(dy); + } + } + + /** + * Adds the given amount to the group's Y position. Equivalent to {@code setY(getY() + y)}. + */ + public void changeY(float y) { + setY(this.y + y); + } + + /** + * Sets both X and Y in a single call, applying the deltas to all members in one pass + * to avoid iterating twice. + */ + public void setPosition(float x, float y) { + float dx = x - this.x; + float dy = y - this.y; + this.x = x; + this.y = y; + + if (rotationMode != RotationMode.WHEEL) { + transformMembersPosition(dx, dy); + } + } + + + /** + * Returns the current rotation mode. + */ + public RotationMode getRotationMode() { + return rotationMode; + } + + /** + * Sets the rotation mode. See {@link RotationMode} for the available options. + */ + public void setRotationMode(RotationMode rotationMode) { + this.rotationMode = rotationMode; + } + + public float getRotationRadius() { + return rotationRadius; + } + + public void setRotationRadius(float rotationRadius) { + this.rotationRadius = rotationRadius; + } + + /** + * Returns the group's rotation in degrees. + */ + public float getRotation() { + return rotation; + } + + /** + * Sets the group's rotation in degrees. The behaviour depends on {@link #rotationMode}: + * + */ + public void setRotation(float rotation) { + float delta = rotation - this.rotation; + this.rotation = rotation; + + switch (rotationMode) { + case INDIVIDUAL: + transformMembersIndividualRotation(delta); + break; + case ORBIT: + orbitMembersAroundCenter(delta); + break; + case WHEEL: + break; + } + } + + /** + * Adds the given amount to the group's rotation. Equivalent to + * {@code setRotation(getRotation() + degrees)}. + */ + public void changeRotation(float degrees) { + setRotation(this.rotation + degrees); + } + + /** + * Number of members. Prefer this over {@code members.size} for consistency. + */ + public int getLength() { + return members.size; + } + + public int getMaxSize() { + return maxSize; + } + + public void setMaxSize(int maxSize) { + this.maxSize = Math.max(0, maxSize); + } + + + /** + * Adds a sprite to the group. The sprite's position is automatically offset by + * the group's current ({@link #x}, {@link #y}), matching HaxeFlixel's {@code preAdd} behaviour. + */ + @Override + public void add(FlixelSprite sprite) { + if (sprite == null) { + return; + } + + if (maxSize > 0 && members.size >= maxSize) { + return; + } + + preAdd(sprite); + super.add(sprite); + } + + /** + * Adds a sprite and returns it for chaining. + */ + public FlixelSprite addAndReturn(FlixelSprite sprite) { + add(sprite); + return sprite; + } + + /** + * Inserts a new sprite at the given index. The sprite is offset by the group's position. + */ + public void insert(int index, FlixelSprite sprite) { + if (sprite == null) { + return; + } + + if (maxSize > 0 && members.size >= maxSize) { + return; + } + + preAdd(sprite); + index = MathUtils.clamp(index, 0, members.size); + members.insert(index, sprite); + } + + /** + * Removes a sprite from the group. The group's position offset is subtracted from the + * sprite, restoring it to "local" coordinates, matching HaxeFlixel's {@code remove} behaviour. + */ + @Override + public void remove(FlixelSprite sprite) { + if (sprite == null) { + return; + } + + super.remove(sprite); + sprite.setX(sprite.getX() - x); + sprite.setY(sprite.getY() - y); + } + + /** + * Replaces an existing sprite with a new one. The new sprite is offset by the group's position. + * + * @return the new sprite + */ + public FlixelSprite replace(FlixelSprite oldSprite, FlixelSprite newSprite) { + if (oldSprite == null || newSprite == null) { + return newSprite; + } + + int idx = members.indexOf(oldSprite, true); + if (idx < 0) { + add(newSprite); + return newSprite; + } + + preAdd(newSprite); + members.set(idx, newSprite); + return newSprite; + } + + /** + * Sorts members using the given comparator. + */ + public void sort(Comparator comparator) { + if (comparator == null) { + return; + } + + members.sort((a, b) -> comparator.compare((FlixelSprite) a, (FlixelSprite) b)); + } + + /** + * Returns a random member, or null if the group is empty. + */ + public FlixelSprite getRandom() { + return getRandom(0, members.size); + } + + /** + * Returns a random member from the range [startIndex, startIndex + length), or null if none. + */ + public FlixelSprite getRandom(int startIndex, int length) { + if (members.size == 0) { + return null; + } + + startIndex = MathUtils.clamp(startIndex, 0, members.size - 1); + if (length <= 0) { + length = members.size; + } + + int end = Math.min(startIndex + length, members.size); + int span = end - startIndex; + if (span <= 0) { + return null; + } + + return (FlixelSprite) members.get(startIndex + RANDOM.nextInt(span)); + } + + /** + * Applies a function to each member. + */ + public void forEach(Consumer function) { + if (function == null) { + return; + } + + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + function.accept(s); + } + } + } + + /** + * Returns the member at the given index, or null. + */ + public FlixelSprite get(int index) { + if (index < 0 || index >= members.size) { + return null; + } + + return (FlixelSprite) members.get(index); + } + + @Override + public void update(float elapsed) { + super.update(elapsed); + + if (rotationMode == RotationMode.WHEEL) { + applyWheelRotation(); + } + } + + + @Override + public void clear() { + for (int i = 0, n = members.size; i < n; i++) { + FlixelObject member = members.get(i); + if (member != null) { + member.destroy(); + } + } + + members.clear(); + } + + /** + * Offsets a sprite by the group's current position before it is inserted into the group, + * matching HaxeFlixel's {@code preAdd} behaviour. + */ + private void preAdd(FlixelSprite sprite) { + sprite.setX(sprite.getX() + x); + sprite.setY(sprite.getY() + y); + } + + /** + * Moves every member's X by the given delta. + */ + private void transformMembersX(float dx) { + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + s.setX(s.getX() + dx); + } + } + } + + /** + * Moves every member's Y by the given delta. + */ + private void transformMembersY(float dy) { + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + s.setY(s.getY() + dy); + } + } + } + + /** + * Moves every member's position by the given deltas in a single pass. + */ + private void transformMembersPosition(float dx, float dy) { + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + s.setPosition(s.getX() + dx, s.getY() + dy); + } + } + } + + /** + * {@link RotationMode#INDIVIDUAL}: applies the rotation delta to each sprite's own rotation + * without changing any positions. + */ + private void transformMembersIndividualRotation(float delta) { + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s != null) { + s.setRotation(s.getRotation() + delta); + } + } + } + + /** + * {@link RotationMode#ORBIT}: rotates every member's position around the group origin + * ({@link #x}, {@link #y}) by {@code angleDelta} degrees and adjusts each sprite's own + * rotation by the same amount, like a rotating camera/TV screen. + */ + private void orbitMembersAroundCenter(float angleDelta) { + float cos = MathUtils.cosDeg(angleDelta); + float sin = MathUtils.sinDeg(angleDelta); + + for (int i = 0, n = members.size; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s == null) { + continue; + } + + float localX = s.getX() - x; + float localY = s.getY() - y; + float rotatedX = localX * cos - localY * sin; + float rotatedY = localX * sin + localY * cos; + s.setPosition(x + rotatedX, y + rotatedY); + s.setRotation(s.getRotation() + angleDelta); + } + } + + /** + * {@link RotationMode#WHEEL}: positions and rotates each sprite consecutively around the + * group center in a radial/wheel pattern. Called each frame from {@link #update(float)}. + */ + private void applyWheelRotation() { + int n = members.size; + if (n == 0) { + return; + } + + float angleStep = 360f / n; + for (int i = 0; i < n; i++) { + FlixelSprite s = (FlixelSprite) members.get(i); + if (s == null) { + continue; + } + + float angleDeg = rotation + angleStep * i; + float px = x + rotationRadius * MathUtils.cosDeg(angleDeg); + float py = y + rotationRadius * MathUtils.sinDeg(angleDeg); + s.setPosition(px, py); + s.setRotation(angleDeg); + } + } + + /** + * Controls how a {@link FlixelSpriteGroup}'s {@link #rotation} affects its members. + */ + public enum RotationMode { + + /** + * Rotation is applied to each sprite's individual rotation by the delta. + * No positional changes occur from rotation. + */ + INDIVIDUAL, + + /** + * Sprites are arranged in a radial/wheel pattern around the group center + * ({@link #x}, {@link #y}). Each sprite is positioned at {@link #rotationRadius} from the + * center, spaced evenly around 360 degrees. Positions and rotations are set absolutely + * each frame in {@link #update(float)}. + *

+ * Note that you cannot change the rotation of any specific sprite of the group when in this mode, as it + * is locked to the group's rotation. + */ + WHEEL, + + /** + * All sprites orbit around the group origin ({@link #x}, {@link #y}) as a rigid body, + * like a rotating camera or TV screen. When rotation changes, each sprite's position is + * rotated around the center by the delta and its individual rotation is also adjusted + * by the same amount. + */ + ORBIT + } +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java new file mode 100644 index 0000000..3b522fc --- /dev/null +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/input/FlixelKey.java @@ -0,0 +1,11 @@ +package me.stringdotjar.flixelgdx.input; + +import com.badlogic.gdx.Input; + +/** + * A simple extension of {@link Input.Keys} to simplify accessing key constants. + */ +public class FlixelKey extends Input.Keys { + + private FlixelKey() {} +} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java index 84f010b..0156008 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/signal/FlixelSignalData.java @@ -1,7 +1,7 @@ package me.stringdotjar.flixelgdx.signal; import games.rednblack.miniaudio.MASound; -import me.stringdotjar.flixelgdx.graphics.screen.FlixelScreen; +import me.stringdotjar.flixelgdx.graphics.FlixelState; import me.stringdotjar.flixelgdx.Flixel; /** @@ -10,9 +10,9 @@ */ public final class FlixelSignalData { - public record RenderSignalData(float delta) {} + public record UpdateSignalData(float delta) {} - public record ScreenSwitchSignalData(FlixelScreen screen) {} + public record StateSwitchSignalData(FlixelState screen) {} public record SoundPlayedSignalData(MASound sound) {} diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java index 149dda7..3d4a1df 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelConstants.java @@ -42,4 +42,6 @@ public static final class AsciiCodes { private AsciiCodes() {} } + + private FlixelConstants() {} } diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java index 2c0ec5c..9fdc8b3 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelReflectUtil.java @@ -13,7 +13,9 @@ * Backend utility class for obtaining and manipulating fields on objects through the usage of Java * reflection. */ -public class FlixelReflectUtil { +public final class FlixelReflectUtil { + + private FlixelReflectUtil() {} /** * Checks if a field exists on a given object. diff --git a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java index 4d8a48c..7f35c86 100644 --- a/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java +++ b/flixelgdx/src/main/java/me/stringdotjar/flixelgdx/util/FlixelRuntimeUtil.java @@ -1,5 +1,11 @@ package me.stringdotjar.flixelgdx.util; +import me.stringdotjar.flixelgdx.Flixel; + +import javax.swing.JOptionPane; +import java.awt.EventQueue; +import java.lang.reflect.InvocationTargetException; + /** * Utility class for handling operation related to the runtime environment, including OS detection, * extracting runtime information, obtaining information from exceptions, and other related tasks. @@ -52,5 +58,57 @@ public static String getFullExceptionMessage(Throwable exception) { return messageBuilder.toString(); } + /** + * Displays a new general info alert window with the given title and message. + * + * @param title The title of the alert window. + * @param message The message content to display in the alert window. + */ + public static void showInfoAlert(String title, Object message) { + showAlert(title, message, JOptionPane.INFORMATION_MESSAGE); + } + + /** + * Displays a new warning alert window with the given title and message. + * + * @param title The title of the alert window. + * @param message The message content to display in the alert window. + */ + public static void showWarningAlert(String title, Object message) { + showAlert(title, message, JOptionPane.WARNING_MESSAGE); + } + + /** + * Displays a new error alert window with the given title and message. + * + * @param title The title of the alert window. + * @param message The message content to display in the alert window. + */ + public static void showErrorAlert(String title, Object message) { + showAlert(title, message, JOptionPane.ERROR_MESSAGE); + } + + /** + * Displays a new alert window with the given title, message, and type. + * + * @param title The title of the alert window. + * @param message The message content to display in the alert window. + * @param type The type of alert (e.g., JOptionPane.INFORMATION_MESSAGE). + */ + public static void showAlert(String title, Object message, int type) { + String msg = message != null ? message.toString() : "null"; + if (EventQueue.isDispatchThread()) { + JOptionPane.showMessageDialog(null, msg, title, type); + } else { + try { + EventQueue.invokeAndWait(() -> { + JOptionPane.showMessageDialog(null, msg, title, type); + }); + } catch (InterruptedException | InvocationTargetException e) { + Flixel.error(FlixelConstants.System.LOG_TAG, "Failed to show alert message.", e); + } + } + } + private FlixelRuntimeUtil() {} } diff --git a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java index cfdce50..866df71 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/FunkinGame.java @@ -1,9 +1,10 @@ package me.stringdotjar.funkin; +import com.badlogic.gdx.Input; 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.flixelgdx.graphics.FlixelState; import me.stringdotjar.polyverse.Polyverse; import me.stringdotjar.polyverse.script.type.Script; import me.stringdotjar.polyverse.script.type.SystemScript; @@ -15,7 +16,7 @@ public class FunkinGame extends FlixelGame { private float lastVolume = 1.0f; - public FunkinGame(String title, int width, int height, FlixelScreen initialScreen) { + public FunkinGame(String title, int width, int height, FlixelState initialScreen) { super(title, width, height, initialScreen); } @@ -28,12 +29,18 @@ public void create() { @Override public void render() { super.render(); - Polyverse.forAllScripts(script -> script.onRender(Flixel.getDelta())); + + if (Flixel.keyJustPressed(Input.Keys.F11)) { + toggleFullscreen(); + } + + Polyverse.forAllScripts(script -> script.onRender(Flixel.getElapsed())); } @Override public void dispose() { super.dispose(); + Flixel.info("Funkin", "Disposing scripts..."); Polyverse.forAllScripts(Script::onDispose); } @@ -41,7 +48,7 @@ public void dispose() { public void onWindowFocused() { super.onWindowFocused(); Flixel.setMasterVolume(lastVolume); - Polyverse.forEachScript(SystemScript.class, SystemScript::onWindowFocused); + Polyverse.forEachScriptSuccessor(SystemScript.class, SystemScript::onWindowFocused); } @Override @@ -49,7 +56,7 @@ public void onWindowUnfocused() { super.onWindowUnfocused(); lastVolume = Flixel.getMasterVolume(); Flixel.setMasterVolume(0.008f); - Polyverse.forEachScript(SystemScript.class, SystemScript::onWindowUnfocused); + Polyverse.forEachScriptSuccessor(SystemScript.class, SystemScript::onWindowUnfocused); } @Override @@ -57,7 +64,7 @@ public void onWindowMinimized(boolean iconified) { super.onWindowMinimized(iconified); lastVolume = Flixel.getMasterVolume(); Flixel.setMasterVolume(0); - Polyverse.forEachScript(SystemScript.class, script -> script.onWindowMinimized(iconified)); + Polyverse.forEachScriptSuccessor(SystemScript.class, script -> script.onWindowMinimized(iconified)); } private void configurePolyverse() { @@ -67,5 +74,6 @@ private void configurePolyverse() { Polyverse.registerScript(FlixelPaths.asset("test.groovy")); Polyverse.registerScript(FlixelPaths.asset("another_test.groovy")); + Polyverse.registerScript(FlixelPaths.asset("oml.groovy")); } } diff --git a/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java b/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java deleted file mode 100644 index e90d619..0000000 --- a/funkin/src/main/java/me/stringdotjar/funkin/InitScreen.java +++ /dev/null @@ -1,14 +0,0 @@ -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/InitState.java b/funkin/src/main/java/me/stringdotjar/funkin/InitState.java new file mode 100644 index 0000000..412adcc --- /dev/null +++ b/funkin/src/main/java/me/stringdotjar/funkin/InitState.java @@ -0,0 +1,14 @@ +package me.stringdotjar.funkin; + +import me.stringdotjar.flixelgdx.graphics.FlixelState; +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.funkin.menus.TitleState; + +public class InitState extends FlixelState { + + @Override + public void create() { + super.create(); + Flixel.switchState(new TitleState()); + } +} diff --git a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java deleted file mode 100644 index 99c78e2..0000000 --- a/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleScreen.java +++ /dev/null @@ -1,90 +0,0 @@ -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/menus/TitleState.java b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java new file mode 100644 index 0000000..e02411f --- /dev/null +++ b/funkin/src/main/java/me/stringdotjar/funkin/menus/TitleState.java @@ -0,0 +1,165 @@ +package me.stringdotjar.funkin.menus; + +import games.rednblack.miniaudio.MASound; +import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.flixelgdx.backend.FlixelPaths; +import me.stringdotjar.flixelgdx.graphics.FlixelState; +import me.stringdotjar.flixelgdx.graphics.sprite.FlixelSprite; +import me.stringdotjar.flixelgdx.group.FlixelSpriteGroup; +import me.stringdotjar.flixelgdx.input.FlixelKey; +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 TitleState extends FlixelState { + + private FlixelSprite logo; + + private FlixelSpriteGroup spriteGroup; + private FlixelSprite s1; + private FlixelSprite s2; + private FlixelSprite s3; + private FlixelSprite s4; + + private FlixelTween tween; + private MASound tickleFight; + + @Override + public void create() { + super.create(); + + 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); + + spriteGroup = new FlixelSpriteGroup(0, 200f, 0f); + s1 = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset("transitionSwag/stickers-set-1/bfSticker1")); + s2 = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset("transitionSwag/stickers-set-1/bfSticker2")); + s3 = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset("transitionSwag/stickers-set-1/bfSticker3")); + s4 = new FlixelSprite().loadGraphic(FlixelPaths.sharedImageAsset("transitionSwag/stickers-set-1/gfSticker1")); + s1.setX(200); + s2.setX(-200); + s3.setX(200); + s4.setX(-200); + s1.setY(200); + s2.setY(200); + s3.setY(-200); + s4.setY(-200); + spriteGroup.add(s1); + spriteGroup.add(s2); + spriteGroup.add(s3); + spriteGroup.add(s4); + spriteGroup.setRotationMode(FlixelSpriteGroup.RotationMode.INDIVIDUAL); + add(spriteGroup); + + 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 update(float elapsed) { + super.update(elapsed); + + float speed = 500 * elapsed; + // if (Flixel.keyPressed(FlixelKey.W)) { + // logo.changeY(speed); + // } + // if (Flixel.keyPressed(FlixelKey.S)) { + // logo.changeY(-speed); + // } + // if (Flixel.keyPressed(FlixelKey.A)) { + // logo.changeX(-speed); + // } + // if (Flixel.keyPressed(FlixelKey.D)) { + // logo.changeX(speed); + // } + if (Flixel.keyPressed(FlixelKey.W)) { + spriteGroup.changeY(speed); + } + if (Flixel.keyPressed(FlixelKey.S)) { + spriteGroup.changeY(-speed); + } + if (Flixel.keyPressed(FlixelKey.A)) { + spriteGroup.changeX(-speed); + } + if (Flixel.keyPressed(FlixelKey.D)) { + spriteGroup.changeX(speed); + } + + if (Flixel.keyPressed(FlixelKey.I)) { + s1.changeY(speed); + } + if (Flixel.keyPressed(FlixelKey.K)) { + s1.changeY(-speed); + } + if (Flixel.keyPressed(FlixelKey.J)) { + s1.changeX(-speed); + } + if (Flixel.keyPressed(FlixelKey.L)) { + s1.changeX(speed); + } + if (Flixel.keyPressed(FlixelKey.U)) { + s1.changeRotation(speed); + } + if (Flixel.keyPressed(FlixelKey.O)) { + s1.changeRotation(-speed); + } + + if (Flixel.keyJustPressed(FlixelKey.NUM_1)) { + spriteGroup.setRotationMode(FlixelSpriteGroup.RotationMode.INDIVIDUAL); + } + if (Flixel.keyJustPressed(FlixelKey.NUM_2)) { + spriteGroup.setRotationMode(FlixelSpriteGroup.RotationMode.WHEEL); + } + if (Flixel.keyJustPressed(FlixelKey.NUM_3)) { + spriteGroup.setRotationMode(FlixelSpriteGroup.RotationMode.ORBIT); + } + + if (Flixel.keyPressed(FlixelKey.LEFT)) { + spriteGroup.changeRotation(speed); + } + if (Flixel.keyPressed(FlixelKey.RIGHT)) { + spriteGroup.changeRotation(-speed); + } + + if (Flixel.keyJustPressed(FlixelKey.SPACE)) { + logo.playAnimation("test", true); + } + + if (Flixel.keyJustPressed(FlixelKey.T)) { + tween.start(); + } + + if (Flixel.keyJustPressed(FlixelKey.R)) { + tween.reset(); + } + + if (Flixel.keyJustPressed(FlixelKey.Y)) { + if (tween.paused) { + tween.resume(); + } else { + tween.pause(); + } + } + + if (Flixel.keyJustPressed(FlixelKey.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 index 8fd5196..1981026 100644 --- a/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java +++ b/funkin/src/main/java/me/stringdotjar/funkin/util/FunkinConstants.java @@ -8,7 +8,7 @@ public final class FunkinConstants { /** * The default title for the game's window. */ - public static final String WINDOW_TITLE = "Friday Night Funkin': Java Edition"; + public static final String WINDOW_TITLE = "Polyverse Funkin'"; /** * How wide the window's viewport is in pixels. This also affects how wide the window is when diff --git a/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java b/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java index 7222c22..3ad634d 100644 --- a/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java +++ b/lwjgl3/src/main/java/me/stringdotjar/funkin/lwjgl3/Lwjgl3Launcher.java @@ -5,7 +5,7 @@ import com.badlogic.gdx.backends.lwjgl3.Lwjgl3WindowAdapter; import me.stringdotjar.flixelgdx.Flixel; import me.stringdotjar.funkin.FunkinGame; -import me.stringdotjar.funkin.InitScreen; +import me.stringdotjar.funkin.InitState; import me.stringdotjar.funkin.util.FunkinConstants; /** Launches the desktop (LWJGL3) application. */ @@ -23,7 +23,7 @@ private static void createApplication() { FunkinConstants.WINDOW_TITLE, FunkinConstants.WINDOW_WIDTH, FunkinConstants.WINDOW_HEIGHT, - new InitScreen() + new InitState() ); Flixel.initialize(game); // This is VERY important to do before creating the application! var size = game.getWindowSize(); diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java b/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java index a8383af..8e7942d 100644 --- a/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/Polyverse.java @@ -3,6 +3,7 @@ import com.badlogic.gdx.files.FileHandle; import groovy.lang.GroovyClassLoader; import me.stringdotjar.flixelgdx.Flixel; +import me.stringdotjar.flixelgdx.util.FlixelRuntimeUtil; import me.stringdotjar.polyverse.script.type.Script; import java.util.ArrayList; @@ -81,10 +82,7 @@ public static void registerScript(FileHandle handle) { var typeScripts = scripts.get(mostSpecificType); if (!typeScripts.contains(script)) { typeScripts.add(script); - Flixel.info( - "Polyverse", - "Registered Polyverse script \"" - + script.getClass().getSimpleName() + Flixel.info("Polyverse", "Registered Polyverse script \"" + script.getClass().getSimpleName() + "\" of script type \"" + mostSpecificType.getSimpleName() + "\"."); @@ -93,12 +91,17 @@ public static void registerScript(FileHandle handle) { script.onCreate(); } } catch (Exception e) { - Flixel.error("Polyverse", "Failed to load script: " + handle.path(), e); + StringBuilder errorWindowMessage = new StringBuilder(); + errorWindowMessage.append("There was an uncaught exception for a script during compilation.\n"); + errorWindowMessage.append("Location: ").append(handle.path()).append("\n"); + errorWindowMessage.append("Exception: ").append(e); + Flixel.error("Polyverse", "Failed to compile script: " + handle.path(), e); + FlixelRuntimeUtil.showErrorAlert("Polyverse Script Exception", errorWindowMessage); } } /** - * Executes an action for each script of a certain type, with error handling. + * Executes a function that pertains to a specific type for each script. * * @param type The class type of scripts to iterate over. * @param action The action to perform on each script. @@ -107,6 +110,21 @@ public static void forEachScript(Class type, Consumer a executeScriptList(getScripts(type), action); } + /** + * Executes a function for all scripts that are successors (subclasses or implementations) that are + * of a specific type. + * + * @param type The class type of scripts to iterate over, including its subtypes. + * @param action The function to perform on each script. + */ + public static void forEachScriptSuccessor(Class type, Consumer action) { + for (Class scriptType : scripts.keySet()) { + if (type.isAssignableFrom(scriptType)) { + executeScriptList(getScripts(scriptType), 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 @@ -121,10 +139,10 @@ public static void forAllScripts(Consumer 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. + 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. Plus, it's also to prevent stuttering since we are using + // ArrayLists for storing scripts. for (int i = 0; i < scriptList.size(); i++) { @SuppressWarnings("unchecked") T script = (T) scriptList.get(i); @@ -132,7 +150,12 @@ private static void executeScriptList( try { action.accept(script); } catch (Exception e) { - Flixel.error("Polyverse", "Error in " + script.getClass().getSimpleName(), e); + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append("There was an uncaught exception for a script when executing it.\n"); + errorMsg.append("Script ID: \"").append(script.getId()).append("\"\n"); + errorMsg.append("Exception: ").append(e); + Flixel.error("Polyverse", errorMsg, e); + FlixelRuntimeUtil.showErrorAlert("Polyverse Script Exception", errorMsg); } } } diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/AnotherType.java b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/AnotherType.java new file mode 100644 index 0000000..6e3e4fd --- /dev/null +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/AnotherType.java @@ -0,0 +1,8 @@ +package me.stringdotjar.polyverse.script.type; + +public abstract class AnotherType extends SystemScript { + + public AnotherType(String id) { + super(id); + } +} diff --git a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java index 6b41e28..bfc6788 100644 --- a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/Script.java @@ -4,7 +4,7 @@ public abstract class Script { /** The unique identifier {@code this} script. */ - protected String id; + private final String id; public Script(String id) { this.id = id; 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 index 1024e0e..0dd14fc 100644 --- a/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java +++ b/polyverse/src/main/java/me/stringdotjar/polyverse/script/type/SystemScript.java @@ -14,4 +14,6 @@ public void onWindowUnfocused() {} /** Called when the game window is minimized. */ public void onWindowMinimized(boolean iconified) {} + + public void rawr() {} }