diff --git a/CHANGELOG.md b/CHANGELOG.md index 648e281d0..95fbeebe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,20 +4,34 @@ All notable changes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + ## [0.6.2] - 2025-03-31 + ### Added -- 0.6 credits list updated + +- Updated the 0.6 credits list + ### Fixed -- Additional shader fix for Stress Pico crashing at the end (was the same issue as Senpai Pico shader error, just in a different shaderfile) + +- Additional shader fix for Stress (Pico Mix) crashing at the end (was the same issue as Senpai Pico shader error, just in a different shaderfile) + + ## [0.6.1] - 2025-03-31 + ### Fixed + - Hopefully Senpai Pico/Erect mix shader isn't brokey - NG API encryption key was added proper, so medals + leaderboards posting should work + + ## [0.6.0] - 2025-03-31 The Pit Stop 2 update! + ### Added + - Added six (!) new playable songs! Check them out in the Freeplay menu for their respective characters. - Cocoa (Pico Mix) - Senpai (Pico Mix) @@ -37,57 +51,55 @@ The Pit Stop 2 update! - Added new stickers which appear when exiting Pico songs! - The new sticker system isn't fully available to mods yet, but we're working on it! - New option in the Preferences menu: Strumline Backgrounds! -- Options in the Preferences menu now display an on-screen description when selected. -- Added a little easter egg to Pico's Good Results Screen animation. +- New song event type in the Chart Editor: Set Health Icon! + - This event is now used in Stress (Pico Mix). - The HOME and END keys now jump to the top and bottom of the Freeplay song list, respectively. ([bb974c2](https://github.com/FunkinCrew/Funkin/commit/bb974c264270d10ff503784063e5d77bb352b3f7)) - by @AbnormalPoof in [#4103](https://github.com/FunkinCrew/Funkin/pull/4103) - Added an option to launch the game in fullscreen. ([ee53ccd](https://github.com/FunkinCrew/Funkin/commit/ee53ccd32721e0790adfe82c60d4aca419db0a7f)) - by @AbnormalPoof in [#3738](https://github.com/FunkinCrew/Funkin/pull/3738) -- Added descriptions for each item in the Preferences menu. ([a17b0e8](https://github.com/FunkinCrew/Funkin/commit/a17b0e8b3cc1d56fcdc0b51eaca9fd57cdb5bce0)) - by @anysad in [#3872](https://github.com/FunkinCrew/Funkin/pull/3872) +- Added on-screen descriptions for each item in the Preferences menu. ([a17b0e8](https://github.com/FunkinCrew/Funkin/commit/a17b0e8b3cc1d56fcdc0b51eaca9fd57cdb5bce0)) - by @anysad in [#3872](https://github.com/FunkinCrew/Funkin/pull/3872) - Added precise scrolling in the Chart Editor using Ctrl-Mouse Wheel. ([0d8e4a5](https://github.com/FunkinCrew/Funkin/commit/0d8e4a53305d6d069454812766300122f3581e31)) - by @ninjamuffin99 in [#3806](https://github.com/FunkinCrew/Funkin/pull/3806) - Added a “None” option to the character selector in the Chart Editor. ([d9637d3](https://github.com/FunkinCrew/Funkin/commit/d9637d3a19466b9fc68a102676c46a74ef504909)) - by @Lasercar in [#4279](https://github.com/FunkinCrew/Funkin/pull/4279) - Added the ability to flip the character in the Animation Editor. ([de02137](https://github.com/FunkinCrew/Funkin/commit/de02137d7c7d1779e85aeda34743f506a5b9cc27)) - by @AbnormalPoof in [#3028](https://github.com/FunkinCrew/Funkin/pull/3028) - Added offsets support for album titles. ([69d8570](https://github.com/FunkinCrew/Funkin/commit/69d8570a9eb06011ed6dd95fcbef83d90f7f8684)) - by @AbnormalPoof in [#3618](https://github.com/FunkinCrew/Funkin/pull/3618) - Added three new properties to stage data: `angle`, `scroll`, and `alpha`. ([ff56b19](https://github.com/FunkinCrew/Funkin/commit/ff56b1948aef42bbb6bb4ede4f9b2012d49ab044)) - by @AbnormalPoof in [#3720](https://github.com/FunkinCrew/Funkin/pull/3720) - Added script events for losing/gaining focus. ([4b127b6](https://github.com/FunkinCrew/Funkin/commit/4b127b64130f6f753d0574ec66a1672322e4bd13)) - by @AbnormalPoof in [#3721](https://github.com/FunkinCrew/Funkin/pull/3721) -- Added 10 new functions to `ReflectUtil`.([6216655](https://github.com/FunkinCrew/Funkin/commit/62166554e7a176245d1a63bd15122033044c4e40)) - by @AbnormalPoof in [#3622](https://github.com/FunkinCrew/Funkin/pull/3622), [#3809](https://github.com/FunkinCrew/Funkin/pull/3809), and [#4019](https://github.com/FunkinCrew/Funkin/pull/4019) +- Added 10 new functions to `ReflectUtil`. ([6216655](https://github.com/FunkinCrew/Funkin/commit/62166554e7a176245d1a63bd15122033044c4e40)) - by @AbnormalPoof in [#3622](https://github.com/FunkinCrew/Funkin/pull/3622), [#3809](https://github.com/FunkinCrew/Funkin/pull/3809), and [#4019](https://github.com/FunkinCrew/Funkin/pull/4019) - Added `DEBUG_BUILD` value to `Constants` to indicate whether a build has debug functions enabled. ([ad45b72](https://github.com/FunkinCrew/Funkin/commit/ad45b72b1ae8eb73a12dc51bcb59f66cc55e7bbd)) - by @AbnormalPoof in [#3853](https://github.com/FunkinCrew/Funkin/pull/3853) ### Changed + - Switched from hxCodec to hxvlc for video playback. This may break a mod or two. - Check the [Funkin Modding Docs](https://funkincrew.github.io/funkin-modding-docs/09-migration/09-02-0.5.0-to-0.6.0.html) for more info on how to update your mods. - Polymod should now ignore `.git` files when loading mods. -- Lots of improvements to issue and pull request organization. - by @Hundrec and @AbnormalPoof +- The pause menu can now be opened and closed rapidly. +- Adjusted difficulty ratings and scroll speeds for many songs. +- Chart Editor event fields now allow for values to be as specific as desired. + - For example, the Zoom Camera event can now be set to 0.9857. +- Lots of improvements to GitHub issue and pull request organization. - by @Hundrec and @AbnormalPoof - Overhauled the Changelog to improve readability and properly credit contributors. ([4383fcf](https://github.com/FunkinCrew/Funkin/commit/4383fcf32c280a1c0ee7b9c80d255611d497cabc)) - by @Hundrec in [#4296](https://github.com/FunkinCrew/Funkin/pull/4296) and [#4298](https://github.com/FunkinCrew/Funkin/pull/4298) - Made various improvements to the screenshot function. ([9ce7bbc](https://github.com/FunkinCrew/Funkin/commit/9ce7bbcfbb8f30ae120c876194f89bc4c787f585)) - by @Lasercar in [#4082](https://github.com/FunkinCrew/Funkin/pull/4082) - Accept keybinds (Z and Space by default) can now be used to exit the Results screen. ([edb270d](https://github.com/FunkinCrew/Funkin/commit/edb270d15e41784dccbf75639ac731840e80fe23)) - by @JVNpixels in [#3799](https://github.com/FunkinCrew/Funkin/pull/3799) - Reordered UI keybinds in the controls menu for consistency. ([a01bcc3](https://github.com/FunkinCrew/Funkin/commit/a01bcc3da836ec52851ca9de13ef459daf61269a)) - by @lemz1 in [#3027](https://github.com/FunkinCrew/Funkin/pull/3027) - New save files now have default Freeplay controls for gamepads. ([2b7f62e](https://github.com/FunkinCrew/Funkin/commit/2b7f62edd33de5527e259d9e5643f926d35da734)) - by @MrMadera in [#3934](https://github.com/FunkinCrew/Funkin/pull/3934) -- Renamed “Auto Pause” preference to “Pause on Unfocus” for clarity. ([49a21c1](https://github.com/FunkinCrew/Funkin/commit/49a21c198236fbecbe5902bb106f78939cc6442a)) - by @JackXson-Real in [#4346](https://github.com/FunkinCrew/Funkin/pull/4346) - Made scrolling smoother in the Chart Editor. ([20d9016](https://github.com/FunkinCrew/Funkin/commit/20d90169845f1e50f849e39f4c5f818359756c78)) - by @ninjamuffin99 in [#3768](https://github.com/FunkinCrew/Funkin/pull/3768) -- The Chart Editor now clears the undo/redo history after loading a new song. ([c7bdc1a](https://github.com/FunkinCrew/Funkin/commit/c7bdc1abe265678468920ca3cc3d7eface5d6925)) - by @Lasercar in [#4308](https://github.com/FunkinCrew/Funkin/pull/4308) - Mods with missing dependencies are now skipped instead of preventing all mods from loading. ([1c2fb43](https://github.com/FunkinCrew/Funkin/commit/1c2fb43ae16cf40be5ef94c40b047e8e772b1211)) - by @AbnormalPoof in [#3993](https://github.com/FunkinCrew/Funkin/pull/3993) - Slightly improved flexibility for modding note hit animations. ([3aad825](https://github.com/FunkinCrew/Funkin/commit/3aad825f865c4ed87016983d44121e2c1610d332)) - by @TechnikTil in [#3936](https://github.com/FunkinCrew/Funkin/pull/3936) - Introduced several QoL modding changes. ([785c4be](https://github.com/FunkinCrew/Funkin/commit/785c4be88b52dc1b5899013822fc004ba7d9894d)) - by @Kade-github in [#4009](https://github.com/FunkinCrew/Funkin/pull/4009) -- Lots of smaller bug fixes. +- Lots of smaller changes. -## New Contributors for 0.6.0 +### Fixed -* @PatoFlamejanteTV made their first contribution in [#3843](https://github.com/FunkinCrew/Funkin/pull/3843) -* @sphis-Sinco made their first contribution in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881) -* @MrMadera made their first contribution in [#3934](https://github.com/FunkinCrew/Funkin/pull/3934) -* @Lasercar made their first contribution in [#4100](https://github.com/FunkinCrew/Funkin/pull/4065) -* @MidyGamy made their first contribution in [#4068](https://github.com/FunkinCrew/Funkin/pull/4068) -* @MrScottyPieey made their first contribution in [#4085](https://github.com/FunkinCrew/Funkin/pull/4085) -* @JackXson-Real made their first contribution in [#4346](https://github.com/FunkinCrew/Funkin/pull/4346) - -# Fixed +- Shaders no longer create thin seams within atlas sprites. +- Completing a song in Practice Mode no longer plays a new rank animation in the Freeplay menu. +- Fixed lots of charting issues across many songs. +- The Chart Editor grid now properly adjusts to the new BPM after switching variations. +- Fixed a few crashes in the Stage Editor. - Fixed a bug where the song would restart from the beginning instead of moving to the Results screen. ([3667c51](https://github.com/FunkinCrew/Funkin/commit/3667c51c1efe14cfe7c810e2f35991f08f50781a)) - by @KoloInDaCrib in [#4309](https://github.com/FunkinCrew/Funkin/pull/4309) and @Lasercar in [#4330](https://github.com/FunkinCrew/Funkin/pull/4330) - Reduced stuttering when resyncing instrumental and voices tracks. ([22d41d2](https://github.com/FunkinCrew/Funkin/commit/22d41d21b88acb7422a0afcda8414682710bd2ed)) - by @TechnikTil in [#3955](https://github.com/FunkinCrew/Funkin/pull/3955) - Songs with only instrumental tracks no longer stutter. ([dfe02ec](https://github.com/FunkinCrew/Funkin/commit/dfe02ec668b61d6308f459c978d12a7487f9dc28)) - by @KoloInDaCrib in [#3861](https://github.com/FunkinCrew/Funkin/pull/3861) -- The mouse cursor is no longer visible outside of debug editors. ([1c12b84](https://github.com/FunkinCrew/Funkin/commit/1c12b8467eca350eb28138473360d5358fa620e2)) - by @sphis-Sinco in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881) +- The debug mouse cursor no longer flickers before the Title Screen loads. ([1c12b84](https://github.com/FunkinCrew/Funkin/commit/1c12b8467eca350eb28138473360d5358fa620e2)) - by @sphis-Sinco in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881) - Unbound keys now display as [N/A] instead of crashing the game. ([94e5d3c](https://github.com/FunkinCrew/Funkin/commit/94e5d3ca258cbaa5d9ab6f0cb26feda83b3433a2)) - by @NotHyper-474 in [#4355](https://github.com/FunkinCrew/Funkin/pull/4355) - Songs can no longer be spam-selected after selecting an instrumental in Freeplay. ([0e0c4ae](https://github.com/FunkinCrew/Funkin/commit/0e0c4aeb7745cfb9479685ccbb635cf3743cddbb)) - by @AbnormalPoof in [#3866](https://github.com/FunkinCrew/Funkin/pull/3866) - The Random capsule can now switch to Erect/Nightmare difficulties in Freeplay. ([6f32747](https://github.com/FunkinCrew/Funkin/commit/6f327474f30374f50774f432ea3ccb6d02579445)) - by @KoloInDaCrib in [#3838](https://github.com/FunkinCrew/Funkin/pull/3838) -- The millions place digit of the Freeplay score now updates properly. ([3719ca6](https://github.com/FunkinCrew/Funkin/commit/3719ca6ce7a170c3c890cdb2aeea3e4534b91736)) - by @Lasercar in [#4065](https://github.com/FunkinCrew/Funkin/pull/4065) - Fixed a rare bug where a song would not register as beaten. ([a3e2373](https://github.com/FunkinCrew/Funkin/commit/a3e23733db104b1ef00cfcff17db3a5d032a4d67)) - by @AbnormalPoof in [#3820](https://github.com/FunkinCrew/Funkin/pull/3820) - The difficulty graphic on the Results screen no longer cuts off incorrectly. ([b13bf05](https://github.com/FunkinCrew/Funkin/commit/b13bf05d16ff2977309e0c7ba3f049c0134e8902)) - by @AbnormalPoof in [#4161](https://github.com/FunkinCrew/Funkin/pull/4161) - Four-digit long Total Notes values in the Results screen no longer overflow to the right. ([2be4c0c](https://github.com/FunkinCrew/Funkin/commit/2be4c0c7196e018a43f9697f015179f3efc3fcdb)) - by @Hundrec in [#4356](https://github.com/FunkinCrew/Funkin/pull/4356) @@ -99,6 +111,16 @@ The Pit Stop 2 update! - Removed an unused class from Polymod blacklist. ([06c12e3](https://github.com/FunkinCrew/Funkin/commit/06c12e36c6bd6df4e2be32a3bec540172e79e162)) - by @AbnormalPoof in [#3729](https://github.com/FunkinCrew/Funkin/pull/3729) - Many additional small bug fixes. +## New Contributors for 0.6.0 + +* @PatoFlamejanteTV made their first contribution in [#3843](https://github.com/FunkinCrew/Funkin/pull/3843) +* @sphis-Sinco made their first contribution in [#3881](https://github.com/FunkinCrew/Funkin/pull/3881) +* @MrMadera made their first contribution in [#3934](https://github.com/FunkinCrew/Funkin/pull/3934) +* @MidyGamy made their first contribution in [#4068](https://github.com/FunkinCrew/Funkin/pull/4068) +* @Lasercar made their first contribution in [#4082](https://github.com/FunkinCrew/Funkin/pull/4082) +* @MrScottyPieey made their first contribution in [#4085](https://github.com/FunkinCrew/Funkin/pull/4085) + + ## [0.5.3] - 2024-10-18 This patch resolves a critical issue that could cause user's save data to become corrupted. It is recommended that users switch to this version immediately and avoid using version 0.5.2. diff --git a/hmm.json b/hmm.json index 8b7dffea4..cc1582a75 100644 --- a/hmm.json +++ b/hmm.json @@ -143,7 +143,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "d1322e60f97b5c6e977f9e3e8a04f22b5190e7d9", + "ref": "e41e3d428ccdd13a1197b20c353f7c40acb45e96", "url": "https://github.com/FunkinCrew/lime" }, { @@ -231,4 +231,4 @@ "url": "https://github.com/MAJigsaw77/extension-androidtools" } ] -} +} \ No newline at end of file diff --git a/source/Main.hx b/source/Main.hx index 0d1d2935f..a00620c3d 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -15,6 +15,7 @@ import openfl.events.Event; import openfl.Lib; import openfl.media.Video; import openfl.net.NetStream; +import funkin.util.WindowUtil; /** * The main class which initializes HaxeFlixel and starts the game in its initial state. @@ -79,6 +80,8 @@ class Main extends Sprite funkin.Preferences.lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame"); #end + WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode); + if (hasEventListener(Event.ADDED_TO_STAGE)) { removeEventListener(Event.ADDED_TO_STAGE, init); diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 70f191b6b..a69f1908f 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -19,6 +19,7 @@ import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.freeplay.style.FreeplayStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.song.SongRegistry; +import funkin.data.stickers.StickerRegistry; import funkin.data.event.SongEventRegistry; import funkin.data.stage.StageRegistry; import funkin.data.story.level.LevelRegistry; @@ -220,6 +221,7 @@ class InitState extends FlxState FreeplayStyleRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries(); + StickerRegistry.instance.loadEntries(); // TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers and more prone to syntax errors. // Move it to use a BaseRegistry. @@ -280,6 +282,7 @@ class InitState extends FlxState songId: "cum", characterId: "pico", difficultyId: "nightmare", + variationId: "erect", isNewHighscore: true, scoreData: { diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx index 12b2b0e9f..00fc2e3d7 100644 --- a/source/funkin/Preferences.hx +++ b/source/funkin/Preferences.hx @@ -4,6 +4,7 @@ package funkin; import funkin.mobile.ui.FunkinHitbox; #end import funkin.save.Save; +import funkin.util.WindowUtil; /** * A core class which provides a store of user-configurable, globally relevant values. @@ -204,6 +205,27 @@ class Preferences return value; } + /** + * If enabled, the game will utilize VSync (or adaptive VSync) on startup. + * @default `OFF` + */ + public static var vsyncMode(get, set):lime.ui.WindowVSyncMode; + + static function get_vsyncMode():lime.ui.WindowVSyncMode + { + return Save?.instance?.options?.vsyncMode ?? lime.ui.WindowVSyncMode.OFF; + } + + static function set_vsyncMode(value:lime.ui.WindowVSyncMode):lime.ui.WindowVSyncMode + { + WindowUtil.setVSyncMode(value); + + var save:Save = Save.instance; + save.options.vsyncMode = value; + save.flush(); + return value; + } + public static var unlockedFramerate(get, set):Bool; static function get_unlockedFramerate():Bool @@ -368,6 +390,7 @@ class Preferences { // Apply the autoPause setting (enables automatic pausing on focus lost). FlxG.autoPause = Preferences.autoPause; + // WindowUtil.setVSyncMode(Preferences.vsyncMode); // Apply the debugDisplay setting (enables the FPS and RAM display). toggleDebugDisplay(Preferences.debugDisplay); #if web diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 83413ad00..dad238478 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -16,6 +16,7 @@ typedef EntryConstructorFunction = String->Void; * @param J The type of the JSON data used when constructing. */ @:generic +@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry()) abstract class BaseRegistry & Constructible), J> { /** @@ -189,6 +190,19 @@ abstract class BaseRegistry & Constructible + */ + // public function listBaseGameEntryIds():Array {} + + /** + * A list of all entries that are not included in the base game. + * @return Array + */ + // public function listModdedEntryIds():Array {} + public function toString():String { return 'Registry(' + registryId + ', ${countEntries()} entries)'; diff --git a/source/funkin/data/DefaultRegistryImpl.hx b/source/funkin/data/DefaultRegistryImpl.hx new file mode 100644 index 000000000..f7c137fbb --- /dev/null +++ b/source/funkin/data/DefaultRegistryImpl.hx @@ -0,0 +1,9 @@ +package funkin.data; + +/** + * An interface which automatically implements essential fields for a class extending `BaseRegistry`. + * + * @see `funkin.data.BaseRegistry` + */ +@:autoBuild(funkin.util.macro.RegistryMacro.buildRegistry()) +interface DefaultRegistryImpl {} diff --git a/source/funkin/data/IRegistryEntry.hx b/source/funkin/data/IRegistryEntry.hx index ff506767d..70f464f86 100644 --- a/source/funkin/data/IRegistryEntry.hx +++ b/source/funkin/data/IRegistryEntry.hx @@ -5,6 +5,7 @@ package funkin.data; * A `String->Void` constructor is also mandatory, but enforced elsewhere. * @param T The JSON data type of the registry entry. */ +@:autoBuild(funkin.util.macro.RegistryMacro.buildEntry()) interface IRegistryEntry { public final id:String; diff --git a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx index fad1e43ad..2f79cf973 100644 --- a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx +++ b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx @@ -2,8 +2,10 @@ package funkin.data.dialogue.conversation; import funkin.play.cutscene.dialogue.Conversation; import funkin.play.cutscene.dialogue.ScriptedConversation; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class ConversationRegistry extends BaseRegistry +class ConversationRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the dialogue box data format. @@ -14,74 +16,8 @@ class ConversationRegistry extends BaseRegistry public static final CONVERSATION_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - public static var instance(get, never):ConversationRegistry; - static var _instance:Null = null; - - static function get_instance():ConversationRegistry - { - if (_instance == null) _instance = new ConversationRegistry(); - return _instance; - } - public function new() { super('CONVERSATION', 'dialogue/conversations', CONVERSATION_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Conversation - { - return ScriptedConversation.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedConversation.listScriptClasses(); - } } diff --git a/source/funkin/data/dialogue/dialoguebox/DialogueBoxRegistry.hx b/source/funkin/data/dialogue/dialoguebox/DialogueBoxRegistry.hx index 5c15a72ef..681f8566f 100644 --- a/source/funkin/data/dialogue/dialoguebox/DialogueBoxRegistry.hx +++ b/source/funkin/data/dialogue/dialoguebox/DialogueBoxRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.dialogue.dialoguebox; import funkin.play.cutscene.dialogue.DialogueBox; import funkin.data.dialogue.dialoguebox.DialogueBoxData; import funkin.play.cutscene.dialogue.ScriptedDialogueBox; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class DialogueBoxRegistry extends BaseRegistry +class DialogueBoxRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the dialogue box data format. @@ -15,74 +17,8 @@ class DialogueBoxRegistry extends BaseRegistry public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.2.0"; - public static var instance(get, never):DialogueBoxRegistry; - static var _instance:Null = null; - - static function get_instance():DialogueBoxRegistry - { - if (_instance == null) _instance = new DialogueBoxRegistry(); - return _instance; - } - public function new() { super('DIALOGUEBOX', 'dialogue/boxes', DIALOGUEBOX_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):DialogueBox - { - return ScriptedDialogueBox.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedDialogueBox.listScriptClasses(); - } } diff --git a/source/funkin/data/dialogue/speaker/SpeakerRegistry.hx b/source/funkin/data/dialogue/speaker/SpeakerRegistry.hx index 1692ee0a1..96e85aca2 100644 --- a/source/funkin/data/dialogue/speaker/SpeakerRegistry.hx +++ b/source/funkin/data/dialogue/speaker/SpeakerRegistry.hx @@ -2,8 +2,10 @@ package funkin.data.dialogue.speaker; import funkin.play.cutscene.dialogue.Speaker; import funkin.play.cutscene.dialogue.ScriptedSpeaker; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class SpeakerRegistry extends BaseRegistry +class SpeakerRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the speaker data format. @@ -14,74 +16,8 @@ class SpeakerRegistry extends BaseRegistry public static final SPEAKER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - public static var instance(get, never):SpeakerRegistry; - static var _instance:Null = null; - - static function get_instance():SpeakerRegistry - { - if (_instance == null) _instance = new SpeakerRegistry(); - return _instance; - } - public function new() { super('SPEAKER', 'dialogue/speakers', SPEAKER_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Speaker - { - return ScriptedSpeaker.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedSpeaker.listScriptClasses(); - } } diff --git a/source/funkin/data/freeplay/album/AlbumRegistry.hx b/source/funkin/data/freeplay/album/AlbumRegistry.hx index c04464fd2..a7c3d3990 100644 --- a/source/funkin/data/freeplay/album/AlbumRegistry.hx +++ b/source/funkin/data/freeplay/album/AlbumRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.freeplay.album; import funkin.ui.freeplay.Album; import funkin.data.freeplay.album.AlbumData; import funkin.ui.freeplay.ScriptedAlbum; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class AlbumRegistry extends BaseRegistry +class AlbumRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the album data format. @@ -15,70 +17,8 @@ class AlbumRegistry extends BaseRegistry public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x'; - public static final instance:AlbumRegistry = new AlbumRegistry(); - public function new() { super('ALBUM', 'ui/freeplay/albums', ALBUM_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - * @param id The ID of the entry to load. - * @return The parsed data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser:json2object.JsonParser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - * @return The parsed data object. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser:json2object.JsonParser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Album - { - return ScriptedAlbum.init(clsName, 'unknown'); - } - - function getScriptedClassNames():Array - { - return ScriptedAlbum.listScriptClasses(); - } } diff --git a/source/funkin/data/freeplay/player/CHANGELOG.md b/source/funkin/data/freeplay/player/CHANGELOG.md index 7a31e11ca..ace9a27c2 100644 --- a/source/funkin/data/freeplay/player/CHANGELOG.md +++ b/source/funkin/data/freeplay/player/CHANGELOG.md @@ -5,5 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.1] +### Added +- Added `stickerPack` to specify which sticker pack to use when transitioning into and out of song featuring this character. + - If the value isn't present, it will use a default sticker pack instead. + ## [1.0.0] Initial release. diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index dc972657d..a6e962598 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -31,6 +31,15 @@ class PlayerData @:default(false) public var showUnownedChars:Bool = false; + /** + * The default sticker pack to use for songs featuring this playable character. + * Can be overridden by specific songs. + * @default `default` + */ + @:optional + @:default(funkin.util.Constants.DEFAULT_STICKER_PACK) + public var stickerPack:String = Constants.DEFAULT_STICKER_PACK; + /** * Which freeplay style to use for this character. */ diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx index 76b1c25c1..d584398e1 100644 --- a/source/funkin/data/freeplay/player/PlayerRegistry.hx +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -4,8 +4,10 @@ import funkin.data.freeplay.player.PlayerData; import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter; import funkin.save.Save; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class PlayerRegistry extends BaseRegistry +class PlayerRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the stage data format. @@ -16,15 +18,6 @@ class PlayerRegistry extends BaseRegistry public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - public static var instance(get, never):PlayerRegistry; - static var _instance:Null = null; - - static function get_instance():PlayerRegistry - { - if (_instance == null) _instance = new PlayerRegistry(); - return _instance; - } - /** * A mapping between stage character IDs and Freeplay playable character IDs. */ @@ -130,79 +123,4 @@ class PlayerRegistry extends BaseRegistry { return ownedCharacterIds.exists(characterId); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):PlayableCharacter - { - return ScriptedPlayableCharacter.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedPlayableCharacter.listScriptClasses(); - } - - /** - * A list of all the playable characters from the base game, in order. - */ - public function listBaseGamePlayerIds():Array - { - return ["bf", "pico"]; - } - - /** - * A list of all installed playable characters that are not from the base game. - */ - public function listModdedPlayerIds():Array - { - return listEntryIds().filter(function(id:String):Bool { - return listBaseGamePlayerIds().indexOf(id) == -1; - }); - } } diff --git a/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx b/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx index 626e2ac39..f6d75a86a 100644 --- a/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx +++ b/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.freeplay.style; import funkin.ui.freeplay.FreeplayStyle; import funkin.data.freeplay.style.FreeplayStyleData; import funkin.ui.freeplay.ScriptedFreeplayStyle; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class FreeplayStyleRegistry extends BaseRegistry +class FreeplayStyleRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the style data format. @@ -15,70 +17,8 @@ class FreeplayStyleRegistry extends BaseRegistry - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser:json2object.JsonParser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - * @return The parsed data object. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser:json2object.JsonParser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):FreeplayStyle - { - return ScriptedFreeplayStyle.init(clsName, 'unknown'); - } - - function getScriptedClassNames():Array - { - return ScriptedFreeplayStyle.listScriptClasses(); - } } diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx index 04bdd9190..c88aba6d5 100644 --- a/source/funkin/data/notestyle/NoteStyleRegistry.hx +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.notestyle; import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.notestyle.ScriptedNoteStyle; import funkin.data.notestyle.NoteStyleData; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class NoteStyleRegistry extends BaseRegistry +class NoteStyleRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the note style data format. @@ -15,15 +17,6 @@ class NoteStyleRegistry extends BaseRegistry public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.2.0"; - public static var instance(get, never):NoteStyleRegistry; - static var _instance:Null = null; - - static function get_instance():NoteStyleRegistry - { - if (_instance == null) _instance = new NoteStyleRegistry(); - return _instance; - } - public function new() { super('NOTESTYLE', 'notestyles', NOTE_STYLE_DATA_VERSION_RULE); @@ -33,61 +26,4 @@ class NoteStyleRegistry extends BaseRegistry { return fetchEntry(Constants.DEFAULT_NOTE_STYLE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):NoteStyle - { - return ScriptedNoteStyle.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedNoteStyle.listScriptClasses(); - } } diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md index 86c9f6912..e95c9d98d 100644 --- a/source/funkin/data/song/CHANGELOG.md +++ b/source/funkin/data/song/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.5] +### Added +- Added `playData.stickerPack` to specify which sticker pack to use when transitioning into and out of a song. + - If the value isn't present, it will use the character's sticker pack instead. + ## [2.2.4] ### Added - Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent. diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 074ed0b44..b5da02d00 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -471,6 +471,13 @@ class SongPlayData implements ICloneable @:optional public var album:Null; + /** + * The sticker pack for the song to use during transitions. + * If `null`, display the character's sticker pack. + */ + @:optional + public var stickerPack:Null; + /** * The start time for the audio preview in Freeplay. * Defaults to 0 seconds in. diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 75fbb637f..fcdabc91f 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -10,11 +10,13 @@ import funkin.play.song.ScriptedSong; import funkin.play.song.Song; import funkin.util.assets.DataAssets; import funkin.util.VersionUtil; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; using funkin.data.song.migrator.SongDataMigrator; @:nullSafety -class SongRegistry extends BaseRegistry +class SongRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the stage data format. @@ -40,19 +42,6 @@ class SongRegistry extends BaseRegistry return '${Constants.TITLE} - ${Constants.VERSION}'; } - /** - * TODO: What if there was a Singleton macro which automatically created the property for us? - */ - public static var instance(get, never):SongRegistry; - - static var _instance:Null = null; - - static function get_instance():SongRegistry - { - if (_instance == null) _instance = new SongRegistry(); - return _instance; - } - public function new() { super('SONG', 'songs', SONG_METADATA_VERSION_RULE); @@ -419,16 +408,6 @@ class SongRegistry extends BaseRegistry } } - function createScriptedEntry(clsName:String):Song - { - return ScriptedSong.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedSong.listScriptClasses(); - } - function loadEntryMetadataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; @@ -503,52 +482,6 @@ class SongRegistry extends BaseRegistry return chartData; } - /** - * A list of all the story weeks from the base game, in order. - * TODO: Should this be hardcoded? - */ - public function listBaseGameSongIds():Array - { - return [ - "tutorial", - "bopeebo", - "fresh", - "dadbattle", - "spookeez", - "south", - "monster", - "pico", - "philly-nice", - "blammed", - "satin-panties", - "high", - "milf", - "cocoa", - "eggnog", - "winter-horrorland", - "senpai", - "roses", - "thorns", - "ugh", - "guns", - "stress", - "darnell", - "lit-up", - "2hot", - "blazin" - ]; - } - - /** - * A list of all installed story weeks that are not from the base game. - */ - public function listModdedSongIds():Array - { - return listEntryIds().filter(function(id:String):Bool { - return listBaseGameSongIds().indexOf(id) == -1; - }); - } - /** * A list of all difficulties for a specific character. */ diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx index 6902e6209..0398222d8 100644 --- a/source/funkin/data/stage/StageRegistry.hx +++ b/source/funkin/data/stage/StageRegistry.hx @@ -2,8 +2,10 @@ package funkin.data.stage; import funkin.play.stage.Stage; import funkin.play.stage.ScriptedStage; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class StageRegistry extends BaseRegistry +class StageRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the stage data format. @@ -14,110 +16,8 @@ class StageRegistry extends BaseRegistry public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0"; - public static var instance(get, never):StageRegistry; - static var _instance:Null = null; - - static function get_instance():StageRegistry - { - if (_instance == null) _instance = new StageRegistry(); - return _instance; - } - public function new() { super('STAGE', 'stages', STAGE_DATA_VERSION_RULE); } - - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Stage - { - return ScriptedStage.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedStage.listScriptClasses(); - } - - /** - * A list of all the stages from the base game, in order. - * TODO: Should this be hardcoded? - */ - public function listBaseGameStageIds():Array - { - return [ - "mainStage", - "mainStageErect", - "spookyMansion", - "phillyTrain", - "phillyTrainErect", - "limoRide", - "limoRideErect", - "mallXmas", - "mallXmasErect", - "mallEvil", - "school", - "schoolEvil", - "tankmanBattlefield", - "phillyStreets", - "phillyStreetsErect", - "phillyBlazin", - ]; - } - - /** - * A list of all installed story weeks that are not from the base game. - */ - public function listModdedStageIds():Array - { - return listEntryIds().filter(function(id:String):Bool { - return listBaseGameStageIds().indexOf(id) == -1; - }); - } } diff --git a/source/funkin/data/stickers/CHANGELOG.md b/source/funkin/data/stickers/CHANGELOG.md new file mode 100644 index 000000000..041ecf2f4 --- /dev/null +++ b/source/funkin/data/stickers/CHANGELOG.md @@ -0,0 +1,9 @@ +# Sticker Pack Data Schema Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] +Initial release. Sticker packs have been restructured compared to previous versions to be less complicated. diff --git a/source/funkin/data/stickers/StickerData.hx b/source/funkin/data/stickers/StickerData.hx new file mode 100644 index 000000000..c23511e82 --- /dev/null +++ b/source/funkin/data/stickers/StickerData.hx @@ -0,0 +1,30 @@ +package funkin.data.stickers; + +/** + * A type definition for a sticker pack. + * It includes things like its name, the artist, and what sticker graphics to use. + * @see https://lib.haxe.org/p/json2object/ + */ +typedef StickerData = +{ + /** + * Semantic version of the sticker set data. + */ + public var version:String; + + /** + * Readable name of the sticker set. + */ + public var name:String; + + /** + * The artist of the sticker set. + */ + public var artist:String; + + /** + * The stickers in the set. + * This is simply a list of asset files to use. + */ + public var stickers:Array; +} diff --git a/source/funkin/data/stickers/StickerRegistry.hx b/source/funkin/data/stickers/StickerRegistry.hx new file mode 100644 index 000000000..08c2b9669 --- /dev/null +++ b/source/funkin/data/stickers/StickerRegistry.hx @@ -0,0 +1,89 @@ +package funkin.data.stickers; + +import funkin.data.stickers.StickerData; +import funkin.ui.transition.stickers.StickerPack; +import funkin.ui.transition.stickers.ScriptedStickerPack; + +class StickerRegistry extends BaseRegistry +{ + /** + * The current version string for the sticker pack data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migrateStickerData()` function. + */ + public static final STICKER_DATA_VERSION:thx.semver.Version = '1.0.0'; + + public static final STICKER_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x'; + + public static final instance:StickerRegistry = new StickerRegistry(); + + public function new() + { + super('STICKER', 'stickerpacks', STICKER_DATA_VERSION_RULE); + } + + public function fetchDefault():StickerPack + { + return fetchEntry(Constants.DEFAULT_STICKER_PACK); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + * @param id The ID of the entry to load. + * @return The parsed data object. + */ + public function parseEntryData(id:String):Null + { + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser:json2object.JsonParser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + + switch (loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + * @return The parsed data object. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser:json2object.JsonParser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):StickerPack + { + return ScriptedStickerPack.init(clsName, 'unknown'); + } + + function getScriptedClassNames():Array + { + return ScriptedStickerPack.listScriptClasses(); + } +} diff --git a/source/funkin/data/story/level/LevelRegistry.hx b/source/funkin/data/story/level/LevelRegistry.hx index dba8ce7f0..4e4f66019 100644 --- a/source/funkin/data/story/level/LevelRegistry.hx +++ b/source/funkin/data/story/level/LevelRegistry.hx @@ -3,8 +3,10 @@ package funkin.data.story.level; import funkin.util.SortUtil; import funkin.ui.story.Level; import funkin.ui.story.ScriptedLevel; +import funkin.util.tools.ISingleton; +import funkin.data.DefaultRegistryImpl; -class LevelRegistry extends BaseRegistry +class LevelRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the level data format. @@ -15,110 +17,41 @@ class LevelRegistry extends BaseRegistry public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0"; - public static var instance(get, never):LevelRegistry; - static var _instance:Null = null; - - static function get_instance():LevelRegistry - { - if (_instance == null) _instance = new LevelRegistry(); - return _instance; - } - public function new() { super('LEVEL', 'levels', LEVEL_DATA_VERSION_RULE); } - /** - * Read, parse, and validate the JSON data and produce the corresponding data object. - */ - public function parseEntryData(id:String):Null - { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - - switch (loadEntryFile(id)) - { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; - } - - if (parser.errors.length > 0) - { - printErrors(parser.errors, id); - return null; - } - return parser.value; - } - - /** - * Parse and validate the JSON data and produce the corresponding data object. - * - * NOTE: Must be implemented on the implementation class. - * @param contents The JSON as a string. - * @param fileName An optional file name for error reporting. - */ - public function parseEntryDataRaw(contents:String, ?fileName:String):Null - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(contents, fileName); - - if (parser.errors.length > 0) - { - printErrors(parser.errors, fileName); - return null; - } - return parser.value; - } - - function createScriptedEntry(clsName:String):Level - { - return ScriptedLevel.init(clsName, "unknown"); - } - - function getScriptedClassNames():Array - { - return ScriptedLevel.listScriptClasses(); - } - /** * A list of all the story weeks from the base game, in order. - * TODO: Should this be hardcoded? + * @return Array */ - public function listBaseGameLevelIds():Array + public function listBaseGameEntryIds():Array { + // This MUST be hard-coded (overriding the auto-generated method) + // because the auto-generated method spits out values in alphabetical order. return [ - "tutorial", - "week1", - "week2", - "week3", - "week4", - "week5", - "week6", - "week7", - "weekend1" + 'tutorial', + 'week1', + 'week2', + 'week3', + 'week4', + 'week5', + 'week6', + 'week7', + 'weekend1' ]; } + /** + * A list of all the story weeks in the game, in order. + * Modded levels are in alphabetical order at the end of the list. + * @return Array + */ public function listSortedLevelIds():Array { - var result = listEntryIds(); - result.sort(SortUtil.defaultsThenAlphabetically.bind(listBaseGameLevelIds())); + var result:Array = listEntryIds(); + result.sort(SortUtil.defaultsThenAlphabetically.bind(listBaseGameEntryIds())); return result; } - - /** - * A list of all installed story weeks that are not from the base game. - */ - public function listModdedLevelIds():Array - { - return listEntryIds().filter(function(id:String):Bool { - return listBaseGameLevelIds().indexOf(id) == -1; - }); - } } diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx index 11f1d5d66..f739f59b7 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -74,6 +74,11 @@ interface INoteScriptedClass extends IScriptedClass * Called when EITHER player (usually the player) misses a note. */ public function onNoteMiss(event:NoteScriptEvent):Void; + + /** + * Called when EITHER player (usually the player) drops a hold note. + */ + public function onNoteHoldDrop(event:HoldNoteScriptEvent):Void; } /** diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 3b6b588a7..a4b26140c 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -11,6 +11,7 @@ import funkin.play.notes.notekind.NoteKindManager; import funkin.data.song.SongRegistry; import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.stage.StageRegistry; +import funkin.data.stickers.StickerRegistry; import funkin.data.freeplay.album.AlbumRegistry; import funkin.modding.module.ModuleHandler; import funkin.play.character.CharacterData.CharacterDataParser; @@ -499,6 +500,7 @@ class PolymodHandler SpeakerRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries(); + StickerRegistry.instance.loadEntries(); CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry. NoteKindManager.loadScripts(); diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 43f8fc28b..6f4742a29 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -5,6 +5,7 @@ import funkin.data.song.SongData.SongEventData; import flixel.FlxState; import flixel.FlxSubState; import funkin.play.notes.NoteSprite; +import funkin.play.notes.SustainTrail; import funkin.play.cutscene.dialogue.Conversation; import funkin.play.Countdown.CountdownStep; import funkin.play.notes.NoteDirection; @@ -226,6 +227,48 @@ class GhostMissNoteScriptEvent extends ScriptEvent } } +class HoldNoteScriptEvent extends NoteScriptEvent +{ + /** + * The hold note that was hit (or dropped). + */ + public var holdNote:SustainTrail; + + /** + * The score the player received for hitting the note. + */ + public var score:Int; + + /** + * If the hit causes a combo break. + */ + public var isComboBreak:Bool = false; + + /** + * The time difference when the player hit the note + */ + public var hitDiff:Float = 0; + + /** + * Whether this note hit causes a note splash to display. + * Defaults to true only on "sick" notes. + */ + public var doesNotesplash:Bool = false; + + public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, cancelable:Bool = false):Void + { + super(type, null, healthChange, comboCount, true); + this.holdNote = holdNote; + this.score = score; + this.isComboBreak = isComboBreak; + } + + public override function toString():String + { + return 'HoldNoteScriptEvent(type=$type, holdNote=$holdNote, healthChange=$healthChange, score=$score, isComboBreak=$isComboBreak, cancelable=$cancelable)'; + } +} + /** * An event that is fired when the song reaches an event. */ diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index 15d4dfaf6..7d6e3992f 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -90,6 +90,9 @@ class ScriptEventDispatcher case NOTE_MISS: t.onNoteMiss(cast event); return; + case NOTE_HOLD_DROP: + t.onNoteHoldDrop(cast event); + return; default: // Continue; } } diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx index e44780dcf..02de0dc1a 100644 --- a/source/funkin/modding/events/ScriptEventType.hx +++ b/source/funkin/modding/events/ScriptEventType.hx @@ -88,6 +88,14 @@ enum abstract ScriptEventType(String) from String to String */ var NOTE_MISS = 'NOTE_MISS'; + /** + * Called when a character lets go of a hold note. + * Important information such as note data, player/opponent, etc. are all provided. + * + * This event is not cancelable. + */ + var NOTE_HOLD_DROP = 'NOTE_HOLD_DROP'; + /** * Called when a character presses a note when there was none there, causing them to lose health. * Important information such as direction pressed, etc. are all provided. diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index 57f0cc2da..d53639f09 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -57,6 +57,9 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte // TODO: Half of these aren't actually being called!!!!!!! + /** + * Called when ANY script event is dispatched. + */ public function onScriptEvent(event:ScriptEvent) {} /** @@ -71,55 +74,140 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte */ public function onDestroy(event:ScriptEvent) {} + /** + * Called every frame. + */ public function onUpdate(event:UpdateScriptEvent) {} + /** + * Called when the game is paused. + */ public function onPause(event:PauseScriptEvent) {} + /** + * Called when the game is resumed. + */ public function onResume(event:ScriptEvent) {} + /** + * Called when the song begins. + */ public function onSongStart(event:ScriptEvent) {} + /** + * Called when the song ends. + */ public function onSongEnd(event:ScriptEvent) {} + /** + * Called when the player dies. + */ public function onGameOver(event:ScriptEvent) {} + /** + * Called when a note on the strumline has been rendered and is now onscreen. + * This gets dispatched for both the player and opponent strumlines. + */ public function onNoteIncoming(event:NoteScriptEvent) {} + /** + * Called when a note has been hit. + * This gets dispatched for both the player and opponent strumlines. + */ public function onNoteHit(event:HitNoteScriptEvent) {} + /** + * Called when a note has been missed. + * This gets dispatched for both the player and opponent strumlines. + */ public function onNoteMiss(event:NoteScriptEvent) {} + public function onNoteHoldDrop(event:HoldNoteScriptEvent) {} + + /** + * Called when the player presses a key without any notes present. + */ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} + /** + * Called when a step is hit in the song. + */ public function onStepHit(event:SongTimeScriptEvent) {} + /** + * Called when a beat is hit in the song. + */ public function onBeatHit(event:SongTimeScriptEvent) {} + /** + * Called when a song event is triggered. + */ public function onSongEvent(event:SongEventScriptEvent) {} + /** + * Called when the countdown begins. + */ public function onCountdownStart(event:CountdownScriptEvent) {} + /** + * Called for every step in the countdown. + */ public function onCountdownStep(event:CountdownScriptEvent) {} + /** + * Called when the countdown ends, but BEFORE the song starts. + */ public function onCountdownEnd(event:CountdownScriptEvent) {} + /** + * Called when the song's chart has been parsed and loaded. + */ public function onSongLoaded(event:SongLoadScriptEvent) {} + /** + * Called when the game is about to switch to a new state. + */ public function onStateChangeBegin(event:StateChangeScriptEvent) {} + /** + * Called after the game has switched to a new state. + */ public function onStateChangeEnd(event:StateChangeScriptEvent) {} + /** + * Called when the game regains focus. + * This does not get called if "Auto Pause" is disabled. + */ public function onFocusGained(event:FocusScriptEvent) {} + /** + * Called when the game loses focus. + * This does not get called if "Auto Pause" is disabled. + */ public function onFocusLost(event:FocusScriptEvent) {} + /** + * Called when the game is about to open a substate. + */ public function onSubStateOpenBegin(event:SubStateScriptEvent) {} + /** + * Called when a substate has been opened. + */ public function onSubStateOpenEnd(event:SubStateScriptEvent) {} + /** + * Called when the game is about to close a substate. + */ public function onSubStateCloseBegin(event:SubStateScriptEvent) {} + /** + * Called when a substate has been closed. + */ public function onSubStateCloseEnd(event:SubStateScriptEvent) {} + /** + * Called when the song has been restarted. + */ public function onSongRetry(event:SongRetryEvent) {} } diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 5a6f13d72..af6be8935 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -497,8 +497,6 @@ class GameOverSubState extends MusicBeatSubState public function goBack() { - if (mustNotExit || isEnding) return; - isEnding = true; blueballed = false; PlayState.instance.deathCounter = 0; @@ -522,7 +520,7 @@ class GameOverSubState extends MusicBeatSubState } else { - var targetState:funkin.ui.transition.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> + var targetState:funkin.ui.transition.stickers.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker); if (PlayStatePlaylist.isStoryMode) @@ -530,16 +528,20 @@ class GameOverSubState extends MusicBeatSubState PlayStatePlaylist.reset(); } - var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player); - var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1"; - var stickerPack = switch (PlayState.instance.currentChart.song.id) - { - case "tutorial": "tutorial"; - case "darnell" | "lit-up" | "2hot": "weekend"; - default: "all"; - }; + var stickerPackId:Null = PlayState.instance.currentChart.stickerPack; - openSubState(new funkin.ui.transition.StickerSubState({targetState: targetState, stickerSet: stickerSet, stickerPack: stickerPack})); + if (stickerPackId == null) + { + var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player); + var playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER); + + if (playerCharacter != null) + { + stickerPackId = playerCharacter.getStickerPackID(); + } + } + + openSubState(new funkin.ui.transition.stickers.StickerSubState({targetState: targetState, stickerPack: stickerPackId})); } } diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index b253f242f..1aa980f73 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -805,8 +805,8 @@ class PauseSubState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - var targetState:funkin.ui.transition.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> new StoryMenuState(sticker) : (sticker) -> - FreeplayState.build(sticker); + var targetState:funkin.ui.transition.stickers.StickerSubState->FlxState = (PlayStatePlaylist.isStoryMode) ? (sticker) -> + new StoryMenuState(sticker) : (sticker) -> FreeplayState.build(sticker); // Do this AFTER because this resets the value of isStoryMode! if (PlayStatePlaylist.isStoryMode) @@ -814,20 +814,24 @@ class PauseSubState extends MusicBeatSubState PlayStatePlaylist.reset(); } + var stickerPackId:Null = PlayState.instance.currentChart.stickerPack; + + if (stickerPackId == null) + { + var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player); + var playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER); + + if (playerCharacter != null) + { + stickerPackId = playerCharacter.getStickerPackID(); + } + } + #if FEATURE_ADMOB_ADS AdMobUtil.removeBanner(); #end - var playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(PlayState.instance.currentChart.characters.player); - var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1"; - var stickerPack = switch (PlayState.instance.currentChart.song.id) - { - case "tutorial": "tutorial"; - case "darnell" | "lit-up" | "2hot": "weekend"; - default: "all"; - }; - - state.openSubState(new funkin.ui.transition.StickerSubState({targetState: targetState, stickerSet: stickerSet, stickerPack: stickerPack})); + state.openSubState(new funkin.ui.transition.stickers.StickerSubState({targetState: targetState, stickerPack: stickerPackId})); } /** diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index ff6be6dd6..575cb40e2 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -33,7 +33,6 @@ import funkin.api.newgrounds.Events; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData.CharacterDataParser; -import funkin.play.components.ComboMilestone; import funkin.play.components.HealthIcon; import funkin.play.components.PopUpStuff; import funkin.play.cutscene.dialogue.Conversation; @@ -901,8 +900,6 @@ class PlayState extends MusicBeatSubState startingSong = true; isPlayerDying = false; - inputSpitter = []; - // Reset music properly. if (FlxG.sound.music != null) { @@ -1604,35 +1601,6 @@ class PlayState extends MusicBeatSubState // Break up into individual lines to aid debugging. - var shouldShowComboText:Bool = false; - // TODO: Re-enable combo text (how to do this without sections?). - // if (currentSong != null) - // { - // shouldShowComboText = (Conductor.instance.currentBeat % 8 == 7); - // var daSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16)]; - // shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection); - // shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5); - // - // var daNextSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16) + 1]; - // var isEndOfSong = .getSong().length < Std.int(Conductor.instance.currentBeat / 16); - // shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection)); - // } - - if (shouldShowComboText) - { - var animShit:ComboMilestone = new ComboMilestone(-100, 300, Highscore.tallies.combo); - animShit.scrollFactor.set(0.6, 0.6); - animShit.zIndex = 1100; - animShit.cameras = [camHUD]; - add(animShit); - - var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation - - new FlxTimer().start(((Conductor.instance.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) { - animShit.forceFinish(); - }); - } - if (playerStrumline != null) playerStrumline.onBeatHit(); if (opponentStrumline != null) opponentStrumline.onBeatHit(); @@ -2523,7 +2491,7 @@ class PlayState extends MusicBeatSubState { // Call an event to allow canceling the note miss. // NOTE: This is what handles the character animations! - var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, -Constants.HEALTH_MISS_PENALTY, 0, true); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, 0, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2570,18 +2538,45 @@ class PlayState extends MusicBeatSubState // The player dropped a hold note. holdNote.handledMiss = true; - // Mute vocals and play miss animation, but don't penalize. + // Mute vocals and play miss animation. // vocals.playerVolume = 0; // if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true); + + if (!isBotPlayMode) + { + if (holdNote.sustainLength > Constants.HOLD_DROP_PENALTY_THRESHOLD_MS) + { + // Penalize the player for letting go of a hold note too early. + trace('Player dropped a hold note, penalizing... (has hit: ${holdNote.hitNote})'); + + // Different penalty based on whether the note itself was missed, + // or the note was hit and then the hold was dropped. + var remainingLengthSec = holdNote.sustainLength / Constants.MS_PER_SEC; + var healthChangeUncapped = remainingLengthSec * Constants.HEALTH_HOLD_DROP_PENALTY_PER_SECOND; + // If the base note of the hold was missed, don't penalize them more on top of that. + var healthChangeMax = Constants.HEALTH_HOLD_DROP_PENALTY_MAX - (holdNote.hitNote ? -Constants.HEALTH_MISS_PENALTY : 0); + var healthChange = healthChangeUncapped.clamp(healthChangeMax, 0); + var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec); + + var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true); + dispatchEvent(event); + + trace('Penalizing score by ${event.score} and health by ${event.healthChange} for dropping hold note (is combo break: ${event.isComboBreak})!'); + applyScore(event.score, 'miss', event.healthChange, event.isComboBreak); + + // Play the miss sound. + vocals.playerVolume = 0; + FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.5, 0.6)); + } + else + { + trace('Hold note too short, not penalizing...'); + } + } } } } - /** - * Spitting out the input for ravy 🙇‍♂️!! - */ - var inputSpitter:Array = []; - function handleSkippedNotes():Void { for (note in playerStrumline.notes.members) @@ -2772,31 +2767,10 @@ class PlayState extends MusicBeatSubState { if (pressArray[i]) indices.push(i); } - if (indices.length > 0) - { - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: indices[i], - l: 20 - }); - } - } - else - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: -1, - l: 20 - }); - } } vocals.playerVolume = 0; - applyScore(-10, 'miss', healthChange, true); + applyScore(Scoring.getMissScore(), 'miss', healthChange, true); if (playSound) { @@ -2817,7 +2791,7 @@ class PlayState extends MusicBeatSubState { var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in. hasPossibleNotes, // Whether there was a note you could have hit. - - 1 * Constants.HEALTH_MISS_PENALTY, // How much health to add (negative). + Constants.HEALTH_GHOST_MISS_PENALTY, // How much health to add (negative). - 10 // Amount of score to add (negative). ); dispatchEvent(event); @@ -2842,15 +2816,6 @@ class PlayState extends MusicBeatSubState { if (pressArray[i]) indices.push(i); } - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: indices[i], - l: 20 - }); - } } if (event.playSound) @@ -2924,8 +2889,6 @@ class PlayState extends MusicBeatSubState // SHIFT+PAGEDOWN: Skip backward twenty sections. if (FlxG.keys.justPressed.PAGEDOWN) changeSection(FlxG.keys.pressed.SHIFT ? -20 : -2); #end - - if (FlxG.keys.justPressed.B) trace(inputSpitter.join('\n')); } /** @@ -2991,27 +2954,6 @@ class PlayState extends MusicBeatSubState { if (pressArray[i]) indices.push(i); } - if (indices.length > 0) - { - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: indices[i], - l: 20 - }); - } - } - else - { - inputSpitter.push( - { - t: Std.int(Conductor.instance.songPosition), - d: -1, - l: 20 - }); - } } comboPopUps.displayRating(daRating); if (combo >= 10) comboPopUps.displayCombo(combo); @@ -3119,13 +3061,6 @@ class PlayState extends MusicBeatSubState dispatchEvent(event); if (event.eventCanceled) return; - #if sys - // spitter for ravy, teehee!! - var writer = new json2object.JsonWriter>(); - var output = writer.write(inputSpitter, ' '); - sys.io.File.saveContent("./scores.json", output); - #end - deathCounter = 0; // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. @@ -3160,7 +3095,7 @@ class PlayState extends MusicBeatSubState Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); #if FEATURE_NEWGROUNDS - Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, data.score); + Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, songScore); #end if (!isPracticeMode && !isBotPlayMode) @@ -3518,6 +3453,7 @@ class PlayState extends MusicBeatSubState storyMode: PlayStatePlaylist.isStoryMode, songId: currentChart.song.id, difficultyId: currentDifficulty, + variationId: currentVariation, characterId: currentChart.characters.player, title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), prevScoreData: prevScoreData, diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index e3b4a5dfa..a28c99d73 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -1,36 +1,38 @@ package funkin.play; -import funkin.ui.story.StoryMenuState; -import funkin.graphics.adobeanimate.FlxAtlasSprite; +import flixel.addons.display.FlxBackdrop; +import flixel.effects.FlxFlicker; import flixel.FlxSprite; import flixel.FlxSubState; -import funkin.graphics.FunkinSprite; -import flixel.effects.FlxFlicker; import flixel.graphics.frames.FlxBitmapFont; import flixel.group.FlxGroup.FlxTypedGroup; import funkin.ui.FullScreenScaleMode; import flixel.math.FlxPoint; -import funkin.ui.MusicBeatSubState; import flixel.math.FlxRect; import flixel.text.FlxBitmapText; -import funkin.data.freeplay.player.PlayerRegistry; -import funkin.data.freeplay.player.PlayerData.PlayerResultsAnimationData; -import funkin.ui.freeplay.charselect.PlayableCharacter; -import flixel.util.FlxColor; import flixel.tweens.FlxEase; -import funkin.graphics.FunkinCamera; -import funkin.ui.freeplay.FreeplayState; import flixel.tweens.FlxTween; -import flixel.addons.display.FlxBackdrop; -import funkin.audio.FunkinSound; +import flixel.util.FlxColor; import flixel.util.FlxGradient; import funkin.util.HapticUtil; import flixel.util.FlxTimer; -import funkin.play.scoring.Scoring; -import funkin.save.Save.SaveScoreData; +import funkin.audio.FunkinSound; +import funkin.data.freeplay.player.PlayerData.PlayerResultsAnimationData; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinCamera; +import funkin.graphics.FunkinSprite; import funkin.graphics.shaders.LeftMaskShader; -import funkin.play.components.TallyCounter; import funkin.play.components.ClearPercentCounter; +import funkin.play.components.TallyCounter; +import funkin.play.scoring.Scoring; +import funkin.play.song.Song; +import funkin.data.song.SongRegistry; +import funkin.save.Save.SaveScoreData; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.freeplay.FreeplayState; +import funkin.ui.MusicBeatSubState; +import funkin.ui.story.StoryMenuState; #if FEATURE_NEWGROUNDS import funkin.api.newgrounds.Medals; #end @@ -80,9 +82,10 @@ class ResultState extends MusicBeatSubState delay:Float }> = []; - var playerCharacterId:Null; + var playerCharacterId:Null = null; + var playerCharacter:Null = null; - var introMusicAudio:Null; + var introMusicAudio:Null = null; var rankBg:FunkinSprite; final cameraBG:FunkinCamera; @@ -181,7 +184,7 @@ class ResultState extends MusicBeatSubState // Fetch playable character data. Default to BF on the results screen if we can't find it. playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId); - var playerCharacter:Null = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); + playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); trace('Got playable character: ${playerCharacter?.getName()}'); // Query JSON data based on the rank, then use that to build the animation(s) the player sees. @@ -278,7 +281,7 @@ class ResultState extends MusicBeatSubState } } - var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}'; + var diffSpr:String = 'diff_${params?.difficultyId ?? Constants.DEFAULT_DIFFICULTY}'; difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr)); add(difficulty); @@ -824,7 +827,6 @@ class ResultState extends MusicBeatSubState override function update(elapsed:Float):Void { - // maskShaderSongName.swagSprX = songName.x; maskShaderDifficulty.swagSprX = difficulty.x; if (movingSongStuff) @@ -900,13 +902,18 @@ class ResultState extends MusicBeatSubState var shouldTween = false; var shouldUseSubstate = false; - var stickerSet = (playerCharacterId == "pico") ? "stickers-set-2" : "stickers-set-1"; - var stickerPack = switch (PlayState.instance?.currentChart?.song?.id) + var stickerPackId:Null = null; + + var song:Null = params.songId == null ? null : SongRegistry.instance.fetchEntry(params.songId); + + if (song != null) { - case "tutorial": "tutorial"; - case "darnell" | "lit-up" | "2hot": "weekend"; - default: "all"; - }; + stickerPackId = song.getStickerPackId(params?.difficultyId ?? Constants.DEFAULT_DIFFICULTY, params?.variationId ?? Constants.DEFAULT_VARIATION); + } + if (stickerPackId == null && playerCharacter != null) + { + stickerPackId = playerCharacter.getStickerPackID(); + } if (params.storyMode) { @@ -929,11 +936,10 @@ class ResultState extends MusicBeatSubState // No new characters. shouldTween = false; shouldUseSubstate = true; - targetState = new funkin.ui.transition.StickerSubState( + targetState = new funkin.ui.transition.stickers.StickerSubState( { targetState: (sticker) -> new StoryMenuState(sticker), - stickerSet: stickerSet, - stickerPack: stickerPack + stickerPack: stickerPackId }); } } @@ -966,11 +972,10 @@ class ResultState extends MusicBeatSubState { shouldTween = false; shouldUseSubstate = true; - targetState = new funkin.ui.transition.StickerSubState( + targetState = new funkin.ui.transition.stickers.StickerSubState( { targetState: (sticker) -> FreeplayState.build(null, sticker), - stickerSet: stickerSet, - stickerPack: stickerPack + stickerPack: stickerPackId }); } } @@ -1070,10 +1075,16 @@ typedef ResultsStateParams = /** * The difficulty ID of the song/week we just played. - * @default Normal + * @default `Constants.DEFAULT_DIFFICULTY` */ var ?difficultyId:String; + /** + * The variation ID of the song/week we just played. + * @default `Constants.DEFAULT_VARIATION` + */ + var ?variationId:String; + /** * The score, accuracy, and judgements. */ diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index a85b76688..edcf231e8 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -553,17 +553,40 @@ class BaseCharacter extends Bopper if (event.note.noteData.getMustHitNote() && characterType == BF) { - // If the note is from the same strumline, play the sing animation. + // If the note is from the same strumline, play the miss animation. this.playSingAnimation(event.note.noteData.getDirection(), true); } else if (!event.note.noteData.getMustHitNote() && characterType == DAD) { - // If the note is from the same strumline, play the sing animation. + // If the note is from the same strumline, play the miss animation. this.playSingAnimation(event.note.noteData.getDirection(), true); } else if (event.note.noteData.getMustHitNote() && characterType == GF) { - playComboDropAnimation(Highscore.tallies.combo); + playComboDropAnimation(event.comboCount); + } + } + + public override function onNoteHoldDrop(event:HoldNoteScriptEvent) + { + super.onNoteHoldDrop(event); + + // If another script cancelled the event, don't do anything. + if (event.eventCanceled) return; + + if (event.holdNote.noteData.getMustHitNote() && characterType == BF) + { + // If the note is from the same strumline, play the miss animation. + this.playSingAnimation(event.holdNote.noteData.getDirection(), true); + } + else if (!event.holdNote.noteData.getMustHitNote() && characterType == DAD) + { + // If the note is from the same strumline, play the miss animation. + this.playSingAnimation(event.holdNote.noteData.getDirection(), true); + } + else if (event.holdNote.noteData.getMustHitNote() && event.isComboBreak && characterType == GF) + { + playComboDropAnimation(event.comboCount); } } diff --git a/source/funkin/play/components/ComboMilestone.hx b/source/funkin/play/components/ComboMilestone.hx deleted file mode 100644 index 22ce2d671..000000000 --- a/source/funkin/play/components/ComboMilestone.hx +++ /dev/null @@ -1,126 +0,0 @@ -package funkin.play.components; - -import flixel.FlxSprite; -import flixel.group.FlxGroup.FlxTypedGroup; -import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; -import flixel.util.FlxTimer; -import funkin.audio.FunkinSound; - -class ComboMilestone extends FlxTypedSpriteGroup -{ - var effectStuff:FlxSprite; - - var wasComboSetup:Bool = false; - var daCombo:Int = 0; - - var grpNumbers:FlxTypedGroup; - - var onScreenTime:Float = 0; - - public function new(x:Float, y:Float, daCombo:Int = 0) - { - super(x, y); - - this.daCombo = daCombo; - - effectStuff = new FlxSprite(0, 0); - effectStuff.frames = Paths.getSparrowAtlas('comboMilestone'); - effectStuff.animation.addByPrefix('funny', 'NOTE COMBO animation', 24, false); - effectStuff.animation.play('funny'); - effectStuff.animation.finishCallback = function(nameThing) { - kill(); - }; - effectStuff.setGraphicSize(Std.int(effectStuff.width * 0.7)); - add(effectStuff); - - grpNumbers = new FlxTypedGroup(); - // add(grpNumbers); - } - - public function forceFinish():Void - { - if (onScreenTime < 0.9) - { - new FlxTimer().start((Conductor.instance.beatLengthMs / 1000) * 0.25, function(tmr) { - forceFinish(); - }); - } - else - effectStuff.animation.play('funny', true, false, 18); - } - - override function update(elapsed:Float) - { - onScreenTime += elapsed; - - if (effectStuff.animation.curAnim.curFrame == 17) effectStuff.animation.pause(); - - if (effectStuff.animation.curAnim.curFrame == 2 && !wasComboSetup) - { - setupCombo(daCombo); - } - - if (effectStuff.animation.curAnim.curFrame == 18) - { - grpNumbers.forEach(function(spr:ComboMilestoneNumber) { - spr.animation.reset(); - }); - } - - if (effectStuff.animation.curAnim.curFrame == 20) - { - grpNumbers.forEach(function(spr:ComboMilestoneNumber) { - spr.kill(); - }); - } - - super.update(elapsed); - } - - function setupCombo(daCombo:Int) - { - FunkinSound.playOnce(Paths.sound('comboSound')); - - wasComboSetup = true; - var loopNum:Int = 0; - - while (daCombo > 0) - { - var comboNumber:ComboMilestoneNumber = new ComboMilestoneNumber(450 - (100 * loopNum), 20 + 14 * loopNum, daCombo % 10); - comboNumber.setGraphicSize(Std.int(comboNumber.width * 0.7)); - grpNumbers.add(comboNumber); - add(comboNumber); - - loopNum += 1; - - daCombo = Math.floor(daCombo / 10); - } - } -} - -class ComboMilestoneNumber extends FlxSprite -{ - public function new(x:Float, y:Float, digit:Int) - { - super(x - 20, y); - - var stringNum:String = Std.string(digit); - frames = Paths.getSparrowAtlas('comboMilestoneNumbers'); - animation.addByPrefix(stringNum, stringNum, 24, false); - animation.play(stringNum); - updateHitbox(); - } - - var shiftedX:Bool = false; - - override function update(elapsed:Float) - { - if (animation.curAnim.curFrame == 2 && !shiftedX) - { - shiftedX = true; - x += 20; - } - - super.update(elapsed); - } -} diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx index fd7504725..a94de8f94 100644 --- a/source/funkin/play/cutscene/dialogue/Conversation.hx +++ b/source/funkin/play/cutscene/dialogue/Conversation.hx @@ -28,22 +28,11 @@ import funkin.util.EaseUtil; @:nullSafety class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry { - /** - * The ID of the conversation. - */ - public final id:String; - /** * The current state of the conversation. */ var state:ConversationState = ConversationState.Start; - /** - * Conversation data as parsed from the JSON file. - * `null` if the data could not be parsed or loaded. - */ - public final _data:Null; - /** * The current entry in the dialogue. */ @@ -659,16 +648,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl outroTween = null; } } - - public override function toString():String - { - return 'Conversation($id)'; - } - - static function _fetchData(id:String):Null - { - return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id)); - } } // Managing things with a single enum is a lot easier than a multitude of flags. diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx index 5ac6b5772..2d7b3b9d5 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx @@ -17,8 +17,6 @@ import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry; class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry { - public final id:String; - public var dialogueBoxName(get, never):String; function get_dialogueBoxName():String @@ -26,8 +24,6 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple return _data.name ?? 'UNKNOWN'; } - public final _data:DialogueBoxData; - /** * Offset the speaker's sprite by this much when playing each animation. */ @@ -420,14 +416,4 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple } public function onScriptEvent(event:ScriptEvent):Void {} - - public override function toString():String - { - return 'DialogueBox($id)'; - } - - static function _fetchData(id:String):Null - { - return DialogueBoxRegistry.instance.parseEntryDataWithMigration(id, DialogueBoxRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx index 98a1c3807..fb31e7f4d 100644 --- a/source/funkin/play/cutscene/dialogue/Speaker.hx +++ b/source/funkin/play/cutscene/dialogue/Speaker.hx @@ -17,16 +17,6 @@ import funkin.ui.FullScreenScaleMode; */ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry { - /** - * The internal ID for this speaker. - */ - public final id:String; - - /** - * The full data for a speaker. - */ - public final _data:SpeakerData; - /** * A readable name for this speaker. */ @@ -299,14 +289,4 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe } public function onScriptEvent(event:ScriptEvent):Void {} - - public override function toString():String - { - return 'Speaker($id)'; - } - - static function _fetchData(id:String):Null - { - return SpeakerRegistry.instance.parseEntryDataWithMigration(id, SpeakerRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx index 6f0ecb7ad..9cc125a31 100644 --- a/source/funkin/play/notes/notekind/NoteKind.hx +++ b/source/funkin/play/notes/notekind/NoteKind.hx @@ -66,6 +66,8 @@ class NoteKind implements INoteScriptedClass public function onNoteHit(event:HitNoteScriptEvent):Void {} public function onNoteMiss(event:NoteScriptEvent):Void {} + + public function onNoteHoldDrop(event:HoldNoteScriptEvent) {} } /** diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx index d698ba5d0..f1f0c11fe 100644 --- a/source/funkin/play/notes/notekind/NoteKindManager.hx +++ b/source/funkin/play/notes/notekind/NoteKindManager.hx @@ -49,7 +49,7 @@ class NoteKindManager { var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent); - var noteKind:NoteKind = noteKinds.get(noteEvent.note.kind); + var noteKind:NoteKind = noteKinds.get(noteEvent?.note?.kind); if (noteKind != null) { diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index f497a58c2..bce2bcd45 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -19,11 +19,6 @@ using funkin.data.animation.AnimationData.AnimationDataUtil; @:nullSafety class NoteStyle implements IRegistryEntry { - /** - * The ID of the note style. - */ - public final id:String; - /** * Note style data as parsed from the JSON file. */ diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index 5bd6efc25..cbb947ab6 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -69,6 +69,19 @@ class Scoring } } + public static function getMissScore(scoringSystem:ScoringSystem = PBOT1):Int + { + return switch (scoringSystem) + { + case LEGACY: LEGACY_MISS_SCORE; + case WEEK7: WEEK7_MISS_SCORE; + case PBOT1: PBOT1_MISS_SCORE; + default: + FlxG.log.error('Unknown scoring system: ${scoringSystem}'); + 0; + } + } + /** * The maximum score a note can receive. */ @@ -92,7 +105,7 @@ class Scoring /** * The score a note receives when it is missed. */ - public static final PBOT1_MISS_SCORE:Int = 0; + public static final PBOT1_MISS_SCORE:Int = -100; /** * The threshold at which a note hit is considered perfect and always given the max score. @@ -228,6 +241,11 @@ class Scoring */ public static final LEGACY_SHIT_SCORE:Int = 50; + /** + * The score a note receives when missed. + */ + public static final LEGACY_MISS_SCORE:Int = -10; + static function scoreNoteLEGACY(msTiming:Float):Int { var absTiming:Float = Math.abs(msTiming); @@ -276,6 +294,7 @@ class Scoring public static final WEEK7_BAD_THRESHOLD:Float = 0.8; // 80% of the hit window, or ~125ms public static final WEEK7_GOOD_THRESHOLD:Float = 0.55; // 55% of the hit window, or ~91ms public static final WEEK7_SICK_THRESHOLD:Float = 0.2; // 20% of the hit window, or ~33ms + public static final WEEK7_MISS_SCORE:Int = -10; public static final WEEK7_SHIT_SCORE:Int = 50; public static final WEEK7_BAD_SCORE:Int = 100; public static final WEEK7_GOOD_SCORE:Int = 200; @@ -363,10 +382,9 @@ class Scoring // Else, use the standard grades - // Grade % (only good and sick), 1.00 is a full combo + // Final Grade = (Sick + Good - Miss) / (Total Notes) + var grade = (scoreData.tallies.sick + scoreData.tallies.good) / scoreData.tallies.totalNotes; - // Clear % (including bad and shit). 1.00 is a full clear but not a full combo - var clear = (scoreData.tallies.totalNotesHit) / scoreData.tallies.totalNotes; if (grade == Constants.RANK_PERFECT_THRESHOLD) { diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index ebc58c83b..b41e252c1 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -65,19 +65,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry; - // key = variation id, value = metadata final _metadata:Map; @@ -289,6 +276,14 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + { + var diff:Null = getDifficulty(diffId, variation); + if (diff == null) return null; + + return diff.stickerPack; + } + /** * Populate the difficulty data from the provided metadata. * Does not load chart data (that is triggered later when we want to play the song). @@ -331,6 +326,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = null; + public var stickerPack:Null = null; public function new(song:Song, diffId:String, variation:String) { diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 76c8fc810..b539da77f 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -371,6 +371,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass public function onNoteHit(event:HitNoteScriptEvent) {} + public function onNoteHoldDrop(event:HoldNoteScriptEvent) {} + public function onNoteMiss(event:NoteScriptEvent) {} public function onSongEvent(event:SongEventScriptEvent) {} diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 4513bde1c..cf432a52a 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -30,10 +30,6 @@ typedef StagePropGroup = FlxTypedSpriteGroup; */ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements IRegistryEntry { - public final id:String; - - public final _data:StageData; - public var stageName(get, never):String; function get_stageName():String @@ -859,16 +855,6 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements } } - public override function toString():String - { - return 'Stage($id)'; - } - - static function _fetchData(id:String):Null - { - return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id)); - } - public function onScriptEvent(event:ScriptEvent) { // Ensure all custom events get broadcast to the elements of the stage. @@ -901,6 +887,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements public function onNoteMiss(event:NoteScriptEvent) {} + public function onNoteHoldDrop(event:HoldNoteScriptEvent) {} + public function onSongEvent(event:SongEventScriptEvent) {} public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index d33f49ab6..84381d02b 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -44,7 +44,7 @@ class Save { if (_instance == null) { - return _instance = load(); + return load(); } return _instance; } @@ -56,7 +56,10 @@ class Save trace("[SAVE] Loading save..."); // Bind save data. - return loadFromSlot(BASE_SAVE_SLOT); + var loadedSave:Save = loadFromSlot(BASE_SAVE_SLOT); + if (_instance == null) _instance = loadedSave; + + return loadedSave; } public static function clearData():Void @@ -120,6 +123,7 @@ class Save debugDisplay: false, vibration: true, autoPause: true, + vsyncMode: lime.ui.WindowVSyncMode.OFF, strumlineBackgroundOpacity: 0, autoFullscreen: false, inputOffset: 0, @@ -1161,6 +1165,7 @@ class Save * @param slot The slot number to check. * @return Whether the slot is not empty. */ + @:haxe.warning("-WDeprecated") static function querySlot(slot:Int):Bool { var targetSaveData = new FlxSave(); @@ -1443,6 +1448,12 @@ typedef SaveDataOptions = */ var autoPause:Bool; + /** + * If enabled, the game will utilize VSync (or adaptive VSync) on startup. + * @default `OFF` + */ + var vsyncMode:lime.ui.WindowVSyncMode; + /** * If >0, the game will display a semi-opaque background under the notes. * `0` for no background, `100` for solid black if you're freaky like that diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index f8e409e15..155b11f8f 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -123,7 +123,7 @@ class DebugMenuSubState extends MusicBeatSubState function testStickers() { - openSubState(new funkin.ui.transition.StickerSubState({})); + openSubState(new funkin.ui.transition.stickers.StickerSubState({})); trace('opened stickers'); } diff --git a/source/funkin/ui/debug/results/ResultsDebugSubState.hx b/source/funkin/ui/debug/results/ResultsDebugSubState.hx index 81628f30f..39dd19d55 100644 --- a/source/funkin/ui/debug/results/ResultsDebugSubState.hx +++ b/source/funkin/ui/debug/results/ResultsDebugSubState.hx @@ -54,7 +54,7 @@ class ResultsDebugSubState extends MusicBeatSubState FlxG.switchState(() -> new funkin.play.ResultState(resultsParams)); }); }); - createToggleListItem("Character", PlayerRegistry.instance.listBaseGamePlayerIds(), function(result:String) { + createToggleListItem("Character", PlayerRegistry.instance.listBaseGameEntryIds(), function(result:String) { resultsParams.characterId = result; }); createToggleListItem("Results Mode", ["Debug", "Story", "Freeplay"], function(result:String) { @@ -125,6 +125,7 @@ class ResultsDebugSubState extends MusicBeatSubState songId: "cum", characterId: "bf", difficultyId: "nightmare", + variationId: "erect", isNewHighscore: true, isPracticeMode: true, // Invalidates achievements/scores. isBotPlayMode: true, // Invalidates achievements/scores. diff --git a/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx b/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx index 2794da465..5cc07fbe9 100644 --- a/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx +++ b/source/funkin/ui/debug/stageeditor/components/WelcomeDialog.hx @@ -56,7 +56,7 @@ class WelcomeDialog extends Dialog boxDrag.onClick = function(_) FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], loadFromFilePath, null, null, "Open Stage Data"); - var defaultStages = StageRegistry.instance.listBaseGameStageIds(); + var defaultStages:Array = StageRegistry.instance.listBaseGameEntryIds(); defaultStages.sort(funkin.util.SortUtil.alphabetically); for (stage in defaultStages) diff --git a/source/funkin/ui/freeplay/Album.hx b/source/funkin/ui/freeplay/Album.hx index 154013ea3..264094d6e 100644 --- a/source/funkin/ui/freeplay/Album.hx +++ b/source/funkin/ui/freeplay/Album.hx @@ -11,16 +11,6 @@ import flixel.graphics.FlxGraphic; */ class Album implements IRegistryEntry { - /** - * The internal ID for this album. - */ - public final id:String; - - /** - * The full data for an album. - */ - public final _data:AlbumData; - public function new(id:String) { this.id = id; @@ -93,16 +83,4 @@ class Album implements IRegistryEntry { return _data.albumTitleAnimations; } - - public function toString():String - { - return 'Album($id)'; - } - - public function destroy():Void {} - - static function _fetchData(id:String):Null - { - return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 297d5a862..7854a9c4b 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -42,7 +42,7 @@ import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatSubState; import funkin.ui.story.Level; import funkin.ui.transition.LoadingState; -import funkin.ui.transition.StickerSubState; +import funkin.ui.transition.stickers.StickerSubState; import funkin.util.MathUtil; import funkin.util.SortUtil; import funkin.util.HapticUtil; @@ -804,8 +804,20 @@ class FreeplayState extends MusicBeatSubState if (onlyIfChanged) { - // == performs equality by reference - if (tempSongs.isEqualUnordered(currentFilteredSongs)) return; + if (tempSongs.isEqualUnordered(currentFilteredSongs)) + { + // If the song list is the same, we don't need to generate a new list. + + // Instead, we just apply the jump-in animation to the existing capsules. + for (capsule in grpCapsules.members) + { + capsule.initPosition(FlxG.width, 0); + capsule.initJumpIn(0, force); + } + + // Stop processing. + return; + } } // Only now do we know that the filter is actually changing. @@ -822,7 +834,8 @@ class FreeplayState extends MusicBeatSubState // Initialize the random capsule, with empty/blank info (which we display once bf/pico does his hand) var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); - randomCapsule.init(FlxG.width, 0, null, styleData); + randomCapsule.initPosition(FlxG.width, 0); + randomCapsule.initData(null, styleData); randomCapsule.y = randomCapsule.intendedY(0) + 10; randomCapsule.targetPos.x = randomCapsule.x; randomCapsule.alpha = 0; @@ -836,7 +849,10 @@ class FreeplayState extends MusicBeatSubState if (fromCharSelect) randomCapsule.forcePosition(); else - randomCapsule.initJumpIn(0, force); + { + // randomCapsule.initJumpIn(0, force); + randomCapsule.forcePosition(); + } var hsvShader:HSVShader = new HSVShader(); randomCapsule.hsvShader = hsvShader; @@ -849,7 +865,8 @@ class FreeplayState extends MusicBeatSubState var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem); - funnyMenu.init(FlxG.width, 0, tempSong, styleData); + funnyMenu.initPosition(FlxG.width, 0); + funnyMenu.initData(tempSong, styleData); funnyMenu.onConfirm = function() { capsuleOnOpenDefault(funnyMenu); }; @@ -859,8 +876,12 @@ class FreeplayState extends MusicBeatSubState funnyMenu.capsule.alpha = 0.5; funnyMenu.hsvShader = hsvShader; funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45); + + // This stops the bounce-in animation. funnyMenu.forcePosition(); + // funnyMenu.initJumpIn(0, force); + grpCapsules.add(funnyMenu); // add(funnyMenu.theActualHitbox); } @@ -1563,14 +1584,14 @@ class FreeplayState extends MusicBeatSubState { dj?.resetAFKTimer(); changeDiff(-1); - generateSongList(currentFilter, true); + generateSongList(currentFilter, true, false); } if (rightPressed) { dj?.resetAFKTimer(); changeDiff(1); - generateSongList(currentFilter, true); + generateSongList(currentFilter, true, false); } } @@ -1892,6 +1913,37 @@ class FreeplayState extends MusicBeatSubState }); } + /** + * findClosestDiff will find the closest difficulty to the given diff. + * It will return the index of the closest song in the grpCapsules.members array. + * @param diff + * @return Int + */ + function findClosestDiff(characterVariations:Array, diff:String):Int + { + var closestIndex:Int = 0; + var closest:Int = curSelected; + + for (index in 0...grpCapsules.members.length) + { + var song:Null = grpCapsules.members[index].freeplayData; + if (song == null) continue; + var characterVar = song.data.getVariationsByCharacter(currentCharacter); + var songDiff:Null = song.data.getDifficulty(diff, null, characterVar)?.difficulty; + // if the difference between the current index and this index is the smallest so far, + // take this one as the closest index. (By comparing with the closest variable) + var c:Int = curSelected - index; + if (songDiff == diff && (Math.abs(c) < Math.abs(closestIndex - curSelected) || closestIndex == 0)) + { + // trace('Found closest diff: ${songDiff} at index ${index} (current: ${curSelected})'); + closestIndex = index; + closest = c; + } + } + + return closestIndex; + } + /** * changeDiff is the root of both difficulty and variation changes/management. * It will check the difficulty of the current variation, all available variations, and all available difficulties per variation. @@ -1930,10 +1982,10 @@ class FreeplayState extends MusicBeatSubState // Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico` // but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell) var characterVariations:Array = grpCapsules.members[curSelected].freeplayData?.data.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST; - + var difficultiesAvailable:Array = allDifficulties; // Gets all available difficulties for our character, via our available variations - var difficultiesAvailable:Array = grpCapsules.members[curSelected].freeplayData?.data.listDifficulties(null, - characterVariations) ?? allDifficulties; + var songDifficulties:Array = grpCapsules.members[curSelected].freeplayData?.data.listDifficulties(null, + characterVariations) ?? Constants.DEFAULT_DIFFICULTY_LIST; var currentDifficultyIndex:Int = difficultiesAvailable.indexOf(currentDifficulty); @@ -1943,9 +1995,15 @@ class FreeplayState extends MusicBeatSubState if (currentDifficultyIndex < 0) currentDifficultyIndex = Std.int(difficultiesAvailable.length - 1); if (currentDifficultyIndex >= difficultiesAvailable.length) currentDifficultyIndex = 0; - // Update the current difficulty currentDifficulty = difficultiesAvailable[currentDifficultyIndex]; + // For when we change the difficulty, but the song doesn't have that difficulty! + if (!songDifficulties.contains(difficultiesAvailable[currentDifficultyIndex])) + { + curSelected = findClosestDiff(characterVariations, difficultiesAvailable[currentDifficultyIndex]); + rememberedSongId = grpCapsules.members[curSelected].freeplayData?.data.id; + } + for (variation in characterVariations) { if (grpCapsules.members[curSelected].freeplayData?.data.hasDifficulty(currentDifficulty, variation) ?? false) @@ -2017,7 +2075,7 @@ class FreeplayState extends MusicBeatSubState if (songCapsule.freeplayData != null) { - songCapsule.init(null, null, songCapsule.freeplayData); + songCapsule.initData(songCapsule.freeplayData); songCapsule.checkClip(); } } @@ -2420,7 +2478,7 @@ class FreeplayState extends MusicBeatSubState } #end instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; - trace('Attempting to play partial preview: ${previewSong.id}:${instSuffix}'); + // trace('Attempting to play partial preview: ${previewSong.id}:${instSuffix}'); FunkinSound.playMusic(previewSong.id, { startingVolume: 0.0, diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 08ae36007..2afdc67f7 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -506,10 +506,14 @@ class SongMenuItem extends FlxSpriteGroup updateSelected(); } - public function init(?x:Float, ?y:Float, freeplayData:Null, ?styleData:FreeplayStyle = null):Void + public function initPosition(x:Float, y:Float):Void + { + this.x = x; + this.y = y; + } + + public function initData(freeplayData:Null, ?styleData:FreeplayStyle = null):Void { - if (x != null) this.x = x; - if (y != null) this.y = y; this.freeplayData = freeplayData; // im so mad i have to do this but im pretty sure with the capsules recycling i cant call the new function properly :/ @@ -702,6 +706,17 @@ class SongMenuItem extends FlxSpriteGroup if (selected && songText.tooLong) songText.initMove(); } + + public override function kill():Void + { + super.kill(); + + visible = true; + capsule.alpha = 1; + doLerp = false; + doJumpIn = false; + doJumpOut = false; + } } /** diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index 408a91672..fe5b8b9e1 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -12,16 +12,6 @@ import funkin.play.scoring.Scoring.ScoringRank; @:nullSafety class PlayableCharacter implements IRegistryEntry { - /** - * The ID of the playable character. - */ - public final id:String; - - /** - * Playable character data as parsed from the JSON file. - */ - public final _data:Null; - /** * @param id The ID of the JSON file to parse. */ @@ -79,6 +69,11 @@ class PlayableCharacter implements IRegistryEntry return false; } + public function getStickerPackID():String + { + return _data?.stickerPack ?? Constants.DEFAULT_STICKER_PACK; + } + public function getFreeplayStyleID():String { return _data?.freeplayStyle ?? Constants.DEFAULT_FREEPLAY_STYLE; @@ -156,25 +151,4 @@ class PlayableCharacter implements IRegistryEntry { return _data?.unlocked ?? true; } - - /** - * Called when the character is destroyed. - * TODO: Document when this gets called - */ - public function destroy():Void {} - - public function toString():String - { - return 'PlayableCharacter($id)'; - } - - /** - * Retrieve and parse the JSON data for a playable character by ID. - * @param id The ID of the character - * @return The parsed player data, or null if not found or invalid - */ - static function _fetchData(id:String):Null - { - return PlayerRegistry.instance.parseEntryDataWithMigration(id, PlayerRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx index 95b81bfcf..e2c7a0957 100644 --- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx +++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx @@ -2,6 +2,7 @@ package funkin.ui.haxeui.components; import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent; import funkin.modding.events.ScriptEvent.NoteScriptEvent; +import funkin.modding.events.ScriptEvent.HoldNoteScriptEvent; import funkin.modding.events.ScriptEvent.HitNoteScriptEvent; import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; import funkin.modding.events.ScriptEvent.UpdateScriptEvent; @@ -237,6 +238,16 @@ class CharacterPlayer extends Box if (character != null) character.onNoteMiss(event); } + /** + * Called when a hold note is dropped in the song + * Used to play character animations. + * @param event The event. + */ + public function onNoteHoldDrop(event:HoldNoteScriptEvent):Void + { + if (character != null) character.onNoteHoldDrop(event); + } + /** * Called when a key is pressed but no note is hit in the song * Used to play character animations. diff --git a/source/funkin/ui/options/ControlsMenu.hx b/source/funkin/ui/options/ControlsMenu.hx index c9935fbbe..b1cce7665 100644 --- a/source/funkin/ui/options/ControlsMenu.hx +++ b/source/funkin/ui/options/ControlsMenu.hx @@ -44,12 +44,15 @@ class ControlsMenu extends Page var deviceList:TextMenuList; var menuCamera:FlxCamera; var prompt:Prompt; + var popup:Prompt; var camFollow:FlxObject; var labels:FlxTypedGroup; var currentDevice:Device = Keys; var deviceListSelected:Bool = false; + var actionPrevented:Bool = false; + static final CONTROL_BASE_X = 50; static final CONTROL_MARGIN_X = 700; static final CONTROL_SPACING_X = 300; @@ -181,6 +184,13 @@ class ControlsMenu extends Page prompt.back.scrollFactor.set(0, 0); prompt.exists = false; add(prompt); + + popup = new Prompt("\nYou cannot unbind\nthat key!\n\n\nEscape to exit", None); + popup.create(); + popup.createBgFromMargin(100, 0xFFfafd6d); + popup.back.scrollFactor.set(0, 0); + popup.exists = false; + add(popup); } function createItem(x = 0.0, y = 0.0, control:Control, index:Int) @@ -213,6 +223,12 @@ class ControlsMenu extends Page prompt.exists = true; } + function createPopup():Void + { + canExit = false; + popup.exists = true; + } + function goToDeviceList():Void { controlGrid.selectedItem.idle(); @@ -234,9 +250,16 @@ class ControlsMenu extends Page var inputName = device == Keys ? "key" : "button"; var cancel = device == Keys ? "Escape" : "Back"; // todo: alignment - if (device == Keys) prompt.setText('\nPress any key to rebind\n\n\n\n $cancel to cancel'); + if (device == Keys) + { + prompt.setText('\nPress any key to rebind\n\n\n\n $cancel to cancel'); + popup.setText('\nYou cannot unbind\nthat key!\n\n\n$cancel to exit'); + } else + { prompt.setText('\nPress any button\n to rebind\n\n\n $cancel to cancel'); + popup.setText('\nYou cannot unbind\nthat button!\n\n\n$cancel to exit'); + } controlGrid.selectedItem.select(); labels.members[Std.int(controlGrid.selectedIndex / COLUMNS)].alpha = 1.0; @@ -313,6 +336,28 @@ class ControlsMenu extends Page } } + if (actionPrevented && !popup.exists) createPopup(); + + if (popup.exists) + { + switch (currentDevice) + { + case Keys: + { + var key = FlxG.keys.firstJustReleased(); + if (key == ESCAPE) closePopup(); + } + case Gamepad(id): + { + var button = FlxG.gamepads.getByID(id).firstJustReleasedID(); + if (button == BACK) closePopup(); + + var key = FlxG.keys.firstJustReleased(); + if (key == ESCAPE) closePopup(); + } + } + } + switch (currentDevice) { case Keys: @@ -339,6 +384,19 @@ class ControlsMenu extends Page function onInputSelect(input:Int):Void { var item = controlGrid.selectedItem; + var leftItem = controlGrid.members[controlGrid.selectedIndex - 1]; + var rightItem = controlGrid.members[controlGrid.selectedIndex + 1]; + + // check if all keybinds are being removed and this is a UI control, prevent removing last keybind for this + if (input == FlxKey.NONE && controlGrid.selectedIndex != 1 && rightItem.input == FlxKey.NONE) + { + for (group in itemGroups) + { + if (group.toString() == itemGroups[1].toString() && group.contains(item)) actionPrevented = true; + } + } + + if (actionPrevented) return; // check if that key is already set for this if (input != FlxKey.NONE) @@ -379,7 +437,6 @@ class ControlsMenu extends Page if (controlGrid.selectedIndex % 2 == 1) { trace('Modified item on right side of grid'); - var leftItem = controlGrid.members[controlGrid.selectedIndex - 1]; if (leftItem != null && input != FlxKey.NONE && leftItem.input == FlxKey.NONE) { trace('Left item is unbound and right item is not!'); @@ -395,7 +452,6 @@ class ControlsMenu extends Page else { trace('Modified item on left side of grid'); - var rightItem = controlGrid.members[controlGrid.selectedIndex + 1]; if (rightItem != null && input == FlxKey.NONE && rightItem.input != FlxKey.NONE) { trace('Left item is unbound and right item is not!'); @@ -419,6 +475,13 @@ class ControlsMenu extends Page if (deviceList == null) canExit = true; } + function closePopup() + { + popup.exists = false; + actionPrevented = false; + if (deviceList == null) canExit = true; + } + override function destroy() { super.destroy(); @@ -476,6 +539,7 @@ class InputItem extends TextMenuItem function getInput() { var list = PlayerSettings.player1.controls.getInputsFor(control, device); + list = list.distinct(); if (list.length > index) { if (list[index] != FlxKey.ESCAPE || list[index] != FlxGamepadInputID.BACK) return list[index]; diff --git a/source/funkin/ui/options/FunkinSoundTray.hx b/source/funkin/ui/options/FunkinSoundTray.hx index 0eab4a0c0..047c86c96 100644 --- a/source/funkin/ui/options/FunkinSoundTray.hx +++ b/source/funkin/ui/options/FunkinSoundTray.hx @@ -142,7 +142,7 @@ class FunkinSoundTray extends FlxSoundTray var sound:String = up ? volumeUpSound : volumeDownSound; if (globalVolume == 10) sound = volumeMaxSound; - if (sound != null) FlxG.sound.load(sound).play(); + if (sound != null) FlxG.sound.load(sound).play().volume = 0.3; } return globalVolume; diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx index fb64f8139..f69465462 100644 --- a/source/funkin/ui/options/OptionsState.hx +++ b/source/funkin/ui/options/OptionsState.hx @@ -161,6 +161,12 @@ class OptionsMenu extends Page return item; } + override function update(elapsed:Float) + { + enabled = (prompt == null); + super.update(elapsed); + } + override function set_enabled(value:Bool) { items.enabled = value; diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx index 07d5b8e03..68048d079 100644 --- a/source/funkin/ui/options/PreferencesMenu.hx +++ b/source/funkin/ui/options/PreferencesMenu.hx @@ -16,12 +16,13 @@ import funkin.ui.TextMenuList.TextMenuItem; import funkin.ui.options.items.CheckboxPreferenceItem; import funkin.ui.options.items.NumberPreferenceItem; import funkin.ui.options.items.EnumPreferenceItem; -import funkin.mobile.ui.FunkinBackspace; #if mobile +import funkin.mobile.ui.FunkinBackspace; import funkin.mobile.ui.FunkinHitbox; import funkin.util.TouchUtil; import funkin.util.SwipeUtil; #end +import lime.ui.WindowVSyncMode; class PreferencesMenu extends Page { @@ -134,6 +135,22 @@ class PreferencesMenu extends Page }, Preferences.autoFullscreen); #end + // disabled on macos due to "error: Late swap tearing currently unsupported" + #if !mac + createPrefItemEnum('VSync', 'If enabled, game will attempt to match framerate with your monitor.', [ + "Off" => WindowVSyncMode.OFF, + "On" => WindowVSyncMode.ON, + "Adaptive" => WindowVSyncMode.ADAPTIVE, + ], function(key:String, value:WindowVSyncMode):Void { + trace("Setting vsync mode to " + key); + Preferences.vsyncMode = value; + }, switch (Preferences.vsyncMode) + { + case WindowVSyncMode.OFF: "Off"; + case WindowVSyncMode.ON: "On"; + case WindowVSyncMode.ADAPTIVE: "Adaptive"; + }); + #end #if web createPrefItemCheckbox('Unlocked Framerate', 'If enabled, the framerate will be unlocked.', function(value:Bool):Void { Preferences.unlockedFramerate = value; @@ -154,7 +171,8 @@ class PreferencesMenu extends Page createPrefItemCheckbox('Preview on save', 'If enabled, the preview will be shown only after a screenshot is saved.', function(value:Bool):Void { Preferences.previewOnSave = value; }, Preferences.previewOnSave); - createPrefItemEnum('Save Format', 'Save screenshots to this format.', ['PNG' => 'PNG', 'JPEG' => 'JPEG'], function(value:String):Void { + // TODO: having oValue is weird, probably change this later? was done to accomodate VSync changes. + createPrefItemEnum('Save Format', 'Save screenshots to this format.', ['PNG' => 'PNG', 'JPEG' => 'JPEG'], function(value:String, oValue:String):Void { Preferences.saveFormat = value; }, Preferences.saveFormat); createPrefItemNumber('JPEG Quality', 'The quality of JPEG screenshots.', function(value:Float) { @@ -185,13 +203,19 @@ class PreferencesMenu extends Page var thyOffset:Int = 0; // Initializing thy text width (if thou text present) var thyTextWidth:Int = 0; - if (Std.isOfType(daItem, EnumPreferenceItem)) thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth(); - else if (Std.isOfType(daItem, NumberPreferenceItem)) thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth(); - - if (thyTextWidth != 0) + switch (Type.typeof(daItem)) { - // Magic number because of the weird offset thats being added by default - thyOffset += thyTextWidth - 75; + case TClass(CheckboxPreferenceItem): + thyTextWidth = 0; + thyOffset = 0; + case TClass(EnumPreferenceItem): + thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth(); + thyOffset = 0 + thyTextWidth - 75; + case TClass(NumberPreferenceItem): + thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth(); + thyOffset = 0 + thyTextWidth - 75; + default: + // Huh? } if (items.selectedItem == daItem) @@ -277,9 +301,9 @@ class PreferencesMenu extends Page * @param onChange Gets called every time the player changes the value; use this to apply the value * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) */ - function createPrefItemEnum(prefName:String, prefDesc:String, values:Map, onChange:String->Void, defaultValue:String):Void + function createPrefItemEnum(prefName:String, prefDesc:String, values:Map, onChange:String->T->Void, defaultKey:String):Void { - var item = new EnumPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, values, defaultValue, onChange); + var item = new EnumPreferenceItem(funkin.ui.FullScreenScaleMode.gameNotchSize.x, (120 * items.length) + 30, prefName, values, defaultKey, onChange); items.addItem(prefName, item); preferenceItems.add(item.lefthandText); preferenceDesc.push(prefDesc); diff --git a/source/funkin/ui/options/items/EnumPreferenceItem.hx b/source/funkin/ui/options/items/EnumPreferenceItem.hx index e829d7afa..e0867beb6 100644 --- a/source/funkin/ui/options/items/EnumPreferenceItem.hx +++ b/source/funkin/ui/options/items/EnumPreferenceItem.hx @@ -10,7 +10,7 @@ import funkin.util.SwipeUtil; /** * Preference item that allows the player to pick a value from an enum (list of values) */ -class EnumPreferenceItem extends TextMenuItem +class EnumPreferenceItem extends TextMenuItem { function controls():Controls { @@ -19,34 +19,37 @@ class EnumPreferenceItem extends TextMenuItem public var lefthandText:AtlasText; - public var currentValue:String; - public var onChangeCallback:NullVoid>; - public var map:Map; + public var currentKey:String; + public var onChangeCallback:NullT->Void>; + public var map:Map; public var keys:Array = []; var index = 0; - public function new(x:Float, y:Float, name:String, map:Map, defaultValue:String, ?callback:String->Void) + public function new(x:Float, y:Float, name:String, map:Map, defaultKey:String, ?callback:String->T->Void) { super(x, y, name, function() { - callback(this.currentValue); + var value = map.get(this.currentKey); + callback(this.currentKey, value); }); updateHitbox(); this.map = map; - this.currentValue = defaultValue; + this.currentKey = defaultKey; this.onChangeCallback = callback; var i:Int = 0; for (key in map.keys()) { + var value:T = map[key]; + this.keys.push(key); - if (this.currentValue == key) index = i; + if (this.currentKey == key) index = i; i += 1; } - lefthandText = new AtlasText(x + 15, y, formatted(defaultValue), AtlasFont.DEFAULT); + lefthandText = new AtlasText(x + 15, y, formatted(defaultKey), AtlasFont.DEFAULT); this.fireInstantly = true; } @@ -67,21 +70,22 @@ class EnumPreferenceItem extends TextMenuItem if (index > keys.length - 1) index = 0; if (index < 0) index = keys.length - 1; - currentValue = keys[index]; + currentKey = keys[index]; if (onChangeCallback != null && (shouldIncrease || shouldDecrease)) { - onChangeCallback(currentValue); + var value = map.get(currentKey); + onChangeCallback(currentKey, value); } } - lefthandText.text = formatted(currentValue); + lefthandText.text = formatted(currentKey); } - function formatted(value:String):String + function formatted(key:String):String { // FIXME: Can't add arrows around the text because the font doesn't support < > // var leftArrow:String = selected ? '<' : ''; // var rightArrow:String = selected ? '>' : ''; - return '${map.get(value) ?? value}'; + return '${key}'; } } diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index 683e94815..2d7a697f1 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -15,16 +15,6 @@ import funkin.data.story.level.LevelData; */ class Level implements IRegistryEntry { - /** - * The ID of the story mode level. - */ - public final id:String; - - /** - * Level data as parsed from the JSON file. - */ - public final _data:LevelData; - /** * @param id The ID of the JSON file to parse. */ @@ -249,25 +239,4 @@ class Level implements IRegistryEntry return props; } - - /** - * Called when the level is destroyed. - * TODO: Document when this gets called - */ - public function destroy():Void {} - - public function toString():String - { - return 'Level($id)'; - } - - /** - * Retrieve and parse the JSON data for a level by ID. - * @param id The ID of the level - * @return The parsed level data, or null if not found or invalid - */ - static function _fetchData(id:String):Null - { - return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id)); - } } diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 44c324f57..46e9aa6c7 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -21,7 +21,7 @@ import funkin.save.Save.SaveScoreData; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatState; import funkin.ui.transition.LoadingState; -import funkin.ui.transition.StickerSubState; +import funkin.ui.transition.stickers.StickerSubState; import funkin.util.MathUtil; import funkin.util.SwipeUtil; import funkin.util.TouchUtil; diff --git a/source/funkin/ui/transition/stickers/ScriptedStickerPack.hx b/source/funkin/ui/transition/stickers/ScriptedStickerPack.hx new file mode 100644 index 000000000..34a0efd2c --- /dev/null +++ b/source/funkin/ui/transition/stickers/ScriptedStickerPack.hx @@ -0,0 +1,8 @@ +package funkin.ui.transition.stickers; + +/** + * A script that can be tied to a StickerPack. + * Create a scripted class that extends StickerPack to use this. + */ +@:hscriptClass +class ScriptedStickerPack extends funkin.ui.transition.stickers.StickerPack implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/ui/transition/stickers/StickerPack.hx b/source/funkin/ui/transition/stickers/StickerPack.hx new file mode 100644 index 000000000..ecd79bf37 --- /dev/null +++ b/source/funkin/ui/transition/stickers/StickerPack.hx @@ -0,0 +1,82 @@ +package funkin.ui.transition.stickers; + +import funkin.data.stickers.StickerData; +import funkin.data.stickers.StickerRegistry; +import funkin.data.IRegistryEntry; +import funkin.graphics.FunkinSprite; + +/** + * A class representing the data for a sticker pack as displayed in the Sticker SubState. + */ +class StickerPack implements IRegistryEntry +{ + /** + * The internal ID for this sticker pack. + */ + public final id:String; + + /** + * The full data for this sticker pack. + */ + public final _data:StickerData; + + public function new(id:String) + { + this.id = id; + this._data = _fetchData(id); + + if (_data == null) + { + throw 'Could not parse sticker pack data for id: $id'; + } + } + + /** + * Return the name of the sticker pack. + * @return The name of the sticker pack + */ + public function getStickerPackName():String + { + return _data.name; + } + + /** + * Return the artist of the sticker pack. + * @return The list of artists + */ + public function getStickerPackArtist():String + { + return _data.artist; + } + + /** + * Gets a list of all the sticker assets available in the pack. + * @return The list of stickers as raw strings. + */ + public function getStickers():Array + { + return _data.stickers; + } + + /** + * Retrieve a random sticker from the pack. + * @param last Whether this will be the last sticker to be placed on the screen. + * @return An asset path to a sticker to display. + */ + public function getRandomStickerPath(last:Bool):String + { + return FlxG.random.getObject(getStickers()); + } + + public function toString():String + { + return 'StickerPack($id)'; + } + + public function destroy():Void {} + + static function _fetchData(id:String):Null + { + return StickerRegistry.instance.parseEntryDataWithMigration(id, StickerRegistry.instance.fetchEntryVersion(id)); + } +} diff --git a/source/funkin/ui/transition/stickers/StickerSprite.hx b/source/funkin/ui/transition/stickers/StickerSprite.hx new file mode 100644 index 000000000..b993587bc --- /dev/null +++ b/source/funkin/ui/transition/stickers/StickerSprite.hx @@ -0,0 +1,16 @@ +package funkin.ui.transition.stickers; + +import funkin.graphics.FunkinSprite; + +class StickerSprite extends FunkinSprite +{ + public var timing:Float = 0; + + public function new(x:Float, y:Float, filePath:String):Void + { + super(x, y); + loadTexture(filePath); + updateHitbox(); + scrollFactor.set(); + } +} diff --git a/source/funkin/ui/transition/StickerSubState.hx b/source/funkin/ui/transition/stickers/StickerSubState.hx similarity index 62% rename from source/funkin/ui/transition/StickerSubState.hx rename to source/funkin/ui/transition/stickers/StickerSubState.hx index 0023f2314..d19f0cadf 100644 --- a/source/funkin/ui/transition/StickerSubState.hx +++ b/source/funkin/ui/transition/stickers/StickerSubState.hx @@ -1,30 +1,39 @@ -package funkin.ui.transition; +package funkin.ui.transition.stickers; -import haxe.Json; -import funkin.graphics.FunkinSprite; -// import flxtyped group -import funkin.ui.MusicBeatSubState; -import flixel.group.FlxGroup.FlxTypedGroup; -import flixel.util.FlxTimer; +import flixel.addons.transition.FlxTransitionableState; import flixel.FlxG; +import flixel.FlxState; +import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxMath; import flixel.util.FlxSort; -import funkin.ui.mainmenu.MainMenuState; -import flixel.addons.transition.FlxTransitionableState; +import flixel.util.FlxTimer; import funkin.audio.FunkinSound; -import flixel.FlxState; import funkin.util.HapticUtil; +import funkin.data.stickers.StickerRegistry; +import funkin.graphics.FunkinSprite; +import funkin.ui.freeplay.FreeplayState; +import funkin.ui.MusicBeatSubState; +import funkin.ui.transition.stickers.StickerPack; using Lambda; using StringTools; typedef StickerSubStateParams = { + /* + * The state to transition into. + */ ?targetState:StickerSubState->FlxState, - ?stickerSet:String, + /** + * The sticker pack to retrieve and use. + * @default `Constants.DEFAULT_STICKER_PACK` + */ ?stickerPack:String, + /** + * An existing set of stickers to transition out with. + */ ?oldStickers:Array, } @@ -33,9 +42,6 @@ class StickerSubState extends MusicBeatSubState { public var grpStickers:FlxTypedGroup; - // yes... a damn OpenFL sprite!!! - // public var dipshit:Sprite; - /** * The state to switch to after the stickers are done. * This is a FUNCTION so we can pass it directly to `FlxG.switchState()`, @@ -43,9 +49,8 @@ class StickerSubState extends MusicBeatSubState */ var targetState:StickerSubState->FlxState; - // what stickers to use - var stickerSet:String; - var stickerPack:String; + var stickerPackId:String; + var stickerPack:StickerPack; // what "folders" to potentially load from (as of writing only "keys" exist) var soundSelections:Array = []; @@ -57,21 +62,20 @@ class StickerSubState extends MusicBeatSubState { super(); - this.stickerSet = params.stickerSet ?? 'stickers-set-1'; - this.stickerPack = params.stickerPack ?? 'all'; + // Define the target state, with a default fallback. + this.targetState = params?.targetState ?? (sticker) -> FreeplayState.build(null, sticker); - this.targetState = params.targetState ?? (sticker) -> new MainMenuState(); + this.stickerPackId = params.stickerPack ?? Constants.DEFAULT_STICKER_PACK; - // todo still - // make sure that ONLY plays mp3/ogg files - // if there's no mp3/ogg file, then it regenerates/reloads the random folder + var targetStickerPack = StickerRegistry.instance.fetchEntry(this.stickerPackId); + this.stickerPack = targetStickerPack ?? StickerRegistry.instance.fetchDefault(); + + // TODO: Make this tied to the sticker pack more closely. var assetsInList = Assets.list(); - var soundFilterFunc = function(a:String) { return a.startsWith('assets/shared/sounds/stickersounds/'); }; - soundSelections = assetsInList.filter(soundFilterFunc); soundSelections = soundSelections.map(function(a:String) { return a.replace('assets/shared/sounds/stickersounds/', '').split('/')[0]; @@ -90,12 +94,10 @@ class StickerSubState extends MusicBeatSubState soundSelections.push(i); } - trace(soundSelections); - soundSelection = FlxG.random.getObject(soundSelections); var filterFunc = function(a:String) { - return a.startsWith('assets/shared/sounds/stickersounds/' + this.soundSelection + '/'); + return a.startsWith('assets/shared/sounds/stickersounds/' + soundSelection + '/'); }; var assetsInList3 = Assets.list(); sounds = assetsInList3.filter(filterFunc); @@ -105,8 +107,6 @@ class StickerSubState extends MusicBeatSubState sounds[i] = sounds[i].substring(0, sounds[i].lastIndexOf('.')); } - trace(sounds); - // makes the stickers on the most recent camera, which is more often than not... a UI camera!! // grpStickers.cameras = [FlxG.cameras.list[FlxG.cameras.list.length - 1]]; grpStickers.cameras = FlxG.cameras.list; @@ -121,7 +121,9 @@ class StickerSubState extends MusicBeatSubState degenStickers(); } else + { regenStickers(); + } } public function degenStickers():Void @@ -169,23 +171,14 @@ class StickerSubState extends MusicBeatSubState grpStickers.clear(); } - var stickerInfo:StickerInfo = new StickerInfo(this.stickerSet); - var stickers:Map> = new Map>(); - for (stickerSets in stickerInfo.getPack(this.stickerPack)) - { - stickers.set(stickerSets, stickerInfo.getStickers(stickerSets)); - } - + // Initialize stickers at each point on the screen, then shuffle up the order they will get placed. + // This ensures stickers consistently cover the screen. var xPos:Float = -100; var yPos:Float = -100; while (xPos <= FlxG.width) { - var stickersId:String = FlxG.random.getObject(stickers.keyValues()); - var stickers = stickers.get(stickersId); - if (stickers == null) throw 'Could not get sticker group ${stickersId}'; - - var sticker:String = FlxG.random.getObject(stickers); - var sticky:StickerSprite = new StickerSprite(0, 0, stickerInfo.name, sticker); + var stickerPath:String = stickerPack.getRandomStickerPath(false); + var sticky:StickerSprite = new StickerSprite(0, 0, stickerPath); sticky.visible = false; sticky.x = xPos; @@ -207,32 +200,14 @@ class StickerSubState extends MusicBeatSubState FlxG.random.shuffle(grpStickers.members); - // var stickerCount:Int = 0; - - // for (w in 0...6) - // { - // var xPos:Float = FlxG.width * (w / 6); - // for (h in 0...6) - // { - // var yPos:Float = FlxG.height * (h / 6); - // var sticker = grpStickers.members[stickerCount]; - // xPos -= sticker.width / 2; - // yPos -= sticker.height * 0.9; - // sticker.x = xPos; - // sticker.y = yPos; - - // stickerCount++; - // } - // } - - // for (ind => sticker in grpStickers.members) - // { - // sticker.x = (ind % 8) * sticker.width; - // var yShit:Int = Math.floor(ind / 8); - // sticker.y += yShit * sticker.height; - // // scales it juuuust a smidge - // sticker.y += 20 * yShit; - // } + // Creates a new sticker for the very center. + var lastStickerPath:String = stickerPack.getRandomStickerPath(true); + var lastSticker:StickerSprite = new StickerSprite(0, 0, lastStickerPath); + lastSticker.visible = false; + lastSticker.updateHitbox(); + lastSticker.angle = 0; + lastSticker.screenCenter(); + grpStickers.add(lastSticker); // another damn for loop... apologies!!! for (ind => sticker in grpStickers.members) @@ -297,12 +272,6 @@ class StickerSubState extends MusicBeatSubState grpStickers.sort((ord, a, b) -> { return FlxSort.byValues(ord, a.timing, b.timing); }); - - // centers the very last sticker - var lastOne:StickerSprite = grpStickers.members[grpStickers.members.length - 1]; - lastOne.updateHitbox(); - lastOne.angle = 0; - lastOne.screenCenter(); } override public function update(elapsed:Float):Void @@ -324,79 +293,3 @@ class StickerSubState extends MusicBeatSubState super.destroy(); } } - -class StickerSprite extends FunkinSprite -{ - public var timing:Float = 0; - - public function new(x:Float, y:Float, stickerSet:String, stickerName:String):Void - { - super(x, y); - loadTexture('transitionSwag/' + stickerSet + '/' + stickerName); - updateHitbox(); - scrollFactor.set(); - } -} - -class StickerInfo -{ - public var name:String; - public var artist:String; - public var stickers:Map>; - public var stickerPacks:Map>; - - public function new(stickerSet:String):Void - { - var path = Paths.file('images/transitionSwag/' + stickerSet + '/stickers.json'); - var json = Json.parse(Assets.getText(path)); - - // doin this dipshit nonsense cuz i dunno how to deal with casting a json object with - // a dash in its name (sticker-packs) - var jsonInfo:StickerShit = cast json; - - this.name = jsonInfo.name; - this.artist = jsonInfo.artist; - - stickerPacks = new Map>(); - - for (field in Reflect.fields(json.stickerPacks)) - { - var stickerFunny = json.stickerPacks; - var stickerStuff = Reflect.field(stickerFunny, field); - - stickerPacks.set(field, cast stickerStuff); - } - - // creates a similar for loop as before but for the stickers - stickers = new Map>(); - - for (field in Reflect.fields(json.stickers)) - { - var stickerFunny = json.stickers; - var stickerStuff = Reflect.field(stickerFunny, field); - - stickers.set(field, cast stickerStuff); - } - } - - public function getStickers(stickerName:String):Array - { - return this.stickers[stickerName]; - } - - public function getPack(packName:String, fallback:String = "all"):Array - { - if (!this.stickerPacks.exists(packName)) return this.stickerPacks[fallback]; - - return this.stickerPacks[packName]; - } -} - -// somethin damn cute just for the json to cast to! -typedef StickerShit = -{ - name:String, - artist:String, - stickers:Map>, - stickerPacks:Map> -} diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index b4c8b7f79..20b3d574b 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -238,6 +238,11 @@ class Constants */ public static final DEFAULT_VARIATION_LIST:Array = ['default', 'erect', 'pico', 'bf']; + /** + * Default sticker pack for transitions + */ + public static final DEFAULT_STICKER_PACK:String = 'default'; + /** * The default intensity multiplier for camera bops. * Prolly needs to be tuned bc it's a multiplier now. @@ -458,27 +463,32 @@ class Constants /** * The amount of health the player gains, while holding a hold note, per second. */ - public static final HEALTH_HOLD_BONUS_PER_SECOND:Float = 7.5 / 100.0 * HEALTH_MAX; // +7.5% / second + public static final HEALTH_HOLD_BONUS_PER_SECOND:Float = 6.0 / 100.0 * HEALTH_MAX; // +6.0% / second /** * The amount of health the player loses upon missing a note. */ - public static final HEALTH_MISS_PENALTY:Float = 4.0 / 100.0 * HEALTH_MAX; // 4.0% + public static final HEALTH_MISS_PENALTY:Float = -4.0 / 100.0 * HEALTH_MAX; // 4.0% /** * The amount of health the player loses upon pressing a key when no note is there. */ - public static final HEALTH_GHOST_MISS_PENALTY:Float = 2.0 / 100.0 * HEALTH_MAX; // 2.0% + public static final HEALTH_GHOST_MISS_PENALTY:Float = -4.0 / 100.0 * HEALTH_MAX; // 2.0% /** - * The amount of health the player loses upon letting go of a hold note while it is still going. + * The amount of health the player loses upon letting go of a hold note, per second remaining. */ - public static final HEALTH_HOLD_DROP_PENALTY:Float = 0.0; // 0.0% + public static final HEALTH_HOLD_DROP_PENALTY_PER_SECOND:Float = 0 / 100.0 * HEALTH_MAX; // -4.5% / second + + /** + * The maximum amount of health the player can lose upon letting go of a hold note. + */ + public static final HEALTH_HOLD_DROP_PENALTY_MAX:Float = 0 / 100.0 * HEALTH_MAX; // -10.0% /** * The amount of health the player loses upon hitting a mine. */ - public static final HEALTH_MINE_PENALTY:Float = 15.0 / 100.0 * HEALTH_MAX; // 15.0% + public static final HEALTH_MINE_PENALTY:Float = -15.0 / 100.0 * HEALTH_MAX; // 15.0% /** * SCORE VALUES @@ -486,11 +496,21 @@ class Constants // ============================== /** - * The amount of score the player gains for every send they hold a hold note. + * The amount of score the player gains for every second they hold a hold note. * A fraction of this value is granted every frame. */ public static final SCORE_HOLD_BONUS_PER_SECOND:Float = 250.0; + /** + * The amount of score the player loses upon letting go of a hold note, per second remaining. + */ + public static final SCORE_HOLD_DROP_PENALTY_PER_SECOND:Float = -125.0; + + /** + * The minimum amount of the hold note, in milliseconds, before the player gets penalized for letting go of it early. + */ + public static final HOLD_DROP_PENALTY_THRESHOLD_MS:Float = 160.0; + public static final JUDGEMENT_KILLER_COMBO_BREAK:Bool = false; public static final JUDGEMENT_SICK_COMBO_BREAK:Bool = false; public static final JUDGEMENT_GOOD_COMBO_BREAK:Bool = false; diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index c4ffcfe54..b8a807af0 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -159,4 +159,20 @@ class WindowUtil { lime.app.Application.current.window.title = value; } + + public static function setVSyncMode(value:lime.ui.WindowVSyncMode):Void + { + // vsync crap dont worky on mac rn derp + #if !mac + var res:Bool = FlxG.stage.application.window.setVSyncMode(value); + + // SDL_GL_SetSwapInterval returns the value we assigned on success, https://wiki.libsdl.org/SDL2/SDL_GL_GetSwapInterval#return-value. + // In lime, we can compare this to the original value to get a boolean. + if (!res) + { + trace('Failed to set VSync mode to ' + value); + FlxG.stage.application.window.setVSyncMode(lime.ui.WindowVSyncMode.OFF); + } + #end + } } diff --git a/source/funkin/util/macro/MacroUtil.hx b/source/funkin/util/macro/MacroUtil.hx index 2e2c73279..bbe6e71af 100644 --- a/source/funkin/util/macro/MacroUtil.hx +++ b/source/funkin/util/macro/MacroUtil.hx @@ -4,6 +4,9 @@ import haxe.macro.Context; import haxe.macro.Expr; import haxe.macro.Type; +/** + * A collection of utility functions for Haxe macros. + */ class MacroUtil { /** @@ -13,9 +16,9 @@ class MacroUtil * @param defaultValue The value to return if the define is not set. * @return An expression containing the value of the define. */ - public static macro function getDefine(key:String, defaultValue:String = null):haxe.macro.Expr + public static macro function getDefine(key:String, ?defaultValue:String):haxe.macro.Expr { - var value = haxe.macro.Context.definedValue(key); + var value:Null = haxe.macro.Context.definedValue(key); if (value == null) value = defaultValue; return macro $v{value}; } @@ -26,13 +29,13 @@ class MacroUtil */ public static macro function getDate():ExprOf { - var date = Date.now(); - var year = toExpr(date.getFullYear()); - var month = toExpr(date.getMonth()); - var day = toExpr(date.getDate()); - var hours = toExpr(date.getHours()); - var mins = toExpr(date.getMinutes()); - var secs = toExpr(date.getSeconds()); + var date:Date = Date.now(); + var year:Expr = toExpr(date.getFullYear()); + var month:Expr = toExpr(date.getMonth()); + var day:Expr = toExpr(date.getDate()); + var hours:Expr = toExpr(date.getHours()); + var mins:Expr = toExpr(date.getMinutes()); + var secs:Expr = toExpr(date.getSeconds()); return macro new Date($year, $month, $day, $hours, $mins, $secs); } @@ -44,6 +47,8 @@ class MacroUtil /** * Convert an ExprOf> to a ClassType. * @see https://github.com/jasononeil/compiletime/blob/master/src/CompileTime.hx#L201 + * @param e The expression to convert. + * @return The `ClassType` */ public static function getClassTypeFromExpr(e:Expr):ClassType { @@ -99,6 +104,11 @@ class MacroUtil return null; } + /** + * Determine whether a field is static. + * @param field The field to check. + * @return Whether the field is static. + */ public static function isFieldStatic(field:haxe.macro.Expr.Field):Bool { return field.access.contains(AStatic); @@ -107,11 +117,17 @@ class MacroUtil /** * Converts a value to an equivalent macro expression. */ - public static function toExpr(value:Dynamic):ExprOf + public static function toExpr(value:Any):ExprOf { return Context.makeExpr(value, Context.currentPos()); } + /** + * Determine whether two classes are equal. + * @param class1 The first class to compare. + * @param class2 The second class to compare. + * @return Whether the two classes are equivalent. + */ public static function areClassesEqual(class1:ClassType, class2:ClassType):Bool { return class1.pack.join('.') == class2.pack.join('.') && class1.name == class2.name; @@ -119,6 +135,8 @@ class MacroUtil /** * Retrieve a ClassType from a string name. + * @param name The name of the class to retrieve. + * @return The `ClassType` */ public static function getClassType(name:String):ClassType { @@ -131,6 +149,43 @@ class MacroUtil } } + /** + * If we are in a build macro, return whether a field already exists on the current class. + * @param name The name of the field to check for. + * @return Whether the field already exists. + */ + public static function fieldAlreadyExists(name:String):Bool + { + for (field in Context.getBuildFields()) + { + if (field.name == name && !((field.access ?? []).contains(Access.AAbstract))) + { + return true; + } + } + + function fieldAlreadyExistsSuper(name:String, superClass:Null) + { + if (superClass == null) + { + return false; + } + + for (field in superClass.fields.get()) + { + if (field.name == name && !field.isAbstract) + { + return true; + } + } + + // recursively check superclasses + return fieldAlreadyExistsSuper(name, superClass.superClass?.t.get()); + } + + return fieldAlreadyExistsSuper(name, Context.getLocalClass().get().superClass?.t.get()); + } + /** * Determine whether a given ClassType is a subclass of a given superclass. * @param classType The class to check. diff --git a/source/funkin/util/macro/RegistryMacro.hx b/source/funkin/util/macro/RegistryMacro.hx new file mode 100644 index 000000000..bc16e92be --- /dev/null +++ b/source/funkin/util/macro/RegistryMacro.hx @@ -0,0 +1,392 @@ +package funkin.util.macro; + +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Expr.ComplexType; +import haxe.macro.Expr.Field; +import haxe.macro.Expr.TypeDefKind; +import haxe.macro.Expr.MetadataEntry; +import haxe.macro.Type; +import haxe.macro.Type.ClassType; +import sys.FileSystem; + +using Lambda; +using haxe.macro.ExprTools; +using haxe.macro.TypeTools; +using StringTools; + +/** + * The type parameters for a class extending `BaseRegistry`. + */ +typedef RegistryTypeParams = +{ + /** + * The class type of the entry. Must implement `IRegistryEntry`. + */ + var entryType:ClassType; + + /** + * The type for the data of an entry. This is usually a typedef of a struct. + */ + var dataType:Any; // DefType or ClassType + +} + +/** + * A set of build macros to be applied to `Registry` classes in the `funkin.data` package. + * + * @see `funkin.data.BaseRegistry` + */ +class RegistryMacro +{ + static final DATA_FILE_BASE_PATH:String = 'assets/preload/data'; + + /** + * Builds the registry class. + * + * @return The modified list of fields for the target class. + */ + public static macro function buildRegistry():Array + { + var cls:ClassType = Context.getLocalClass().get(); + var fields:Array = Context.getBuildFields(); + + // Classes with the `@:funkinBase` meta or `@:funkinProcessed` meta should be ignored. + var baseMeta:Null = cls.meta.get().find(function(m) return m.name == ':funkinBase'); + if (baseMeta != null || alreadyProcessed(cls)) return fields; + + var typeParams:RegistryTypeParams = getTypeParams(cls); + + // Build an internal class with static functions that allow the Entry class to call functions on the Registry class. + buildEntryImpl(typeParams.entryType, cls); + + fields = fields.concat(buildRegistryMethods(cls, fields, typeParams.entryType, typeParams.dataType)); + + // Indicate that the class has been processed so we don't process twice. + cls.meta.add(":funkinProcessed", [], cls.pos); + + return fields; + } + + /** + * Builds the registry entry class. + * + * @return The modified list of fields for the target class. + */ + public static macro function buildEntry():Array + { + var cls:ClassType = Context.getLocalClass().get(); + var fields:Array = Context.getBuildFields(); + + // Classes with the `@:funkinProcessed` meta should be ignored. + if (alreadyProcessed(cls)) return fields; + + // Get the type of the JSON data for an entry. + var entryData:Any = getEntryData(cls); + + // Build variables and methods for the entry. + fields = fields.concat(buildEntryVariables(cls, entryData)); + fields = fields.concat(buildEntryMethods(cls)); + + // Indicate that the class has been processed so we don't process twice. + cls.meta.add(":funkinProcessed", [], cls.pos); + + return fields; + } + + #if macro + /** + * Retrieve the type parameters for a class extending `BaseRegistry`. + * @param cls The class to retrieve the type parameters for. + * @return The type parameters for the class. + */ + static function getTypeParams(cls:ClassType):RegistryTypeParams + { + switch (cls.superClass.t.get().kind) + { + case KGenericInstance(_, params): + var typeParams:Array = []; + for (param in params) + { + switch (param) + { + case TInst(t, _): + typeParams.push(t.get()); + case TType(t, _): + typeParams.push(t.get()); + default: + throw 'Not a class'; + } + } + return {entryType: typeParams[0], dataType: typeParams[1]}; + default: + throw '${cls.name}: Could not interpret type parameters of Registry class.'; + } + } + + /** + * Builds new static and instance methods for a registry class. + * + * @param cls The registry class to build the methods for. + * @param fields The fields of the registry class. + * @param entryType The class type of entries in the registry. + * @param dataType The type of the data for entries in the registry. + * @return The modified list of fields for the target class. + */ + static function buildRegistryMethods(cls:ClassType, fields:Array, entryType:ClassType, dataType:Dynamic):Array + { + var scriptedEntryClsName:String = entryType.pack.join('.') + '.Scripted' + entryType.name; + + var getScriptedClassName:String = '${scriptedEntryClsName}'; + + var createScriptedEntry:String = '${scriptedEntryClsName}.init(clsName, "unknown")'; + + var newJsonParser:String = 'new json2object.JsonParser<${dataType.module}.${dataType.name}>()'; + + var dataFilePath:String = getRegistryDataFilePath(cls, fields); + var baseGameEntryIds:Array = listBaseGameEntryIds('${DATA_FILE_BASE_PATH}/${dataFilePath}/'); + + return (macro class TempClass + { + public function listBaseGameEntryIds():Array + { + return $a{baseGameEntryIds}; + } + + public function listModdedEntryIds():Array + { + return listEntryIds().filter(function(id:String):Bool { + return listBaseGameEntryIds().indexOf(id) == -1; + }); + } + + function getScriptedClassNames() + { + return ${Context.parse(getScriptedClassName, Context.currentPos())}.listScriptClasses(); + } + + function createScriptedEntry(clsName:String) + { + return ${Context.parse(createScriptedEntry, Context.currentPos())}; + } + + public function parseEntryData(id:String) + { + var parser = ${Context.parse(newJsonParser, Context.currentPos())}; + parser.ignoreUnknownVariables = false; + + @:privateAccess + switch (this.loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + @:privateAccess + this.printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + public function parseEntryDataRaw(contents:String, ?fileName:String) + { + var parser = ${Context.parse(newJsonParser, Context.currentPos())}; + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + @:privateAccess + this.printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + }).fields.filter((field) -> return !MacroUtil.fieldAlreadyExists(field.name)); + } + + /** + * Retrieve the type of the JSON data for an entry. + * @param cls The entry class to retrieve the type of the JSON data for. + * @return Will be either a `DefType` or a `ClassType`. + */ + static function getEntryData(cls:ClassType):Any // DefType or ClassType + { + try + { + switch (cls.interfaces[0].params[0]) + { + case Type.TInst(t, _): + return t.get(); + case Type.TType(t, _): + return t.get(); + default: + throw '${cls.name}: Type parameter for Entry must be a Class or typedef'; + } + } + catch (e) + { + throw '${cls.name}: IRegistryEntry must be the last implemented interface'; + } + } + + /** + * Add fields to the entry class. + * @param cls The entry class to add fields to. + * @param entryData The type of the data for the entry. + * @return The modified list of fields for the target class. + */ + static function buildEntryVariables(cls:ClassType, entryData:Dynamic):Array + { + var entryDataType:ComplexType = Context.getType('${entryData.module}.${entryData.name}').toComplexType(); + + return (macro class TempClass + { + public final id:String; + + public final _data:Null<$entryDataType>; + }).fields.filter((field) -> return !MacroUtil.fieldAlreadyExists(field.name)); + } + + /** + * Add methods to the entry class. + * @param cls The entry class to add methods to. + * @return The modified list of fields for the target class. + */ + static function buildEntryMethods(cls:ClassType):Array + { + // The internal class built by `buildEntryImpl`. + var impl:String = 'funkin.macro.impl._${cls.name}_Impl'; + + return (macro class TempClass + { + public function _fetchData(id:String) + { + return ${Context.parse(impl, Context.currentPos())}._fetchData(this, id); + } + + public function toString() + { + return ${Context.parse(impl, Context.currentPos())}.toString(this); + } + + public function destroy() + { + ${Context.parse(impl, Context.currentPos())}.destroy(this); + } + }).fields.filter((field) -> return !MacroUtil.fieldAlreadyExists(field.name)); + } + + /** + * Build an internal class that calls functions for an associated registry class. + * @param cls The entry class to build the internal class for. + * @param registryCls The registry class that the entry class is associated with. + */ + static function buildEntryImpl(cls:ClassType, registryCls:ClassType):Void + { + var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType(); + + var registry:String = '${registryCls.module}.${registryCls.name}'; + + Context.defineType( + { + pos: Context.currentPos(), + pack: ['funkin', 'macro', 'impl'], + name: '_${cls.name}_Impl', + kind: TypeDefKind.TDClass(null, [], false, false, false), + fields: (macro class TempClass + { + public static inline function _fetchData(me:$clsType, id:String) + { + return $ + { + Context.parse(registry, Context.currentPos()) + }.instance.parseEntryDataWithMigration(id, ${Context.parse(registry, Context.currentPos())}.instance.fetchEntryVersion(id)); + } + + public static inline function toString(me:$clsType) + { + return $v{cls.name} + '(' + me.id + ')'; + } + + public static inline function destroy(me:$clsType) {} + }).fields + }); + } + + static function getRegistryDataFilePath(cls:ClassType, fields:Array):String + { + for (field in fields) + { + if (field.name == 'new') + { + // We found a field called `new`, it's probably the constructor. + switch (field.kind) + { + case FFun(f): + // Inside the function. + switch (f.expr.expr) + { + // Inside the block. + case EBlock(exprs): + var superCall:Expr = exprs[0]; + switch (superCall.expr) + { + // Inside the super() call. + case ECall(_, args): + // var registryId:String = args[0].toString(); + var dataPath:String = args[1].toString().replace('"', '').replace("'", ''); + // var versionRule = args[2].toString(); + + return dataPath; + default: + Context.error('${cls.name}.new: RegistryMacro expected super call', field.pos); + } + default: + Context.error('${cls.name}.new: RegistryMacro expected super call', field.pos); + } + default: + // Continue looking for the actual constructor. + } + } + } + + return ''; + } + + static function listBaseGameEntryIds(dataFilePath:String):Array + { + var result:Array = []; + var files:Array = FileSystem.readDirectory(dataFilePath); + + for (file in files) + { + result.push(macro $v{file.replace('.json', '')}); + } + + return result; + } + + /** + * Check whether this class has already been processed by the RegistryMacro, + * as indicated by the `@:funkinProcessed` meta. + * @param cls The class to check. + * @return `true` if the class has already been processed, `false` otherwise. + */ + static function alreadyProcessed(cls:ClassType):Bool + { + // Check for the `@:funkinProcessed` meta. + var processedMeta:MetadataEntry = cls.meta.get().find(function(m) return m.name == ':funkinProcessed'); + if (processedMeta != null) return true; + + // If it's not found, check the superclass. + if (cls.superClass != null) return alreadyProcessed(cls.superClass.t.get()); + return false; + } + #end +} diff --git a/source/funkin/util/macro/SingletonMacro.hx b/source/funkin/util/macro/SingletonMacro.hx new file mode 100644 index 000000000..4148c7c12 --- /dev/null +++ b/source/funkin/util/macro/SingletonMacro.hx @@ -0,0 +1,46 @@ +package funkin.util.macro; + +import haxe.macro.Context; +import haxe.macro.Type.ClassType; +import haxe.macro.Expr.Field; +import haxe.macro.Expr.ComplexType; + +using haxe.macro.Tools; + +/** + * A macro which automatically creates a Singleton `instance` property for a class. + * Add `implements funkin.util.tools.ISingleton` to your class to use. + */ +class SingletonMacro +{ + /** + * Applies an `instance` static field to the target class. + * @return The modified list of fields for the target class. + */ + public static macro function build():Array + { + var cls:ClassType = Context.getLocalClass().get(); + + var fields:Array = Context.getBuildFields(); + + var clsType:ComplexType = Context.getType('${cls.module}.${cls.name}').toComplexType(); + var newExpr:String = 'new ${cls.module}.${cls.name}()'; + + fields = fields.concat((macro class TempClass + { + static var _instance:Null<$clsType>; + public static var instance(get, never):$clsType; + + static function get_instance():$clsType + { + if (_instance == null) + { + _instance = ${Context.parse(newExpr, Context.currentPos())}; + } + return _instance; + } + }).fields); + + return fields; + } +} diff --git a/source/funkin/util/tools/ISingleton.hx b/source/funkin/util/tools/ISingleton.hx new file mode 100644 index 000000000..642ec617f --- /dev/null +++ b/source/funkin/util/tools/ISingleton.hx @@ -0,0 +1,7 @@ +package funkin.util.tools; + +/** + * An interface which applies a macro to add a Singleton `instance` property to the class. + */ +@:autoBuild(funkin.util.macro.SingletonMacro.build()) +interface ISingleton {} diff --git a/source/funkin/util/tools/SongEventDataArrayTools.hx b/source/funkin/util/tools/SongEventDataArrayTools.hx index dd2e3137e..38e298dde 100644 --- a/source/funkin/util/tools/SongEventDataArrayTools.hx +++ b/source/funkin/util/tools/SongEventDataArrayTools.hx @@ -46,10 +46,15 @@ class SongEventDataArrayTools // Search the lower half of the range. highIndex = midIndex - 1; } + // Found it? Make a more thorough check. + else if (midNote == note) + { + return midIndex; + } else { - // Found it! - return midIndex; + // We may be close, so constrain the range (but only a little) and try again. + highIndex -= 1; } } return -1;