diff --git a/.gitattributes b/.gitattributes index 94f480de9..e7a218c89 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ -* text=auto eol=lf \ No newline at end of file +* text=auto eol=lf +*.hxc linguist-language=Haxe +*.hxp linguist-language=Haxe diff --git a/.github/actions/setup-haxe/action.yml b/.github/actions/setup-haxe/action.yml index 5a9f7b293..438f330a2 100644 --- a/.github/actions/setup-haxe/action.yml +++ b/.github/actions/setup-haxe/action.yml @@ -44,7 +44,7 @@ runs: g++ \ libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ libgl-dev libgl1-mesa-dev \ - libasound2-dev + libasound2-dev libpulse-dev ln -s /usr/lib/x86_64-linux-gnu/libffi.so.8 /usr/lib/x86_64-linux-gnu/libffi.so.6 || true - name: Install linux-specific dependencies if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }} @@ -56,12 +56,17 @@ runs: shell: bash run: | echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV" - haxelib --debug --never install haxelib 4.1.0 --global - haxelib --debug --never deleterepo || true + haxelib fixrepo --global || true + haxelib --debug --never --global install haxelib 4.1.0 + haxelib --debug --global set haxelib 4.1.0 + haxelib --global remove haxelib git || true + haxelib --global remove hmm || true + rm -rf .haxelib + haxelib --debug --never --global git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies + haxelib --debug --never --global git hmm https://github.com/FunkinCrew/hmm funkin-patches haxelib --debug --never newrepo + haxelib version echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" - haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master - haxelib --debug --global install hmm echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV" - name: Restore cached dependencies @@ -75,7 +80,11 @@ runs: name: Prep git for dependency install uses: gacts/run-and-post-run@v1 with: - run: git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/ + run: | + git config --global --name-only --get-regexp 'url\.https\:\/\/x-access-token:.+@github\.com\/\.insteadOf' \ + | xargs git config --global --unset + git config -l --show-scope --show-origin + git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/ post: git config --global --unset 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} diff --git a/.github/labeler.yml b/.github/labeler.yml index e8e490865..e8250b4e7 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,7 +1,6 @@ # Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder Documentation: - changed-files: - - any-glob-to-any-file: - any-glob-to-any-file: - docs/* - '**/*.md' diff --git a/.github/workflows/build-game.yml b/.github/workflows/build-game.yml index 07802557c..dff9a369d 100644 --- a/.github/workflows/build-game.yml +++ b/.github/workflows/build-game.yml @@ -45,7 +45,11 @@ jobs: uses: ./.github/actions/setup-haxe with: gh-token: ${{ steps.app_token.outputs.token }} - + - name: Setup HXCPP dev commit + run: | + cd .haxelib/hxcpp/git/tools/hxcpp + haxe compile.hxml + cd ../../../../.. - name: Build game if: ${{ matrix.target == 'windows' }} run: | @@ -107,7 +111,9 @@ jobs: name: Install dependencies run: | git config --global 'url.https://x-access-token:${{ steps.app_token.outputs.token }}@github.com/.insteadOf' https://github.com/ + git config --global advice.detachedHead false haxelib --global run hmm install -q + cd .haxelib/hxcpp/git/tools/hxcpp && haxe compile.hxml - if: ${{ matrix.target != 'html5' }} name: Restore hxcpp cache diff --git a/.vscode/settings.json b/.vscode/settings.json index 26fe0b042..227cb94ec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -94,12 +94,12 @@ { "label": "Windows / Debug", "target": "windows", - "args": ["-debug", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "Linux / Debug", "target": "linux", - "args": ["-debug", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HashLink / Debug", @@ -109,7 +109,7 @@ { "label": "Windows / Debug (FlxAnimate Test)", "target": "windows", - "args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HashLink / Debug (FlxAnimate Test)", @@ -119,7 +119,7 @@ { "label": "Windows / Debug (Straight to Freeplay)", "target": "windows", - "args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HashLink / Debug (Straight to Freeplay)", @@ -132,13 +132,13 @@ "args": [ "-debug", "-DSONG=bopeebo -DDIFFICULTY=normal", - "-DFORCE_DEBUG_VERSION" + "-DFEATURE_DEBUG_FUNCTIONS" ] }, { "label": "Windows / Debug (Straight to Play - 2hot)", "target": "windows", - "args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HashLink / Debug (Straight to Play - Bopeebo Normal)", @@ -148,7 +148,7 @@ { "label": "Windows / Debug (Conversation Test)", "target": "windows", - "args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HashLink / Debug (Conversation Test)", @@ -163,7 +163,7 @@ { "label": "Windows / Debug (Straight to Chart Editor)", "target": "windows", - "args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HashLink / Debug (Straight to Chart Editor)", @@ -173,12 +173,12 @@ { "label": "Windows / Debug (Straight to Animation Editor)", "target": "windows", - "args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DANIMDEBUG", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "Windows / Debug (Debug hxCodec)", "target": "windows", - "args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HashLink / Debug (Straight to Animation Editor)", @@ -188,7 +188,7 @@ { "label": "Windows / Debug (Latency Test)", "target": "windows", - "args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HashLink / Debug (Latency Test)", @@ -198,7 +198,7 @@ { "label": "Windows / Debug (Waveform Test)", "target": "windows", - "args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "Windows / Release", @@ -218,17 +218,17 @@ { "label": "HTML5 / Debug", "target": "html5", - "args": ["-debug", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "HTML5 / Debug (Watch)", "target": "html5", - "args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-watch", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "macOS / Debug", "target": "mac", - "args": ["-debug", "-DFORCE_DEBUG_VERSION"] + "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] }, { "label": "macOS / Release", diff --git a/CHANGELOG.md b/CHANGELOG.md index 653ee203f..53e981284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,62 @@ 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.5.0] - 2024-08-?? +### Added +- Added a new Character Select screen to switch between playable characters in Freeplay + - Modding isn't 100% there but we're working on it! +- Added Pico as a playable character! Unlock him by completing Weekend 1 (if you haven't already done that) + - The songs from Weekend 1 have moved; you must now switch to Pico in Freeplay to access them +- Added ## new Pico remixes! Access them by selecting Pico from in the Character Select screen +- Added 2 new Erect remixes! Access them by switching difficulty on the song +- Implemented support for a new Instrumental Selector in Freeplay + - Beating a Pico remix lets you use that instrumental when playing as Boyfriend +- Added the first batch of Erect Stages! These graphical overhauls of the original stages will be used when playing Erect remixes and Pico remixes +- Implemented support for scripted Note Kinds. You can use HScript define a different note style to display for these notes as well as custom behavior. (community feature by lemz1) +- Implemented a new Strumline Background option, to display a darkened background behind the strumline with your choice of opacity. +- Implemented support for Numeric and Selector options in the Options menu. (community feature by FlooferLand) +## Changed +- Girlfriend and Nene now perform previously unused animations when you achieve a large combo, or drop a large combo. +- The pixel character icons in the Freeplay menu now display an animation! +- Altered how Week 6 displays sprites to make things look more retro. +- Character offsets are now independent of the character's scale. + - This should resolve issues with offsets when porting characters from older mods. + - Pixel character offsets have been modified to compensate. +- Note style data can now specify custom combo count graphics, judgement graphics, countdown graphics, and countdown audio. (community feature by anysad) + - These were previously using hardcoded values based on whether the stage was `school` or `schoolEvil`. +- The `danceEvery` property of characters and stage props can now use values with a precision of `0.25`, to play their idle animation up to four times per beat. +- Reworked the JSON merging system in Polymod; you can now include JSONPatch files under `_merge` in your mod folder to add, modify, or remove values in a JSON without replacing it entirely! +- Cutscenes now automatically pause when tabbing out (community fix by AbnormalPoof) +- Characters will now respect the `danceEvery` property (community fix by gamerbross) +- The F5 function now reloads the current song's chart data from disc (community feature by gamerbross) +- Refactored the compilation guide and added common troubleshooting steps (community fix by Hundrec) +- Made several layout improvements and fixes to the Animation Offsets editor in the Debug menu (community fix by gamerbross) +- Fixed a bug where the Back sound would be not played when leaving the Story menu and Options menu (community fix by AppleHair) +- Animation offsets no longer directly modify the `x` and `y` position of props, which makes props work better with tweens (community fix by Sword352) +- The YEAH! events in Tutorial now use chart events rather than being hard-coded (community fix by anysad) +- The player's Score now displays commas in it (community fix by loggo) +## Fixed +- Fixed an issue where songs with no notes would crash on the Results screen. +- Fixed an issue where the old icon easter egg would not work properly on pixel levels. +- Fixed an issue where you could play notes during the Thorns cutscene. +- Fixed an issue where the Heart icon when favoriting a song in Freeplay would be malformed. +- Fixed an issue where Pico's death animation displays a faint blue background (community fix by doggogit) +- Fixed an issue where mod songs would not play a preview in the Freeplay menu (community fix by KarimAkra) +- Fixed an issue where the Memory Usage counter could overflow and display a negative number (community fix by KarimAkra) +- Fixed an issue where pressing the Chart Editor keybind while playtesting a chart would reset the chart editor (community fix by gamerbross) +- Fixed a crash bug when pressing F5 after seeing the sticker transition (community fix by gamerbross) +- Fixed an issue where the Story Mode menu couldn't be scrolled with a mouse (community fix by JVNpixels) +- Fixed an issue causing the song to majorly desync sometimes (community fix by Burgerballs) +- Fixed an issue where the Freeplay song preview would not respect the instrumental ID specified in the song metadata (community fix by AppleHair) +- Fixed an issue where Tankman's icon wouldn't display in the Chart Editor (community fix by hundrec) +- Fixed an issue where pausing the game during a camera zoom would zoom the pause menu. (community fix by gamerbros) +- Fixed an issue where certain UI elements would not flash at a consistent rate (community fix by cyn0x8) +- Fixed an issue where the game would not use the placeholder health icon as a fallback (community fix by gamerbross) +- Fixed an issue where the chart editor could get stuck creating a hold note when using Live Inputs (community fix by gamerbross) +- Fixed an issue where character graphics could not be placed in week folders (community fix by 7oltan) +- Fixed a crash issue when a Freeplay song has no `Normal` difficulty (community fix by Applehair and gamerbross) +- Fixed an issue in Story Mode where a song that isn't valid for the current variation could be selected (community fix by Applehair) + ## [0.4.1] - 2024-06-12 ### Added - Pressing ESCAPE on the title screen on desktop now exits the game, allowing you to exit the game while in fullscreen on desktop diff --git a/Project.xml b/Project.xml deleted file mode 100644 index 8eb62bb1d..000000000 --- a/Project.xml +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - -
-
- - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
-
- - - - - - -
- - - - -
- - -
- - -
- - -
- - - --> - --> - - - -
- -
- - - - - - -
- - - - - - - - - - - - - - - - -
-
diff --git a/art b/art index faeba700c..0bb988c49 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit faeba700c5526bd4fd57ccc927d875c82b9d3553 +Subproject commit 0bb988c49788fd25a230b56dd9e4448838bc79c9 diff --git a/assets b/assets index c7589a95a..c559b25e9 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit c7589a95af2709d240e1b1a2994e68a04565b00a +Subproject commit c559b25e9a294837e6ba98397dad0ef32f325fd6 diff --git a/build/Dockerfile b/build/Dockerfile index c545d1364..318870166 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -83,7 +83,7 @@ apt-fast install -y --no-install-recommends \ libc6-dev libffi-dev \ libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ libgl-dev libgl1-mesa-dev \ - libasound2-dev \ + libasound2-dev libpulse-dev \ libvlc-dev libvlccore-dev EOF @@ -137,8 +137,8 @@ ENV PATH="$HAXEPATH:$PATH" RUN < = [".*", "cvs", "thumbs.db", "desktop.ini", "*.hash", "*.md"]; + + /** + * Asset path globs to exclude on web platforms. + */ + static final EXCLUDE_ASSETS_WEB:Array = ["*.ogg"]; + /** + * Asset path globs to exclude on native platforms. + */ + static final EXCLUDE_ASSETS_NATIVE:Array = ["*.mp3"]; + + // + // FEATURE FLAGS + // Inverse feature flags are automatically populated. + // + + /** + * `-DGITHUB_BUILD` + * If this flag is enabled, the game will use the configuration used by GitHub Actions + * to generate playtest builds to be pushed to the launcher. + * + * This is generally used to forcibly enable debugging features, + * even when the game is built in release mode for performance reasons. + */ + static final GITHUB_BUILD:FeatureFlag = "GITHUB_BUILD"; + + /** + * `-DREDIRECT_ASSETS_FOLDER` + * If this flag is enabled, the game will redirect the `assets` folder from the `export` folder + * to the `assets` folder at the root of the workspace. + * This is useful for ensuring hot reloaded changes don't get lost when rebuilding the game. + */ + static final REDIRECT_ASSETS_FOLDER:FeatureFlag = "REDIRECT_ASSETS_FOLDER"; + + /** + * `-DTOUCH_HERE_TO_PLAY` + * If this flag is enabled, the game will display a prompt to the user after the preloader completes, + * requiring them to click anywhere on the screen to start the game. + * This is done to ensure that the audio context can initialize properly on HTML5. Not necessary on desktop. + */ + static final TOUCH_HERE_TO_PLAY:FeatureFlag = "TOUCH_HERE_TO_PLAY"; + + /** + * `-DPRELOAD_ALL` + * Whether to preload all asset libraries. + * Disabled on web, enabled on desktop. + */ + static final PRELOAD_ALL:FeatureFlag = "PRELOAD_ALL"; + + /** + * `-DEMBED_ASSETS` + * Whether to embed all asset libraries into the executable. + */ + static final EMBED_ASSETS:FeatureFlag = "EMBED_ASSETS"; + + /** + * `-DHARDCODED_CREDITS` + * If this flag is enabled, the credits will be parsed and encoded in the game at compile time, + * rather than read from JSON data at runtime. + */ + static final HARDCODED_CREDITS:FeatureFlag = "HARDCODED_CREDITS"; + + /** + * `-DFEATURE_DEBUG_FUNCTIONS` + * If this flag is enabled, the game will have all playtester-only debugging functionality enabled. + * This includes debug hotkeys like time travel in the Play State. + * By default, enabled on debug builds or playtester builds and disabled on release builds. + */ + static final FEATURE_DEBUG_FUNCTIONS:FeatureFlag = "FEATURE_DEBUG_FUNCTIONS"; + + /** + * `-DFEATURE_DISCORD_RPC` + * If this flag is enabled, the game will enable the Discord Remote Procedure Call library. + * This is used to provide Discord Rich Presence support. + */ + static final FEATURE_DISCORD_RPC:FeatureFlag = "FEATURE_DISCORD_RPC"; + + /** + * `-DFEATURE_NEWGROUNDS` + * If this flag is enabled, the game will enable the Newgrounds library. + * This is used to provide Medal and Leaderboard support. + */ + static final FEATURE_NEWGROUNDS:FeatureFlag = "FEATURE_NEWGROUNDS"; + + /** + * `-DFEATURE_FUNKVIS` + * If this flag is enabled, the game will enable the Funkin Visualizer library. + * This is used to provide audio visualization like Nene's speaker. + * Disabling this will make some waveforms inactive. + */ + static final FEATURE_FUNKVIS:FeatureFlag = "FEATURE_FUNKVIS"; + + /** + * `-DFEATURE_PARTIAL_SOUNDS` + * If this flag is enabled, the game will enable the FlxPartialSound library. + * This is used to provide audio previews in Freeplay. + * Disabling this will make those previews not play. + */ + static final FEATURE_PARTIAL_SOUNDS:FeatureFlag = "FEATURE_PARTIAL_SOUNDS"; + + /** + * `-DFEATURE_VIDEO_PLAYBACK` + * If this flag is enabled, the game will enable support for video playback. + * This requires the hxCodec library on desktop platforms. + */ + static final FEATURE_VIDEO_PLAYBACK:FeatureFlag = "FEATURE_VIDEO_PLAYBACK"; + + /** + * `-DFEATURE_FILE_DROP` + * If this flag is enabled, the game will support dragging and dropping files onto it for various features. + * Disabled on MacOS. + */ + static final FEATURE_FILE_DROP:FeatureFlag = "FEATURE_FILE_DROP"; + + /** + * `-DFEATURE_OPEN_URL` + * If this flag is enabled, the game will support opening URLs (such as the merch page). + */ + static final FEATURE_OPEN_URL:FeatureFlag = "FEATURE_OPEN_URL"; + + /** + * `-DFEATURE_CHART_EDITOR` + * If this flag is enabled, the Chart Editor will be accessible from the debug menu. + */ + static final FEATURE_CHART_EDITOR:FeatureFlag = "FEATURE_CHART_EDITOR"; + + /** + * `-DFEATURE_STAGE_EDITOR` + * If this flag is enabled, the Stage Editor will be accessible from the debug menu. + */ + static final FEATURE_STAGE_EDITOR:FeatureFlag = "FEATURE_STAGE_EDITOR"; + + /** + * `-DFEATURE_POLYMOD_MODS` + * If this flag is enabled, the game will enable the Polymod library's support for atomic mod loading from the `./mods` folder. + * If this flag is disabled, no mods will be loaded. + */ + static final FEATURE_POLYMOD_MODS:FeatureFlag = "FEATURE_POLYMOD_MODS"; + + /** + * `-DFEATURE_GHOST_TAPPING` + * If this flag is enabled, misses will not be counted when it is not the player's turn. + * Misses are still counted when the player has notes to hit. + */ + static final FEATURE_GHOST_TAPPING:FeatureFlag = "FEATURE_GHOST_TAPPING"; + + // + // CONFIGURATION FUNCTIONS + // + + public function new() { + super(); + + flair(); + configureApp(); + + displayTarget(); + configureFeatureFlags(); + configureCompileDefines(); + configureIncludeMacros(); + configureCustomMacros(); + configureOutputDir(); + configurePolymod(); + configureHaxelibs(); + configureAssets(); + configureIcons(); + } + + /** + * Do something before building, display some ASCII or something IDK + */ + function flair() { + // TODO: Implement this. + info("Friday Night Funkin'"); + info("Initializing build..."); + + info("Target Version: " + VERSION); + info("Git Branch: " + getGitBranch()); + info("Git Commit: " + getGitCommit()); + info("Git Modified? " + getGitModified()); + info("Display? " + isDisplay()); + } + + /** + * Apply basic project metadata, such as the game title and version number, + * as well as info like the package name and company (used by various app stores). + */ + function configureApp() { + this.meta.title = TITLE; + this.meta.version = VERSION; + this.meta.packageName = PACKAGE_NAME; + this.meta.company = COMPANY; + + this.app.main = MAIN_CLASS; + this.app.file = EXECUTABLE_NAME; + this.app.preloader = PRELOADER; + + // Tell Lime where to look for the game's source code. + // If for some reason we have multiple source directories, we can add more entries here. + this.sources.push(SOURCE_DIR); + + // Tell Lime to run some prebuild and postbuild scripts. + this.preBuildCallbacks.push(buildHaxeCLICommand(PREBUILD_HX)); + this.postBuildCallbacks.push(buildHaxeCLICommand(POSTBUILD_HX)); + + // TODO: Should we provide this? + // this.meta.buildNumber = 0; + + // These values are only used by the SWF target I think. + // this.app.path + // this.app.init + // this.app.swfVersion + // this.app.url + + // These values are only used by... FIREFOX MARKETPLACE WHAT? + // this.meta.description = ""; + // this.meta.companyId = COMPANY; + // this.meta.companyUrl = COMPANY; + + // Configure the window. + // Automatically configure FPS. + this.window.fps = 60; + // Set the window size. + this.window.width = 1280; + this.window.height = 720; + // Black background on release builds, magenta on debug builds. + this.window.background = FEATURE_DEBUG_FUNCTIONS.isEnabled(this) ? 0xFFFF00FF : 0xFF000000; + + this.window.hardware = true; + this.window.vsync = false; + + if (isWeb()) { + this.window.resizable = true; + } + + if (isDesktop()) { + this.window.orientation = Orientation.LANDSCAPE; + this.window.fullscreen = false; + this.window.resizable = true; + this.window.vsync = false; + } + + if (isMobile()) { + this.window.orientation = Orientation.LANDSCAPE; + this.window.fullscreen = false; + this.window.resizable = false; + this.window.width = 0; + this.window.height = 0; + } + } + + /** + * Log information about the configured target platform. + */ + function displayTarget() { + // Display the target operating system. + switch (this.target) { + case Platform.WINDOWS: + info('Target Platform: Windows'); + case Platform.MAC: + info('Target Platform: MacOS'); + case Platform.LINUX: + info('Target Platform: Linux'); + case Platform.ANDROID: + info('Target Platform: Android'); + case Platform.IOS: + info('Target Platform: IOS'); + case Platform.HTML5: + info('Target Platform: HTML5'); + // See lime.tools.Platform for a full list. + // case Platform.EMSCRITEN: // A WebAssembly build might be interesting... + // case Platform.AIR: + // case Platform.BLACKBERRY: + // case Platform.CONSOLE_PC: + // case Platform.FIREFOX: + // case Platform.FLASH: + // case Platform.PS3: + // case Platform.PS4: + // case Platform.TIZEN: + // case Platform.TVOS: + // case Platform.VITA: + // case Platform.WEBOS: + // case Platform.WIIU: + // case Platform.XBOX1: + default: + error('Unsupported platform (got ${target})'); + } + + switch (this.platformType) { + case PlatformType.DESKTOP: + info('Platform Type: Desktop'); + case PlatformType.MOBILE: + info('Platform Type: Mobile'); + case PlatformType.WEB: + info('Platform Type: Web'); + case PlatformType.CONSOLE: + info('Platform Type: Console'); + default: + error('Unknown platform type (got ${platformType})'); + } + + // Print whether we are using HXCPP, HashLink, or something else. + if (isWeb()) { + info('Target Language: JavaScript (HTML5)'); + } else if (isHashLink()) { + info('Target Language: HashLink'); + } else if (isNeko()) { + info('Target Language: Neko'); + } else if (isJava()) { + info('Target Language: Java'); + } else if (isNodeJS()) { + info('Target Language: JavaScript (NodeJS)'); + } else if (isCSharp()) { + info('Target Language: C#'); + } else { + info('Target Language: C++'); + } + + for (arch in this.architectures) { + // Display the list of target architectures. + switch (arch) { + case Architecture.X86: + info('Architecture: x86'); + case Architecture.X64: + info('Architecture: x64'); + case Architecture.ARMV5: + info('Architecture: ARMv5'); + case Architecture.ARMV6: + info('Architecture: ARMv6'); + case Architecture.ARMV7: + info('Architecture: ARMv7'); + case Architecture.ARMV7S: + info('Architecture: ARMv7S'); + case Architecture.ARM64: + info('Architecture: ARMx64'); + case Architecture.MIPS: + info('Architecture: MIPS'); + case Architecture.MIPSEL: + info('Architecture: MIPSEL'); + case null: + if (!isWeb()) { + error('Unsupported architecture (got null on non-web platform)'); + } else { + info('Architecture: Web'); + } + default: + error('Unsupported architecture (got ${arch})'); + } + } + } + + /** + * Apply various feature flags based on the target platform and the user-provided build flags. + */ + function configureFeatureFlags() { + // You can explicitly override any of these. + // For example, `-DGITHUB_BUILD` or `-DNO_HARDCODED_CREDITS` + + // Should be false unless explicitly requested. + GITHUB_BUILD.apply(this, false); + FEATURE_STAGE_EDITOR.apply(this, false); + FEATURE_NEWGROUNDS.apply(this, false); + FEATURE_GHOST_TAPPING.apply(this, false); + + // Should be true unless explicitly requested. + HARDCODED_CREDITS.apply(this, true); + FEATURE_OPEN_URL.apply(this, true); + FEATURE_POLYMOD_MODS.apply(this, true); + FEATURE_FUNKVIS.apply(this, true); + FEATURE_PARTIAL_SOUNDS.apply(this, true); + FEATURE_VIDEO_PLAYBACK.apply(this, true); + + // Should be true on debug builds or if GITHUB_BUILD is enabled. + FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this)); + + // Should default to true on workspace builds and false on release builds. + REDIRECT_ASSETS_FOLDER.apply(this, isDebug() && isDesktop()); + + // Should be true on release, non-tester builds. + // We don't want testers to accidentally leak songs to their Discord friends! + // TODO: Re-enable this. + FEATURE_DISCORD_RPC.apply(this, false && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this)); + + // Should be true only on web builds. + // Audio context issues only exist there. + TOUCH_HERE_TO_PLAY.apply(this, isWeb()); + + // Should be true only on web builds. + // Enabling embedding and preloading is required to preload assets properly. + EMBED_ASSETS.apply(this, isWeb()); + PRELOAD_ALL.apply(this, true); + + // Should be true except on MacOS. + // File drop doesn't work there. + FEATURE_FILE_DROP.apply(this, !isMac()); + + // Should be true except on web builds. + // Chart editor doesn't work there. + FEATURE_CHART_EDITOR.apply(this, !isWeb()); + } + + /** + * Set compilation flags which are not feature flags. + */ + function configureCompileDefines() { + // Enable OpenFL's error handler. Required for the crash logger. + setHaxedef("openfl-enable-handle-error"); + + // Enable stack trace tracking. Good for debugging but has a (minor) performance impact. + setHaxedef("HXCPP_CHECK_POINTER"); + setHaxedef("HXCPP_STACK_LINE"); + setHaxedef("HXCPP_STACK_TRACE"); + setHaxedef("hscriptPos"); + + setHaxedef("safeMode"); + + // If we aren't using the Flixel debugger, strip it out. + if (FEATURE_DEBUG_FUNCTIONS.isDisabled(this)) { + setHaxedef("FLX_NO_DEBUG"); + } + + // Disable the built in pause screen when unfocusing the game. + setHaxedef("FLX_NO_FOCUS_LOST_SCREEN"); + + // HaxeUI configuration. + setHaxedef("haxeui_no_mouse_reset"); + setHaxedef("haxeui_focus_out_on_click"); // Unfocus a dialog when clicking out of it + setHaxedef("haxeui_dont_impose_base_class"); // Suppress a macro error + + if (isRelease()) { + // Improve performance on Nape + // TODO: Do we even use Nape? + setHaxedef("NAPE_RELEASE_BUILD"); + } + + // Cleaner looking compiler errors. + setHaxedef("message.reporting", "pretty"); + } + + /** + * Set compilation flags which manage dead code elimination. + */ + function configureIncludeMacros() { + // Disable dead code elimination. + // This prevents functions that are unused by the base game from being unavailable to HScript. + addHaxeFlag("-dce no"); + + // Forcibly include all Funkin' classes in builds. + // This prevents classes that are unused by the base game from being unavailable to HScript. + addHaxeMacro("include('funkin')"); + + // Ensure all HaxeUI components are available at runtime. + addHaxeMacro("include('haxe.ui.backend.flixel.components')"); + addHaxeMacro("include('haxe.ui.core')"); + addHaxeMacro("include('haxe.ui.components')"); + addHaxeMacro("include('haxe.ui.containers')"); + addHaxeMacro("include('haxe.ui.containers.dialogs')"); + addHaxeMacro("include('haxe.ui.containers.menus')"); + addHaxeMacro("include('haxe.ui.containers.properties')"); + + // Ensure all Flixel classes are available at runtime. + // Explicitly ignore packages which require additional dependencies. + addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*' ])"); + } + + /** + * Set compilation flags which manage bespoke build-time macros. + */ + function configureCustomMacros() { + // This macro allows addition of new functionality to existing Flixel. --> + addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')"); + } + + function configureOutputDir() { + // Set the output directory. Depends on the target platform and build type. + + var buildDir = 'export/${isDebug() ? 'debug' : 'release'}/'; + + info('Output directory: $buildDir'); + // setenv('BUILD_DIR', buildDir); + app.path = buildDir; + } + + function configurePolymod() { + // The file extension to use for script files. + setHaxedef("POLYMOD_SCRIPT_EXT", ".hscript"); + // Which asset library to use for scripts. + setHaxedef("POLYMOD_SCRIPT_LIBRARY", "scripts"); + // The base path from which scripts should be accessed. + setHaxedef("POLYMOD_ROOT_PATH", "scripts/"); + // Determines the subdirectory of the mod folder used for file appending. + setHaxedef("POLYMOD_APPEND_FOLDER", "_append"); + // Determines the subdirectory of the mod folder used for file merges. + setHaxedef("POLYMOD_MERGE_FOLDER", "_merge"); + // Determines the file in the mod folder used for metadata. + setHaxedef("POLYMOD_MOD_METADATA_FILE", "_polymod_meta.json"); + // Determines the file in the mod folder used for the icon. + setHaxedef("POLYMOD_MOD_ICON_FILE", "_polymod_icon.png"); + + if (isDebug()) { + // Turns on additional debug logging. + setHaxedef("POLYMOD_DEBUG"); + } + } + + function configureHaxelibs() { + // Don't enforce + addHaxelib('lime'); // Game engine backend + addHaxelib('openfl'); // Game engine backend + + addHaxelib('flixel'); // Game engine + + addHaxelib('flixel-addons'); // Additional utilities for Flixel + addHaxelib('hscript'); // Scripting + // addHaxelib('flixel-ui'); // UI framework (DEPRECATED) + addHaxelib('haxeui-core'); // UI framework + addHaxelib('haxeui-flixel'); // Integrate HaxeUI with Flixel + addHaxelib('flixel-text-input'); // Improved text field rendering for HaxeUI + + addHaxelib('polymod'); // Modding framework + addHaxelib('flxanimate'); // Texture atlas rendering + + addHaxelib('json2object'); // JSON parsing + addHaxelib('jsonpath'); // JSON parsing + addHaxelib('jsonpatch'); // JSON parsing + addHaxelib('thx.core'); // General utility library, "the lodash of Haxe" + addHaxelib('thx.semver'); // Version string handling + + if (isDebug()) { + addHaxelib('hxcpp-debug-server'); // VSCode debug support + } + + if (isDesktop() && !isHashLink() && FEATURE_VIDEO_PLAYBACK.isEnabled(this)) { + // hxCodec doesn't function on HashLink or non-desktop platforms + // It's also unnecessary if video playback is disabled + addHaxelib('hxCodec'); // Video playback + } + + if (FEATURE_DISCORD_RPC.isEnabled(this)) { + addHaxelib('discord_rpc'); // Discord API + } + + if (FEATURE_NEWGROUNDS.isEnabled(this)) { + addHaxelib('newgrounds'); // Newgrounds API + } + + if (FEATURE_FUNKVIS.isEnabled(this)) { + addHaxelib('funkin.vis'); // Audio visualization + addHaxelib('grig.audio'); // Audio data utilities + } + + if (FEATURE_PARTIAL_SOUNDS.isEnabled(this)) { + addHaxelib('FlxPartialSound'); // Partial sound + } + } + + function configureAssets() { + var exclude = EXCLUDE_ASSETS.concat(isWeb() ? EXCLUDE_ASSETS_WEB : EXCLUDE_ASSETS_NATIVE); + var shouldPreload = PRELOAD_ALL.isEnabled(this); + var shouldEmbed = EMBED_ASSETS.isEnabled(this); + + if (shouldEmbed) { + info('Embedding assets into executable...'); + } else { + info('Including assets alongside executable...'); + } + + // Default asset library + var shouldPreloadDefault = true; + addAssetLibrary("default", shouldEmbed, shouldPreloadDefault); + addAssetPath("assets/preload", "assets", "default", ["*"], exclude, shouldEmbed); + + // Font assets + var shouldEmbedFonts = true; + addAssetPath("assets/fonts", null, "default", ["*"], exclude, shouldEmbedFonts); + + // Shared asset libraries + addAssetLibrary("songs", shouldEmbed, shouldPreload); + addAssetPath("assets/songs", "assets/songs", "songs", ["*"], exclude, shouldEmbed); + addAssetLibrary("shared", shouldEmbed, shouldPreload); + addAssetPath("assets/shared", "assets/shared", "shared", ["*"], exclude, shouldEmbed); + if (FEATURE_VIDEO_PLAYBACK.isEnabled(this)) { + var shouldEmbedVideos = false; + addAssetLibrary("videos", shouldEmbedVideos, shouldPreload); + addAssetPath("assets/videos", "assets/videos", "videos", ["*"], exclude, shouldEmbedVideos); + } + + // Level asset libraries + addAssetLibrary("tutorial", shouldEmbed, shouldPreload); + addAssetPath("assets/tutorial", "assets/tutorial", "tutorial", ["*"], exclude, shouldEmbed); + addAssetLibrary("week1", shouldEmbed, shouldPreload); + addAssetPath("assets/week1", "assets/week1", "week1", ["*"], exclude, shouldEmbed); + addAssetLibrary("week2", shouldEmbed, shouldPreload); + addAssetPath("assets/week2", "assets/week2", "week2", ["*"], exclude, shouldEmbed); + addAssetLibrary("week3", shouldEmbed, shouldPreload); + addAssetPath("assets/week3", "assets/week3", "week3", ["*"], exclude, shouldEmbed); + addAssetLibrary("week4", shouldEmbed, shouldPreload); + addAssetPath("assets/week4", "assets/week4", "week4", ["*"], exclude, shouldEmbed); + addAssetLibrary("week5", shouldEmbed, shouldPreload); + addAssetPath("assets/week5", "assets/week5", "week5", ["*"], exclude, shouldEmbed); + addAssetLibrary("week6", shouldEmbed, shouldPreload); + addAssetPath("assets/week6", "assets/week6", "week6", ["*"], exclude, shouldEmbed); + addAssetLibrary("week7", shouldEmbed, shouldPreload); + addAssetPath("assets/week7", "assets/week7", "week7", ["*"], exclude, shouldEmbed); + addAssetLibrary("weekend1", shouldEmbed, shouldPreload); + addAssetPath("assets/weekend1", "assets/weekend1", "weekend1", ["*"], exclude, shouldEmbed); + + // Art asset library (where README and CHANGELOG pull from) + var shouldEmbedArt = false; + var shouldPreloadArt = false; + addAssetLibrary("art", shouldEmbedArt, shouldPreloadArt); + addAsset("art/readme.txt", "do NOT readme.txt", "art", shouldEmbedArt); + addAsset("LICENSE.md", "LICENSE.md", "art", shouldEmbedArt); + addAsset("CHANGELOG.md", "CHANGELOG.md", "art", shouldEmbedArt); + } + + /** + * Configure the application's favicon and executable icon. + */ + function configureIcons() { + addIcon("art/icon16.png", 16); + addIcon("art/icon32.png", 32); + addIcon("art/icon64.png", 64); + addIcon("art/iconOG.png"); + } + + // + // HELPER FUNCTIONS + // Easy functions to make the code more readable. + // + + public function isWeb():Bool { + return this.platformType == PlatformType.WEB; + } + + public function isMobile():Bool { + return this.platformType == PlatformType.MOBILE; + } + + public function isDesktop():Bool { + return this.platformType == PlatformType.DESKTOP; + } + + public function isConsole():Bool { + return this.platformType == PlatformType.CONSOLE; + } + + public function is32Bit():Bool { + return this.architectures.contains(Architecture.X86); + } + + public function is64Bit():Bool { + return this.architectures.contains(Architecture.X64); + } + + public function isWindows():Bool { + return this.target == Platform.WINDOWS; + } + + public function isMac():Bool { + return this.target == Platform.MAC; + } + + public function isLinux():Bool { + return this.target == Platform.LINUX; + } + + public function isAndroid():Bool { + return this.target == Platform.ANDROID; + } + + public function isIOS():Bool { + return this.target == Platform.IOS; + } + + public function isHashLink():Bool { + return this.targetFlags.exists("hl"); + } + + public function isNeko():Bool { + return this.targetFlags.exists("neko"); + } + + public function isJava():Bool { + return this.targetFlags.exists("java"); + } + + public function isNodeJS():Bool { + return this.targetFlags.exists("nodejs"); + } + + public function isCSharp():Bool { + return this.targetFlags.exists("cs"); + } + + public function isDisplay():Bool { + return this.command == "display"; + } + + public function isDebug():Bool { + return this.debug; + } + + public function isRelease():Bool { + return !isDebug(); + } + + public function getHaxedef(name:String):Null { + return this.haxedefs.get(name); + } + + public function setHaxedef(name:String, ?value:String):Void { + if (value == null) value = ""; + + this.haxedefs.set(name, value); + } + + public function unsetHaxedef(name:String):Void { + this.haxedefs.remove(name); + } + + public function getDefine(name:String):Null { + return this.defines.get(name); + } + + public function hasDefine(name:String):Bool { + return this.defines.exists(name); + } + + /** + * Add a library to the list of dependencies for the project. + * @param name The name of the library to add. + * @param version The version of the library to add. Optional. + */ + public function addHaxelib(name:String, version:String = ""):Void { + this.haxelibs.push(new Haxelib(name, version)); + } + + /** + * Add a `haxeflag` to the project. + */ + public function addHaxeFlag(value:String):Void { + this.haxeflags.push(value); + } + + /** + * Call a Haxe build macro. + */ + public function addHaxeMacro(value:String):Void { + addHaxeFlag('--macro ${value}'); + } + + /** + * Add an icon to the project. + * @param icon The path to the icon. + * @param size The size of the icon. Optional. + */ + public function addIcon(icon:String, ?size:Int):Void { + this.icons.push(new Icon(icon, size)); + } + + /** + * Add an asset to the game build. + * @param path The path the asset is located at. + * @param rename The path the asset should be placed. + * @param library The asset library to add the asset to. `null` = "default" + * @param embed Whether to embed the asset in the executable. + */ + public function addAsset(path:String, ?rename:String, ?library:String, embed:Bool = false):Void { + // path, rename, type, embed, setDefaults + var asset = new Asset(path, rename, null, embed, true); + @:nullSafety(Off) + { + asset.library = library ?? "default"; + } + this.assets.push(asset); + } + + /** + * Add an entire path of assets to the game build. + * @param path The path the assets are located at. + * @param rename The path the assets should be placed. + * @param library The asset library to add the assets to. `null` = "default" + * @param include An optional array to include specific asset names. + * @param exclude An optional array to exclude specific asset names. + * @param embed Whether to embed the assets in the executable. + */ + public function addAssetPath(path:String, ?rename:String, library:String, ?include:Array, ?exclude:Array, embed:Bool = false):Void { + // Argument parsing. + if (path == "") return; + + if (include == null) include = []; + + if (exclude == null) exclude = []; + + var targetPath = rename ?? path; + if (targetPath != "") targetPath += "/"; + + // Validate path. + if (!sys.FileSystem.exists(path)) { + error('Could not find asset path "${path}".'); + } else if (!sys.FileSystem.isDirectory(path)) { + error('Could not parse asset path "${path}", expected a directory.'); + } else { + // info(' Found asset path "${path}".'); + } + + for (file in sys.FileSystem.readDirectory(path)) { + if (sys.FileSystem.isDirectory('${path}/${file}')) { + // Attempt to recursively add all assets in the directory. + if (this.filter(file, ["*"], exclude)) { + addAssetPath('${path}/${file}', '${targetPath}${file}', library, include, exclude, embed); + } + } else { + if (this.filter(file, include, exclude)) { + addAsset('${path}/${file}', '${targetPath}${file}', library, embed); + } + } + } + } + + /** + * Add an asset library to the game build. + * @param name The name of the library. + * @param embed + * @param preload + */ + public function addAssetLibrary(name:String, embed:Bool = false, preload:Bool = false):Void { + // sourcePath, name, type, embed, preload, generate, prefix + var sourcePath = ''; + this.libraries.push(new Library(sourcePath, name, null, embed, preload, false, "")); + } + + // + // PROCESS FUNCTIONS + // + + /** + * A CLI command to run a command in the shell. + */ + public function buildCLICommand(cmd:String):CLICommand { + return CommandHelper.fromSingleString(cmd); + } + + /** + * A CLI command to run a Haxe script via `--interp`. + */ + public function buildHaxeCLICommand(path:String):CLICommand { + return CommandHelper.interpretHaxe(path); + } + + public function getGitCommit():String { + // Cannibalized from GitCommit.hx + var process = new sys.io.Process('git', ['rev-parse', 'HEAD']); + if (process.exitCode() != 0) { + var message = process.stderr.readAll().toString(); + error('[ERROR] Could not determine current git commit; is this a proper Git repository?'); + } + + var commitHash:String = process.stdout.readLine(); + var commitHashSplice:String = commitHash.substr(0, 7); + + return commitHashSplice; + } + + public function getGitBranch():String { + // Cannibalized from GitCommit.hx + var branchProcess = new sys.io.Process('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + + if (branchProcess.exitCode() != 0) { + var message = branchProcess.stderr.readAll().toString(); + error('Could not determine current git branch; is this a proper Git repository?'); + } + + var branchName:String = branchProcess.stdout.readLine(); + + return branchName; + } + + public function getGitModified():Bool { + var branchProcess = new sys.io.Process('git', ['status', '--porcelain']); + + if (branchProcess.exitCode() != 0) { + var message = branchProcess.stderr.readAll().toString(); + error('Could not determine current git status; is this a proper Git repository?'); + } + + var output:String = ''; + try { + output = branchProcess.stdout.readLine(); + } catch (e) { + if (e.message == 'Eof') { + // Do nothing. + // Eof = No output. + } else { + // Rethrow other exceptions. + throw e; + } + } + + return output.length > 0; + } + + // + // LOGGING FUNCTIONS + // + + /** + * Display an error message. This should stop the build process. + */ + public function error(message:String):Void { + Log.error('${message}'); + } + + /** + * Display an info message. This should not interfere with the build process. + */ + public function info(message:String):Void { + // CURSED: We have to disable info() log calls because of a bug. + // https://github.com/haxelime/lime-vscode-extension/issues/88 + + // Log.info('[INFO] ${message}'); + + // trace(message); + // Sys.println(message); + // Sys.stdout().writeString(message); + // Sys.stderr().writeString(message); + } +} + +/** + * An object representing a feature flag, which can be enabled or disabled. + * Includes features such as automatic generation of compile defines and inversion. + */ +abstract FeatureFlag(String) { + static final INVERSE_PREFIX:String = "NO_"; + + public function new(input:String) { + this = input; + } + + @:from + public static function fromString(input:String):FeatureFlag { + return new FeatureFlag(input); + } + + /** + * Enable/disable a feature flag if it is unset, and handle the inverse flag. + * Doesn't override a feature flag that was set explicitly. + * @param enableByDefault Whether to enable this feature flag if it is unset. + */ + public function apply(project:Project, enableByDefault:Bool = false):Void { + // TODO: Name this function better? + + if (isEnabled(project)) { + // If this flag was already enabled, disable the inverse. + project.info('Enabling feature flag ${this}'); + getInverse().disable(project, false); + } else if (getInverse().isEnabled(project)) { + // If the inverse flag was already enabled, disable this flag. + project.info('Disabling feature flag ${this}'); + disable(project, false); + } else { + if (enableByDefault) { + // Enable this flag if it was unset, and disable the inverse. + project.info('Enabling feature flag ${this}'); + enable(project, true); + } else { + // Disable this flag if it was unset, and enable the inverse. + project.info('Disabling feature flag ${this}'); + disable(project, true); + } + } + } + + /** + * Enable this feature flag by setting the appropriate compile define. + * + * @param project The project to modify. + * @param andInverse Also disable the feature flag's inverse. + */ + public function enable(project:Project, andInverse:Bool = true) { + project.setHaxedef(this, ""); + if (andInverse) { + getInverse().disable(project, false); + } + } + + /** + * Disable this feature flag by removing the appropriate compile define. + * + * @param project The project to modify. + * @param andInverse Also enable the feature flag's inverse. + */ + public function disable(project:Project, andInverse:Bool = true) { + project.unsetHaxedef(this); + if (andInverse) { + getInverse().enable(project, false); + } + } + + /** + * Query if this feature flag is enabled. + * @param project The project to query. + */ + public function isEnabled(project:Project):Bool { + // Check both Haxedefs and Defines for this flag. + return project.haxedefs.exists(this) || project.defines.exists(this); + } + + /** + * Query if this feature flag's inverse is enabled. + */ + public function isDisabled(project:Project):Bool { + return getInverse().isEnabled(project); + } + + /** + * Return the inverse of this feature flag. + * @return A new feature flag that is the inverse of this one. + */ + public function getInverse():FeatureFlag { + if (this.startsWith(INVERSE_PREFIX)) { + return this.substring(INVERSE_PREFIX.length); + } + return INVERSE_PREFIX + this; + } +} diff --git a/source/Main.hx b/source/Main.hx index add5bbc67..2426fa0d9 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -113,7 +113,7 @@ class Main extends Sprite addChild(game); - #if debug + #if FEATURE_DEBUG_FUNCTIONS game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil()); #end diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 6e370b5ff..21f83022e 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -27,13 +27,14 @@ import funkin.data.dialogue.speaker.SpeakerRegistry; import funkin.data.freeplay.album.AlbumRegistry; import funkin.data.song.SongRegistry; import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.play.notes.notekind.NoteKindManager; import funkin.modding.module.ModuleHandler; import funkin.ui.title.TitleState; import funkin.util.CLIUtil; import funkin.util.CLIUtil.CLIParams; import funkin.util.TimerUtil; import funkin.util.TrackerUtil; -#if discord_rpc +#if FEATURE_DISCORD_RPC import Discord.DiscordClient; #end @@ -122,7 +123,7 @@ class InitState extends FlxState // // DISCORD API SETUP // - #if discord_rpc + #if FEATURE_DISCORD_RPC DiscordClient.initialize(); Application.current.onExit.add(function(exitCode) { @@ -143,7 +144,7 @@ class InitState extends FlxState // Plugins provide a useful interface for globally active Flixel objects, // that receive update events regardless of the current state. // TODO: Move scripted Module behavior to a Flixel plugin. - #if debug + #if FEATURE_DEBUG_FUNCTIONS funkin.util.plugins.MemoryGCPlugin.initialize(); #end funkin.util.plugins.EvacuateDebugPlugin.initialize(); @@ -176,6 +177,8 @@ class InitState extends FlxState // Move it to use a BaseRegistry. CharacterDataParser.loadCharacterCache(); + NoteKindManager.loadScripts(); + ModuleHandler.buildModuleCallbacks(); ModuleHandler.loadModuleCache(); ModuleHandler.callOnCreate(); @@ -241,11 +244,11 @@ class InitState extends FlxState totalNotesHit: 140, totalNotes: 190 } - // 2000 = loss - // 240 = good - // 230 = great - // 210 = excellent - // 190 = perfect + // 2400 total notes = 7% = LOSS + // 240 total notes = 79% = GOOD + // 230 total notes = 82% = GREAT + // 210 total notes = 91% = EXCELLENT + // 190 total notes = PERFECT }, })); #elseif ANIMDEBUG @@ -371,11 +374,16 @@ class InitState extends FlxState // // FLIXEL DEBUG SETUP // - #if (debug || FORCE_DEBUG_VERSION) - // Make errors and warnings less annoying. - // Forcing this always since I have never been happy to have the debugger to pop up + #if FEATURE_DEBUG_FUNCTIONS + trace('Initializing Flixel debugger...'); + + #if !debug + // Make errors less annoying on release builds. LogStyle.ERROR.openConsole = false; LogStyle.ERROR.errorSound = null; + #end + + // Make errors and warnings less annoying. LogStyle.WARNING.openConsole = false; LogStyle.WARNING.errorSound = null; diff --git a/source/funkin/api/discord/Discord.hx b/source/funkin/api/discord/Discord.hx index a4d65684e..9dd513bf7 100644 --- a/source/funkin/api/discord/Discord.hx +++ b/source/funkin/api/discord/Discord.hx @@ -1,13 +1,13 @@ package funkin.api.discord; import Sys.sleep; -#if discord_rpc +#if FEATURE_DISCORD_RPC import discord_rpc.DiscordRpc; #end class DiscordClient { - #if discord_rpc + #if FEATURE_DISCORD_RPC public function new() { trace("Discord Client starting..."); diff --git a/source/funkin/api/newgrounds/NGUnsafe.hx b/source/funkin/api/newgrounds/NGUnsafe.hx index 77e44bd1d..81da7359e 100644 --- a/source/funkin/api/newgrounds/NGUnsafe.hx +++ b/source/funkin/api/newgrounds/NGUnsafe.hx @@ -24,7 +24,7 @@ class NGUnsafe NG.core.calls.event.logEvent(event).send(); trace('should have logged: ' + event); #else - #if debug + #if FEATURE_DEBUG_FUNCTIONS trace('event:$event - not logged, missing NG.io lib'); #end #end @@ -39,7 +39,7 @@ class NGUnsafe if (!medal.unlocked) medal.sendUnlock(); } #else - #if debug + #if FEATURE_DEBUG_FUNCTIONS trace('medal:$id - not unlocked, missing NG.io lib'); #end #end @@ -63,7 +63,7 @@ class NGUnsafe } } #else - #if debug + #if FEATURE_DEBUG_FUNCTIONS trace('Song:$song, Score:$score - not posted, missing NG.io lib'); #end #end diff --git a/source/funkin/api/newgrounds/NGio.hx b/source/funkin/api/newgrounds/NGio.hx index 3f5fc078a..a866783c1 100644 --- a/source/funkin/api/newgrounds/NGio.hx +++ b/source/funkin/api/newgrounds/NGio.hx @@ -239,7 +239,7 @@ class NGio NG.core.calls.event.logEvent(event).send(); trace('should have logged: ' + event); #else - #if debug + #if FEATURE_DEBUG_FUNCTIONS trace('event:$event - not logged, missing NG.io lib'); #end #end @@ -254,7 +254,7 @@ class NGio if (!medal.unlocked) medal.sendUnlock(); } #else - #if debug + #if FEATURE_DEBUG_FUNCTIONS trace('medal:$id - not unlocked, missing NG.io lib'); #end #end @@ -278,7 +278,7 @@ class NGio } } #else - #if debug + #if FEATURE_DEBUG_FUNCTIONS trace('Song:$song, Score:$score - not posted, missing NG.io lib'); #end #end diff --git a/source/funkin/audio/visualize/ABotVis.hx b/source/funkin/audio/visualize/ABotVis.hx index cf43a8add..a6ad0570e 100644 --- a/source/funkin/audio/visualize/ABotVis.hx +++ b/source/funkin/audio/visualize/ABotVis.hx @@ -54,7 +54,7 @@ class ABotVis extends FlxTypedSpriteGroup public function initAnalyzer() { @:privateAccess - analyzer = new SpectralAnalyzer(snd._channel.__source, 7, 0.1, 40); + analyzer = new SpectralAnalyzer(snd._channel.__audioSource, 7, 0.1, 40); #if desktop // On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5 diff --git a/source/funkin/audio/visualize/VisShit.hx b/source/funkin/audio/visualize/VisShit.hx index ba235fe89..83b9496ac 100644 --- a/source/funkin/audio/visualize/VisShit.hx +++ b/source/funkin/audio/visualize/VisShit.hx @@ -117,7 +117,7 @@ class VisShit { // Math.pow3 @:privateAccess - var buf = snd._channel.__source.buffer; + var buf = snd._channel.__audioSource.buffer; // @:privateAccess audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!! diff --git a/source/funkin/audio/waveform/WaveformDataParser.hx b/source/funkin/audio/waveform/WaveformDataParser.hx index 5aa54d744..ca421581b 100644 --- a/source/funkin/audio/waveform/WaveformDataParser.hx +++ b/source/funkin/audio/waveform/WaveformDataParser.hx @@ -16,7 +16,7 @@ class WaveformDataParser // Method 1. This only works if the sound has been played before. @:privateAccess - var soundBuffer:Null = sound?._channel?.__source?.buffer; + var soundBuffer:Null = sound?._channel?.__audioSource?.buffer; if (soundBuffer == null) { diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 118516bec..83413ad00 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -263,7 +263,7 @@ abstract class BaseRegistry & Constructible + public function parseEntryDataWithMigration(id:String, version:Null):Null { if (version == null) { diff --git a/source/funkin/data/event/SongEventRegistry.hx b/source/funkin/data/event/SongEventRegistry.hx index 9b0163557..5ee2d39fa 100644 --- a/source/funkin/data/event/SongEventRegistry.hx +++ b/source/funkin/data/event/SongEventRegistry.hx @@ -46,7 +46,7 @@ class SongEventRegistry if (event != null) { - trace(' Loaded built-in song event: (${event.id})'); + trace(' Loaded built-in song event: ${event.id}'); eventCache.set(event.id, event); } else @@ -59,9 +59,9 @@ class SongEventRegistry static function registerScriptedEvents() { var scriptedEventClassNames:Array = ScriptedSongEvent.listScriptClasses(); + trace('Instantiating ${scriptedEventClassNames.length} scripted song events...'); if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return; - trace('Instantiating ${scriptedEventClassNames.length} scripted song events...'); for (eventCls in scriptedEventClassNames) { var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN"); diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index f6c085018..55657ba46 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -38,6 +38,17 @@ class PlayerData @:optional public var freeplayDJ:Null = null; + /** + * Data for displaying this character in the Character Select menu. + * If null, exclude from Character Select. + */ + @:optional + public var charSelect:Null = null; + + /** + * Data for displaying this character in the results screen. + */ + @:optional public var results:Null = null; /** @@ -97,6 +108,9 @@ class PlayerFreeplayDJData @:optional var cartoon:Null; + @:optional + var fistPump:Null; + public function new() { animationMap = new Map(); @@ -183,6 +197,58 @@ class PlayerFreeplayDJData { return cartoon?.channelChangeFrame ?? 60; } + + public function getFistPumpIntroStartFrame():Int + { + return fistPump?.introStartFrame ?? 0; + } + + public function getFistPumpIntroEndFrame():Int + { + return fistPump?.introEndFrame ?? 0; + } + + public function getFistPumpLoopStartFrame():Int + { + return fistPump?.loopStartFrame ?? 0; + } + + public function getFistPumpLoopEndFrame():Int + { + return fistPump?.loopEndFrame ?? 0; + } + + public function getFistPumpIntroBadStartFrame():Int + { + return fistPump?.introBadStartFrame ?? 0; + } + + public function getFistPumpIntroBadEndFrame():Int + { + return fistPump?.introBadEndFrame ?? 0; + } + + public function getFistPumpLoopBadStartFrame():Int + { + return fistPump?.loopBadStartFrame ?? 0; + } + + public function getFistPumpLoopBadEndFrame():Int + { + return fistPump?.loopBadEndFrame ?? 0; + } +} + +class PlayerCharSelectData +{ + /** + * A zero-indexed number for the character's preferred position in the grid. + * 0 = top left, 4 = center, 8 = bottom right + * In the event of a conflict, the first character alphabetically gets it, + * and others get shifted over. + */ + @:optional + public var position:Null; } typedef PlayerResultsData = @@ -242,3 +308,30 @@ typedef PlayerFreeplayDJCartoonData = var loopFrame:Int; var channelChangeFrame:Int; } + +typedef PlayerFreeplayDJFistPumpData = +{ + @:default(0) + var introStartFrame:Int; + + @:default(4) + var introEndFrame:Int; + + @:default(4) + var loopStartFrame:Int; + + @:default(-1) + var loopEndFrame:Int; + + @:default(0) + var introBadStartFrame:Int; + + @:default(4) + var introBadEndFrame:Int; + + @:default(4) + var loopBadStartFrame:Int; + + @:default(-1) + var loopBadEndFrame:Int; +}; diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx index 4656a1286..c0a15ed1c 100644 --- a/source/funkin/data/freeplay/player/PlayerRegistry.hx +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -3,6 +3,7 @@ package funkin.data.freeplay.player; import funkin.data.freeplay.player.PlayerData; import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter; +import funkin.save.Save; class PlayerRegistry extends BaseRegistry { @@ -53,13 +54,49 @@ class PlayerRegistry extends BaseRegistry log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.'); } + public function countUnlockedCharacters():Int + { + var count = 0; + + for (charId in listEntryIds()) + { + var player = fetchEntry(charId); + if (player == null) continue; + + if (player.isUnlocked()) count++; + } + + return count; + } + + public function hasNewCharacter():Bool + { + var characters = Save.instance.charactersSeen.clone(); + + for (charId in listEntryIds()) + { + var player = fetchEntry(charId); + if (player == null) continue; + + if (!player.isUnlocked()) continue; + if (characters.contains(charId)) continue; + + // This character is unlocked but we haven't seen them in Freeplay yet. + return true; + } + + // Fallthrough case. + return false; + } + /** * Get the playable character associated with a given stage character. * @param characterId The stage character ID. * @return The playable character. */ - public function getCharacterOwnerId(characterId:String):Null + public function getCharacterOwnerId(characterId:Null):Null { + if (characterId == null) return null; return ownedCharacterIds[characterId]; } diff --git a/source/funkin/data/notestyle/CHANGELOG.md b/source/funkin/data/notestyle/CHANGELOG.md new file mode 100644 index 000000000..d85c11cad --- /dev/null +++ b/source/funkin/data/notestyle/CHANGELOG.md @@ -0,0 +1,31 @@ +# Note Style 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.1.0] +### Added +- Added several new `assets`: + - `countdownThree` + - `countdownTwo` + - `countdownOne` + - `countdownGo` + - `judgementSick` + - `judgementGood` + - `judgementBad` + - `judgementShit` + - `comboNumber0` + - `comboNumber1` + - `comboNumber2` + - `comboNumber3` + - `comboNumber4` + - `comboNumber5` + - `comboNumber6` + - `comboNumber7` + - `comboNumber8` + - `comboNumber9` + +## [1.0.0] +Initial version. diff --git a/source/funkin/data/notestyle/NoteStyleData.hx b/source/funkin/data/notestyle/NoteStyleData.hx index 04fda67ca..39162b896 100644 --- a/source/funkin/data/notestyle/NoteStyleData.hx +++ b/source/funkin/data/notestyle/NoteStyleData.hx @@ -74,6 +74,84 @@ typedef NoteStyleAssetsData = */ @:optional var holdNoteCover:NoteStyleAssetData; + + /** + * The THREE sound (and an optional pre-READY graphic). + */ + @:optional + var countdownThree:NoteStyleAssetData; + + /** + * The TWO sound and READY graphic. + */ + @:optional + var countdownTwo:NoteStyleAssetData; + + /** + * The ONE sound and SET graphic. + */ + @:optional + var countdownOne:NoteStyleAssetData; + + /** + * The GO sound and GO! graphic. + */ + @:optional + var countdownGo:NoteStyleAssetData; + + /** + * The SICK! judgement. + */ + @:optional + var judgementSick:NoteStyleAssetData; + + /** + * The GOOD! judgement. + */ + @:optional + var judgementGood:NoteStyleAssetData; + + /** + * The BAD! judgement. + */ + @:optional + var judgementBad:NoteStyleAssetData; + + /** + * The SHIT! judgement. + */ + @:optional + var judgementShit:NoteStyleAssetData; + + @:optional + var comboNumber0:NoteStyleAssetData; + + @:optional + var comboNumber1:NoteStyleAssetData; + + @:optional + var comboNumber2:NoteStyleAssetData; + + @:optional + var comboNumber3:NoteStyleAssetData; + + @:optional + var comboNumber4:NoteStyleAssetData; + + @:optional + var comboNumber5:NoteStyleAssetData; + + @:optional + var comboNumber6:NoteStyleAssetData; + + @:optional + var comboNumber7:NoteStyleAssetData; + + @:optional + var comboNumber8:NoteStyleAssetData; + + @:optional + var comboNumber9:NoteStyleAssetData; } /** @@ -109,10 +187,19 @@ typedef NoteStyleAssetData = @:optional var isPixel:Bool; + /** + * If true, animations will be played on the graphic. + * @default `false` to save performance. + */ + @:default(false) + @:optional + var animated:Bool; + /** * The structure of this data depends on the asset. */ - var data:T; + @:optional + var data:Null; } typedef NoteStyleData_Note = @@ -123,7 +210,14 @@ typedef NoteStyleData_Note = var right:UnnamedAnimationData; } +typedef NoteStyleData_Countdown = +{ + var audioPath:String; +} + typedef NoteStyleData_HoldNote = {} +typedef NoteStyleData_Judgement = {} +typedef NoteStyleData_ComboNum = {} /** * Data on animations for each direction of the strumline. diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx index 5e9fa9a3d..36d1b9200 100644 --- a/source/funkin/data/notestyle/NoteStyleRegistry.hx +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -11,9 +11,9 @@ class NoteStyleRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateNoteStyleData()` function. */ - public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.0.0"; + public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.1.0"; - public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; + public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x"; public static var instance(get, never):NoteStyleRegistry; static var _instance:Null = null; diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md index 4f1c66ade..ca36a1d6d 100644 --- a/source/funkin/data/song/CHANGELOG.md +++ b/source/funkin/data/song/CHANGELOG.md @@ -5,6 +5,13 @@ 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.4] +### Added +- Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent. + - If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent) +- Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player. + - If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player) + ## [2.2.3] ### Added - Added `charter` field to denote authorship of a chart. diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 769af8f08..f487eb54d 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -529,12 +529,26 @@ class SongCharacterData implements ICloneable @:default([]) public var altInstrumentals:Array = []; - public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '') + @:optional + public var opponentVocals:Null> = null; + + @:optional + public var playerVocals:Null> = null; + + public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '', ?altInstrumentals:Array, + ?opponentVocals:Array, ?playerVocals:Array) { this.player = player; this.girlfriend = girlfriend; this.opponent = opponent; this.instrumental = instrumental; + + this.altInstrumentals = altInstrumentals; + this.opponentVocals = opponentVocals; + this.playerVocals = playerVocals; + + if (opponentVocals == null) this.opponentVocals = [opponent]; + if (playerVocals == null) this.playerVocals = [player]; } public function clone():SongCharacterData @@ -722,18 +736,6 @@ class SongEventDataRaw implements ICloneable { return new SongEventDataRaw(this.time, this.eventKind, this.value); } -} - -/** - * Wrap SongEventData in an abstract so we can overload operators. - */ -@:forward(time, eventKind, value, activated, getStepTime, clone) -abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw -{ - public function new(time:Float, eventKind:String, value:Dynamic = null) - { - this = new SongEventDataRaw(time, eventKind, value); - } public function valueAsStruct(?defaultKey:String = "key"):Dynamic { @@ -757,27 +759,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR } } - public inline function getHandler():Null + public function getHandler():Null { return SongEventRegistry.getEvent(this.eventKind); } - public inline function getSchema():Null + public function getSchema():Null { return SongEventRegistry.getEventSchema(this.eventKind); } - public inline function getDynamic(key:String):Null + public function getDynamic(key:String):Null { return this.value == null ? null : Reflect.field(this.value, key); } - public inline function getBool(key:String):Null + public function getBool(key:String):Null { return this.value == null ? null : cast Reflect.field(this.value, key); } - public inline function getInt(key:String):Null + public function getInt(key:String):Null { if (this.value == null) return null; var result = Reflect.field(this.value, key); @@ -787,7 +789,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR return cast result; } - public inline function getFloat(key:String):Null + public function getFloat(key:String):Null { if (this.value == null) return null; var result = Reflect.field(this.value, key); @@ -797,17 +799,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR return cast result; } - public inline function getString(key:String):String + public function getString(key:String):String { return this.value == null ? null : cast Reflect.field(this.value, key); } - public inline function getArray(key:String):Array + public function getArray(key:String):Array { return this.value == null ? null : cast Reflect.field(this.value, key); } - public inline function getBoolArray(key:String):Array + public function getBoolArray(key:String):Array { return this.value == null ? null : cast Reflect.field(this.value, key); } @@ -839,6 +841,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR return result; } +} + +/** + * Wrap SongEventData in an abstract so we can overload operators. + */ +@:forward(time, eventKind, value, activated, getStepTime, clone, getHandler, getSchema, getDynamic, getBool, getInt, getFloat, getString, getArray, + getBoolArray, buildTooltip, valueAsStruct) +abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw +{ + public function new(time:Float, eventKind:String, value:Dynamic = null) + { + this = new SongEventDataRaw(time, eventKind, value); + } public function clone():SongEventData { @@ -951,12 +966,18 @@ class SongNoteDataRaw implements ICloneable return this.kind = value; } - public function new(time:Float, data:Int, length:Float = 0, kind:String = '') + @:alias("p") + @:default([]) + @:optional + public var params:Array; + + public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array) { this.time = time; this.data = data; this.length = length; this.kind = kind; + this.params = params ?? []; } /** @@ -1051,9 +1072,19 @@ class SongNoteDataRaw implements ICloneable _stepLength = null; } + public function cloneParams():Array + { + var params:Array = []; + for (param in this.params) + { + params.push(param.clone()); + } + return params; + } + public function clone():SongNoteDataRaw { - return new SongNoteDataRaw(this.time, this.data, this.length, this.kind); + return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams()); } public function toString():String @@ -1069,9 +1100,9 @@ class SongNoteDataRaw implements ICloneable @:forward abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw { - public function new(time:Float, data:Int, length:Float = 0, kind:String = '') + public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array) { - this = new SongNoteDataRaw(time, data, length, kind); + this = new SongNoteDataRaw(time, data, length, kind, params); } public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String @@ -1115,7 +1146,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw if (other.kind == '' || this.kind == null) return false; } - return this.time == other.time && this.data == other.data && this.length == other.length; + return this.time == other.time && this.data == other.data && this.length == other.length && this.params == other.params; } @:op(A != B) @@ -1134,7 +1165,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw if (other.kind == '') return true; } - return this.time != other.time || this.data != other.data || this.length != other.length; + return this.time != other.time || this.data != other.data || this.length != other.length || this.params != other.params; } @:op(A > B) @@ -1171,7 +1202,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw public function clone():SongNoteData { - return new SongNoteData(this.time, this.data, this.length, this.kind); + return new SongNoteData(this.time, this.data, this.length, this.kind, this.params); } /** @@ -1183,3 +1214,30 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw + (this.kind != '' ? ' [kind: ${this.kind}])' : ')'); } } + +class NoteParamData implements ICloneable +{ + @:alias("n") + public var name:String; + + @:alias("v") + @:jcustomparse(funkin.data.DataParse.dynamicValue) + @:jcustomwrite(funkin.data.DataWrite.dynamicValue) + public var value:Dynamic; + + public function new(name:String, value:Dynamic) + { + this.name = name; + this.value = value; + } + + public function clone():NoteParamData + { + return new NoteParamData(this.name, this.value); + } + + public function toString():String + { + return 'NoteParamData(${this.name}, ${this.value})'; + } +} diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx index acbb99342..96a1051cc 100644 --- a/source/funkin/data/song/importer/FNFLegacyImporter.hx +++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx @@ -199,6 +199,8 @@ class FNFLegacyImporter { // Handle the dumb logic for mustHitSection. var noteData = note.data; + if (noteData < 0) continue; // Exclude Psych event notes. + if (noteData > (STRUMLINE_SIZE * 2)) noteData = noteData % (2 * STRUMLINE_SIZE); // Handle other engine event notes. // Flip notes if mustHitSection is FALSE (not true lol). if (!mustHitSection) diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx index a03371296..7754c380e 100644 --- a/source/funkin/data/stage/StageRegistry.hx +++ b/source/funkin/data/stage/StageRegistry.hx @@ -93,8 +93,8 @@ class StageRegistry extends BaseRegistry public function listBaseGameStageIds():Array { return [ - "mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets", - "phillyBlazin", + "mainStage", "mainStageErect", "spookyMansion", "phillyTrain", "phillyTrainErect", "limoRide", "limoRideErect", "mallXmas", "mallEvil", "school", + "schoolEvil", "tankmanBattlefield", "phillyStreets", "phillyBlazin", ]; } diff --git a/source/funkin/data/story/level/LevelData.hx b/source/funkin/data/story/level/LevelData.hx index d01689a82..d1b00bbe6 100644 --- a/source/funkin/data/story/level/LevelData.hx +++ b/source/funkin/data/story/level/LevelData.hx @@ -93,9 +93,9 @@ typedef LevelPropData = * The frequency to bop at, in beats. * 1 = every beat, 2 = every other beat, etc. * Supports up to 0.25 precision. - * @default 0.0 + * @default 1.0 */ - @:default(0.0) + @:default(1.0) @:optional var danceEvery:Float; diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index eb331b9c3..194584992 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -4,8 +4,11 @@ import flixel.util.FlxSignal.FlxTypedSignal; import flxanimate.FlxAnimate; import flxanimate.FlxAnimate.Settings; import flxanimate.frames.FlxAnimateFrames; +import flixel.graphics.frames.FlxFrame; +import flixel.system.FlxAssets.FlxGraphicAsset; import openfl.display.BitmapData; import openfl.utils.Assets; +import flixel.math.FlxPoint; /** * A sprite which provides convenience functions for rendering a texture atlas with animations. @@ -18,16 +21,26 @@ class FlxAtlasSprite extends FlxAnimate FrameRate: 24.0, Reversed: false, // ?OnComplete:Void -> Void, - ShowPivot: #if debug false #else false #end, + ShowPivot: false, Antialiasing: true, ScrollFactor: null, // Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset }; /** - * Signal dispatched when an animation finishes playing. + * Signal dispatched when an animation advances to the next frame. */ - public var onAnimationFinish:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + public var onAnimationFrame:FlxTypedSignalInt->Void> = new FlxTypedSignal(); + + /** + * Signal dispatched when a non-looping animation finishes playing. + */ + public var onAnimationComplete:FlxTypedSignalVoid> = new FlxTypedSignal(); + + /** + * Signal dispatched when a looping animation finishes playing + */ + public var onAnimationLoopComplete:FlxTypedSignalVoid> = new FlxTypedSignal(); var currentAnimation:String; @@ -44,17 +57,20 @@ class FlxAtlasSprite extends FlxAnimate super(x, y, path, settings); - if (this.anim.curInstance == null) + if (this.anim.stageInstance == null) { throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?'; } - onAnimationFinish.add(cleanupAnimation); + onAnimationComplete.add(cleanupAnimation); // This defaults the sprite to play the first animation in the atlas, // then pauses it. This ensures symbols are intialized properly. this.anim.play(''); this.anim.pause(); + + this.anim.onComplete.add(_onAnimationComplete); + this.anim.onFrame.add(_onAnimationFrame); } /** @@ -62,9 +78,13 @@ class FlxAtlasSprite extends FlxAnimate */ public function listAnimations():Array { - if (this.anim == null) return []; - return this.anim.getFrameLabels(); - // return [""]; + var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name]; + if (mainSymbol == null) + { + FlxG.log.error('FlxAtlasSprite does not have its main symbol!'); + return []; + } + return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull(); } /** @@ -107,12 +127,11 @@ class FlxAtlasSprite extends FlxAnimate * @param restart Whether to restart the animation if it is already playing. * @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing * @param loop Whether to loop the animation + * @param startFrame The frame to start the animation on * NOTE: `loop` and `ignoreOther` are not compatible with each other! */ - public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, ?loop:Bool = false):Void + public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void { - if (loop == null) loop = false; - // Skip if not allowed to play animations. if ((!canPlayOtherAnims && !ignoreOther)) return; @@ -128,7 +147,7 @@ class FlxAtlasSprite extends FlxAnimate else { // Resume animation if it's paused. - anim.play('', false, false); + anim.play('', restart, false, startFrame); } } else @@ -141,31 +160,27 @@ class FlxAtlasSprite extends FlxAnimate } } - anim.callback = function(_, frame:Int) { - var offset = loop ? 0 : -1; - - var frameLabel = anim.getFrameLabel(id); - if (frame == (frameLabel.duration + offset) + frameLabel.index) + anim.onComplete.removeAll(); + anim.onComplete.add(function() { + if (loop) { - if (loop) - { - playAnimation(id, true, false, true); - } - else - { - onAnimationFinish.dispatch(id); - } + onAnimationLoopComplete.dispatch(id); + this.anim.play(id, restart, false, startFrame); + this.currentAnimation = id; } - }; - - anim.onComplete = function() { - onAnimationFinish.dispatch(id); - }; + else + { + onAnimationComplete.dispatch(id); + } + }); // Prevent other animations from playing if `ignoreOther` is true. if (ignoreOther) canPlayOtherAnims = false; // Move to the first frame of the animation. + // goToFrameLabel(id); + trace('Playing animation $id'); + this.anim.play(id, restart, false, startFrame); goToFrameLabel(id); this.currentAnimation = id; } @@ -175,6 +190,24 @@ class FlxAtlasSprite extends FlxAnimate super.update(elapsed); } + /** + * Returns true if the animation has finished playing. + * Never true if animation is configured to loop. + */ + public function isAnimationFinished():Bool + { + return this.anim.finished; + } + + /** + * Returns true if the animation has reached the last frame. + * Can be true even if animation is configured to loop. + */ + public function isLoopComplete():Bool + { + return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1)); + } + /** * Stops the current animation. */ @@ -219,4 +252,76 @@ class FlxAtlasSprite extends FlxAnimate // this.currentAnimation = null; this.anim.pause(); } + + function _onAnimationFrame(frame:Int):Void + { + if (currentAnimation != null) + { + onAnimationFrame.dispatch(currentAnimation, frame); + if (isLoopComplete()) onAnimationLoopComplete.dispatch(currentAnimation); + } + } + + function _onAnimationComplete():Void + { + if (currentAnimation != null) + { + onAnimationComplete.dispatch(currentAnimation); + } + } + + var prevFrames:Map = []; + + public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void + { + if (graphic == null || !Assets.exists(graphic)) + { + var prevFrame:Null = prevFrames.get(index); + if (prevFrame == null) return; + + prevFrame.copyTo(frames.getByIndex(index)); + return; + } + + var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo(); + prevFrames.set(index, prevFrame); + + var frame = FlxG.bitmap.add(graphic).imageFrame.frame; + frame.copyTo(frames.getByIndex(index)); + + // Additional sizing fix. + @:privateAccess + if (true) + { + var frame = frames.getByIndex(index); + frame.tileMatrix[0] = prevFrame.frame.width / frame.frame.width; + frame.tileMatrix[3] = prevFrame.frame.height / frame.frame.height; + } + } + + public function getBasePosition():Null + { + var stagePos = new FlxPoint(anim.stageInstance.matrix.tx, anim.stageInstance.matrix.ty); + var instancePos = new FlxPoint(anim.curInstance.matrix.tx, anim.curInstance.matrix.ty); + var firstElement = anim.curSymbol.timeline?.get(0)?.get(0)?.get(0); + if (firstElement == null) return instancePos; + var firstElementPos = new FlxPoint(firstElement.matrix.tx, firstElement.matrix.ty); + + return instancePos + firstElementPos; + } + + public function getPivotPosition():Null + { + return anim.curInstance.symbol.transformationPoint; + } + + public override function destroy():Void + { + for (prevFrameId in prevFrames.keys()) + { + replaceFrameGraphic(prevFrameId, null); + } + + super.destroy(); + } } diff --git a/source/funkin/graphics/shaders/AdjustColorShader.hx b/source/funkin/graphics/shaders/AdjustColorShader.hx new file mode 100644 index 000000000..2b0970eeb --- /dev/null +++ b/source/funkin/graphics/shaders/AdjustColorShader.hx @@ -0,0 +1,55 @@ +package funkin.graphics.shaders; + +import flixel.addons.display.FlxRuntimeShader; +import funkin.Paths; +import openfl.utils.Assets; + +class AdjustColorShader extends FlxRuntimeShader +{ + public var hue(default, set):Float; + public var saturation(default, set):Float; + public var brightness(default, set):Float; + public var contrast(default, set):Float; + + public function new() + { + super(Assets.getText(Paths.frag('adjustColor'))); + // FlxG.debugger.addTrackerProfile(new TrackerProfile(HSVShader, ['hue', 'saturation', 'brightness', 'contrast'])); + hue = 0; + saturation = 0; + brightness = 0; + contrast = 0; + } + + function set_hue(value:Float):Float + { + this.setFloat('hue', value); + this.hue = value; + + return this.hue; + } + + function set_saturation(value:Float):Float + { + this.setFloat('saturation', value); + this.saturation = value; + + return this.saturation; + } + + function set_brightness(value:Float):Float + { + this.setFloat('brightness', value); + this.brightness = value; + + return this.brightness; + } + + function set_contrast(value:Float):Float + { + this.setFloat('contrast', value); + this.contrast = value; + + return this.contrast; + } +} diff --git a/source/funkin/graphics/shaders/RuntimePostEffectShader.hx b/source/funkin/graphics/shaders/RuntimePostEffectShader.hx index 9f49da075..d39f57efe 100644 --- a/source/funkin/graphics/shaders/RuntimePostEffectShader.hx +++ b/source/funkin/graphics/shaders/RuntimePostEffectShader.hx @@ -2,6 +2,7 @@ package funkin.graphics.shaders; import flixel.FlxCamera; import flixel.FlxG; +import flixel.graphics.frames.FlxFrame; import flixel.addons.display.FlxRuntimeShader; import lime.graphics.opengl.GLProgram; import lime.utils.Log; @@ -32,6 +33,9 @@ class RuntimePostEffectShader extends FlxRuntimeShader // equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom) uniform vec4 uCameraBounds; + // equals (frame.left, frame.top, frame.right, frame.bottom) + uniform vec4 uFrameBounds; + // screen coord -> world coord conversion // returns world coord in px vec2 screenToWorld(vec2 screenCoord) { @@ -56,6 +60,25 @@ class RuntimePostEffectShader extends FlxRuntimeShader return (worldCoord - offset) / scale; } + // screen coord -> frame coord conversion + // returns normalized frame coord + vec2 screenToFrame(vec2 screenCoord) { + float left = uFrameBounds.x; + float top = uFrameBounds.y; + float right = uFrameBounds.z; + float bottom = uFrameBounds.w; + float width = right - left; + float height = bottom - top; + + float clampedX = clamp(screenCoord.x, left, right); + float clampedY = clamp(screenCoord.y, top, bottom); + + return vec2( + (clampedX - left) / (width), + (clampedY - top) / (height) + ); + } + // internally used to get the maximum `openfl_TextureCoordv` vec2 bitmapCoordScale() { return openfl_TextureCoordv / screenCoord; @@ -80,6 +103,8 @@ class RuntimePostEffectShader extends FlxRuntimeShader { super(fragmentSource, null, glVersion); uScreenResolution.value = [FlxG.width, FlxG.height]; + uCameraBounds.value = [0, 0, FlxG.width, FlxG.height]; + uFrameBounds.value = [0, 0, FlxG.width, FlxG.height]; } // basically `updateViewInfo(FlxG.width, FlxG.height, FlxG.camera)` is good @@ -89,6 +114,12 @@ class RuntimePostEffectShader extends FlxRuntimeShader uCameraBounds.value = [camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom]; } + public function updateFrameInfo(frame:FlxFrame) + { + // NOTE: uv.width is actually the right pos and uv.height is the bottom pos + uFrameBounds.value = [frame.uv.x, frame.uv.y, frame.uv.width, frame.uv.height]; + } + override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram { try diff --git a/source/funkin/graphics/shaders/RuntimeRainShader.hx b/source/funkin/graphics/shaders/RuntimeRainShader.hx index 239276bbe..68a203179 100644 --- a/source/funkin/graphics/shaders/RuntimeRainShader.hx +++ b/source/funkin/graphics/shaders/RuntimeRainShader.hx @@ -32,6 +32,14 @@ class RuntimeRainShader extends RuntimePostEffectShader return time = value; } + public var spriteMode(default, set):Bool = false; + + function set_spriteMode(value:Bool):Bool + { + this.setBool('uSpriteMode', value); + return spriteMode = value; + } + // The scale of the rain depends on the world coordinate system, so higher resolution makes // the raindrops smaller. This parameter can be used to adjust the total scale of the scene. // The size of the raindrops is proportional to the value of this parameter. diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx index 6b8e9aa3e..b5aefd08d 100644 --- a/source/funkin/input/Controls.hx +++ b/source/funkin/input/Controls.hx @@ -356,9 +356,10 @@ class Controls extends FlxActionSet public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool { - #if debug + #if FEATURE_DEBUG_FUNCTIONS if (!byName.exists(name)) throw 'Invalid name: $name'; #end + var action = byName[name]; if (gamepadOnly) return action.checkFiltered(trigger, GAMEPAD); else @@ -367,7 +368,7 @@ class Controls extends FlxActionSet public function getKeysForAction(name:Action):Array { - #if debug + #if FEATURE_DEBUG_FUNCTIONS if (!byName.exists(name)) throw 'Invalid name: $name'; #end @@ -382,7 +383,7 @@ class Controls extends FlxActionSet public function getButtonsForAction(name:Action):Array { - #if debug + #if FEATURE_DEBUG_FUNCTIONS if (!byName.exists(name)) throw 'Invalid name: $name'; #end diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index c352aa606..52d4624cb 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -7,6 +7,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry; import funkin.data.event.SongEventRegistry; import funkin.data.story.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.notes.notekind.NoteKindManager; import funkin.data.song.SongRegistry; import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.stage.StageRegistry; @@ -27,11 +28,10 @@ class PolymodHandler { /** * The API version that mods should comply with. - * Format this with Semantic Versioning; ... - * Bug fixes increment the patch version, new features increment the minor version. - * Changes that break old mods increment the major version. + * Indicates which mods are compatible with this version of the game. + * Minor updates rarely impact mods but major versions often do. */ - static final API_VERSION:String = '0.1.0'; + static final API_VERSION:String = "0.5.0"; // Constants.VERSION; /** * Where relative to the executable that mods are located. @@ -177,7 +177,7 @@ class PolymodHandler loadedModIds.push(mod.id); } - #if debug + #if FEATURE_DEBUG_FUNCTIONS var fileList:Array = Polymod.listModFiles(PolymodAssetType.IMAGE); trace('Installed mods have replaced ${fileList.length} images.'); for (item in fileList) @@ -233,6 +233,8 @@ class PolymodHandler // NOTE: Scripted classes are automatically aliased to their parent class. Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint); + Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw); + // Add blacklisting for prohibited classes and packages. // `Sys` @@ -251,8 +253,33 @@ class PolymodHandler // Lib.load() can load malicious DLLs Polymod.blacklistImport('cpp.Lib'); + // `Unserializer` + // Unserializerr.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages + Polymod.blacklistImport('Unserializer'); + + // `lime.system.CFFI` + // Can load and execute compiled binaries. + Polymod.blacklistImport('lime.system.CFFI'); + + // `lime.system.JNI` + // Can load and execute compiled binaries. + Polymod.blacklistImport('lime.system.JNI'); + + // `lime.system.System` + // System.load() can load malicious DLLs + Polymod.blacklistImport('lime.system.System'); + + // `lime.utils.Assets` + // Literally just has a private `resolveClass` function for some reason? + Polymod.blacklistImport('lime.utils.Assets'); + Polymod.blacklistImport('openfl.utils.Assets'); + + // `openfl.desktop.NativeProcess` + // Can load native processes on the host operating system. + Polymod.blacklistImport('openfl.desktop.NativeProcess'); + // `polymod.*` - // You can probably unblacklist a module + // Contains functions which may allow for un-blacklisting other modules. for (cls in ClassMacro.listClassesInPackage('polymod')) { if (cls == null) continue; @@ -261,6 +288,7 @@ class PolymodHandler } // `sys.*` + // Access to system utilities such as the file system. for (cls in ClassMacro.listClassesInPackage('sys')) { if (cls == null) continue; @@ -383,6 +411,7 @@ class PolymodHandler StageRegistry.instance.loadEntries(); CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry. + NoteKindManager.loadScripts(); ModuleHandler.loadModuleCache(); } } diff --git a/source/funkin/modding/base/ScriptedFlxUIState.hx b/source/funkin/modding/base/ScriptedFlxUIState.hx deleted file mode 100644 index c58fc294f..000000000 --- a/source/funkin/modding/base/ScriptedFlxUIState.hx +++ /dev/null @@ -1,8 +0,0 @@ -package funkin.modding.base; - -/** - * A script that can be tied to an FlxUIState. - * Create a scripted class that extends FlxUIState to use this. - */ -@:hscriptClass -class ScriptedFlxUIState extends flixel.addons.ui.FlxUIState implements HScriptedClass {} diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index 55c2a8992..643883a43 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -11,6 +11,9 @@ import funkin.modding.events.ScriptEvent.CountdownScriptEvent; import flixel.util.FlxTimer; import funkin.util.EaseUtil; import funkin.audio.FunkinSound; +import openfl.utils.Assets; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.notes.notestyle.NoteStyle; class Countdown { @@ -19,6 +22,24 @@ class Countdown */ public static var countdownStep(default, null):CountdownStep = BEFORE; + /** + * Which alternate graphic/sound on countdown to use. + * This is set via the current notestyle. + * For example, in Week 6 it is `pixel`. + */ + public static var soundSuffix:String = ''; + + /** + * Which alternate graphic on countdown to use. + * You can set this via script. + * For example, in Week 6 it is `-pixel`. + */ + public static var graphicSuffix:String = ''; + + static var noteStyle:NoteStyle; + + static var fallbackNoteStyle:Null; + /** * The currently running countdown. This will be null if there is no countdown running. */ @@ -30,7 +51,7 @@ class Countdown * This will automatically stop and restart the countdown if it is already running. * @returns `false` if the countdown was cancelled by a script. */ - public static function performCountdown(isPixelStyle:Bool):Bool + public static function performCountdown():Bool { countdownStep = BEFORE; var cancelled:Bool = propagateCountdownEvent(countdownStep); @@ -65,10 +86,10 @@ class Countdown // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0)); // Countdown graphic. - showCountdownGraphic(countdownStep, isPixelStyle); + showCountdownGraphic(countdownStep); // Countdown sound. - playCountdownSound(countdownStep, isPixelStyle); + playCountdownSound(countdownStep); // Event handling bullshit. var cancelled:Bool = propagateCountdownEvent(countdownStep); @@ -177,122 +198,69 @@ class Countdown } /** - * Retrieves the graphic to use for this step of the countdown. - * TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles? - * - * This is public so modules can do lol funny shit. + * Reset the countdown configuration to the default. */ - public static function showCountdownGraphic(index:CountdownStep, isPixelStyle:Bool):Void + public static function reset() { - var spritePath:String = null; + noteStyle = null; + } + + /** + * Retrieve the note style data (if we haven't already) + * @param noteStyleId The id of the note style to fetch. Defaults to the one used by the current PlayState. + * @param force Fetch the note style from the registry even if we've already fetched it. + */ + static function fetchNoteStyle(?noteStyleId:String, force:Bool = false):Void + { + if (noteStyle != null && !force) return; + + if (noteStyleId == null) noteStyleId = PlayState.instance?.currentChart?.noteStyle; + + noteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); + if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); + } + + /** + * Retrieves the graphic to use for this step of the countdown. + */ + public static function showCountdownGraphic(index:CountdownStep):Void + { + fetchNoteStyle(); + + var countdownSprite = noteStyle.buildCountdownSprite(index); + if (countdownSprite == null) return; var fadeEase = FlxEase.cubeInOut; - - if (isPixelStyle) - { - fadeEase = EaseUtil.stepped(8); - switch (index) - { - case TWO: - spritePath = 'weeb/pixelUI/ready-pixel'; - case ONE: - spritePath = 'weeb/pixelUI/set-pixel'; - case GO: - spritePath = 'weeb/pixelUI/date-pixel'; - default: - // null - } - } - else - { - switch (index) - { - case TWO: - spritePath = 'ready'; - case ONE: - spritePath = 'set'; - case GO: - spritePath = 'go'; - default: - // null - } - } - - if (spritePath == null) return; - - var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath); - countdownSprite.scrollFactor.set(0, 0); - - if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE)); - - countdownSprite.antialiasing = !isPixelStyle; - - countdownSprite.updateHitbox(); - countdownSprite.screenCenter(); + if (noteStyle.isCountdownSpritePixel(index)) fadeEase = EaseUtil.stepped(8); // Fade sprite in, then out, then destroy it. - FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100}, Conductor.instance.beatLengthMs / 1000, + FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000, { - ease: FlxEase.cubeInOut, + ease: fadeEase, onComplete: function(twn:FlxTween) { countdownSprite.destroy(); } }); - FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000, - { - ease: fadeEase - }); - + countdownSprite.cameras = [PlayState.instance.camHUD]; PlayState.instance.add(countdownSprite); + countdownSprite.screenCenter(); + + var offsets = noteStyle.getCountdownSpriteOffsets(index); + countdownSprite.x += offsets[0]; + countdownSprite.y += offsets[1]; } /** * Retrieves the sound file to use for this step of the countdown. - * TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles? - * - * This is public so modules can do lol funny shit. */ - public static function playCountdownSound(index:CountdownStep, isPixelStyle:Bool):Void + public static function playCountdownSound(step:CountdownStep):FunkinSound { - var soundPath:String = null; + fetchNoteStyle(); + var path = noteStyle.getCountdownSoundPath(step); + if (path == null) return null; - if (isPixelStyle) - { - switch (index) - { - case THREE: - soundPath = 'intro3-pixel'; - case TWO: - soundPath = 'intro2-pixel'; - case ONE: - soundPath = 'intro1-pixel'; - case GO: - soundPath = 'introGo-pixel'; - default: - // null - } - } - else - { - switch (index) - { - case THREE: - soundPath = 'intro3'; - case TWO: - soundPath = 'intro2'; - case ONE: - soundPath = 'intro1'; - case GO: - soundPath = 'introGo'; - default: - // null - } - } - - if (soundPath == null) return; - - FunkinSound.playOnce(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME); + return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME); } public static function decrement(step:CountdownStep):CountdownStep diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index d0c759b16..9ff70bee5 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -306,7 +306,7 @@ class PauseSubState extends MusicBeatSubState metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); if (PlayState.instance?.currentDifficulty != null) { - metadataDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase(); + metadataDifficulty.text += PlayState.instance.currentDifficulty.replace('-', ' ').toTitleCase(); } metadataDifficulty.scrollFactor.set(0, 0); metadata.add(metadataDifficulty); @@ -430,7 +430,7 @@ class PauseSubState extends MusicBeatSubState resume(this); } - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS // to pause the game and get screenshots easy, press H on pause menu! if (FlxG.keys.justPressed.H) { diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 8d7d82aab..f4b177763 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -49,6 +49,7 @@ import funkin.play.notes.NoteSprite; import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.Strumline; import funkin.play.notes.SustainTrail; +import funkin.play.notes.notekind.NoteKindManager; import funkin.play.scoring.Scoring; import funkin.play.song.Song; import funkin.play.stage.Stage; @@ -66,7 +67,7 @@ import lime.ui.Haptic; import openfl.display.BitmapData; import openfl.geom.Rectangle; import openfl.Lib; -#if discord_rpc +#if FEATURE_DISCORD_RPC import Discord.DiscordClient; #end @@ -444,7 +445,7 @@ class PlayState extends MusicBeatSubState */ public var vocals:VoicesGroup; - #if discord_rpc + #if FEATURE_DISCORD_RPC // Discord RPC variables var storyDifficultyText:String = ''; var iconRPC:String = ''; @@ -503,7 +504,7 @@ class PlayState extends MusicBeatSubState public var camGame:FlxCamera; /** - * The camera which contains, and controls visibility of, a video cutscene. + * The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition. */ public var camCutscene:FlxCamera; @@ -578,7 +579,8 @@ class PlayState extends MusicBeatSubState // TODO: Refactor or document var generatedMusic:Bool = false; - var perfectMode:Bool = false; + + var skipEndingTransition:Bool = false; static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK; @@ -694,14 +696,9 @@ class PlayState extends MusicBeatSubState initMinimalMode(); } initStrumlines(); + initPopups(); - // Initialize the judgements and combo meter. - comboPopUps = new PopUpStuff(); - comboPopUps.zIndex = 900; - add(comboPopUps); - comboPopUps.cameras = [camHUD]; - - #if discord_rpc + #if FEATURE_DISCORD_RPC // Initialize Discord Rich Presence. initDiscord(); #end @@ -741,7 +738,7 @@ class PlayState extends MusicBeatSubState rightWatermarkText.cameras = [camHUD]; // Initialize some debug stuff. - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS // Display the version number (and git commit hash) in the bottom right corner. this.rightWatermarkText.text = Constants.VERSION; @@ -900,7 +897,7 @@ class PlayState extends MusicBeatSubState health = Constants.HEALTH_STARTING; songScore = 0; Highscore.tallies.combo = 0; - Countdown.performCountdown(currentStageId.startsWith('school')); + Countdown.performCountdown(); needsReset = false; } @@ -975,12 +972,12 @@ class PlayState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - pauseSubState.camera = camHUD; + pauseSubState.camera = camCutscene; openSubState(pauseSubState); // boyfriendPos.put(); // TODO: Why is this here? } - #if discord_rpc + #if FEATURE_DISCORD_RPC DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); #end } @@ -1039,7 +1036,7 @@ class PlayState extends MusicBeatSubState // Disable updates, preventing animations in the background from playing. persistentUpdate = false; - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS if (FlxG.keys.pressed.THREE) { // TODO: Change the key or delete this? @@ -1050,7 +1047,7 @@ class PlayState extends MusicBeatSubState { #end persistentDraw = false; - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS } #end @@ -1069,7 +1066,7 @@ class PlayState extends MusicBeatSubState moveToGameOver(); } - #if discord_rpc + #if FEATURE_DISCORD_RPC // Game Over doesn't get his own variable because it's only used here DiscordClient.changePresence('Game Over - ' + detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); #end @@ -1165,6 +1162,9 @@ class PlayState extends MusicBeatSubState // super.dispatchEvent(event) dispatches event to module scripts. super.dispatchEvent(event); + // Dispatch event to note kind scripts + NoteKindManager.callEvent(event); + // Dispatch event to stage script. ScriptEventDispatcher.callEvent(currentStage, event); @@ -1176,14 +1176,12 @@ class PlayState extends MusicBeatSubState // Dispatch event to conversation script. ScriptEventDispatcher.callEvent(currentConversation, event); - - // TODO: Dispatch event to note scripts } /** - * Function called before opening a new substate. - * @param subState The substate to open. - */ + * Function called before opening a new substate. + * @param subState The substate to open. + */ public override function openSubState(subState:FlxSubState):Void { // If there is a substate which requires the game to continue, @@ -1239,9 +1237,9 @@ class PlayState extends MusicBeatSubState } /** - * Function called before closing the current substate. - * @param subState - */ + * Function called before closing the current substate. + * @param subState + */ public override function closeSubState():Void { if (Std.isOfType(subState, PauseSubState)) @@ -1280,7 +1278,7 @@ class PlayState extends MusicBeatSubState // Resume the countdown. Countdown.resumeCountdown(); - #if discord_rpc + #if FEATURE_DISCORD_RPC if (startTimer.finished) { DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, @@ -1303,8 +1301,8 @@ class PlayState extends MusicBeatSubState } /** - * Function called when the game window gains focus. - */ + * Function called when the game window gains focus. + */ public override function onFocus():Void { if (VideoCutscene.isPlaying() && FlxG.autoPause && isGamePaused) VideoCutscene.pauseVideo(); @@ -1313,7 +1311,7 @@ class PlayState extends MusicBeatSubState VideoCutscene.resumeVideo(); #end - #if discord_rpc + #if FEATURE_DISCORD_RPC if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) { if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song @@ -1331,15 +1329,15 @@ class PlayState extends MusicBeatSubState } /** - * Function called when the game window loses focus. - */ + * Function called when the game window loses focus. + */ public override function onFocusLost():Void { #if html5 if (FlxG.autoPause) VideoCutscene.pauseVideo(); #end - #if discord_rpc + #if FEATURE_DISCORD_RPC if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); #end @@ -1348,64 +1346,13 @@ class PlayState extends MusicBeatSubState } /** - * Removes any references to the current stage, then clears the stage cache, - * then reloads all the stages. - * - * This is useful for when you want to edit a stage without reloading the whole game. - * Reloading works on both the JSON and the HXC, if applicable. - * - * Call this by pressing F5 on a debug build. - */ - override function debug_refreshModules():Void + * Call this by pressing F5 on a debug build. + */ + override function reloadAssets():Void { - // Prevent further gameplay updates, which will try to reference dead objects. - criticalFailure = true; - - // Remove the current stage. If the stage gets deleted while it's still in use, - // it'll probably crash the game or something. - if (this.currentStage != null) - { - remove(currentStage); - var event:ScriptEvent = new ScriptEvent(DESTROY, false); - ScriptEventDispatcher.callEvent(currentStage, event); - currentStage = null; - } - - if (!overrideMusic) - { - // Stop the instrumental. - if (FlxG.sound.music != null) - { - FlxG.sound.music.destroy(); - FlxG.sound.music = null; - } - - // Stop the vocals. - if (vocals != null && vocals.exists) - { - vocals.destroy(); - vocals = null; - } - } - else - { - // Stop the instrumental. - if (FlxG.sound.music != null) - { - FlxG.sound.music.stop(); - } - - // Stop the vocals. - if (vocals != null && vocals.exists) - { - vocals.stop(); - } - } - - super.debug_refreshModules(); - - var event:ScriptEvent = new ScriptEvent(CREATE, false); - ScriptEventDispatcher.callEvent(currentSong, event); + funkin.modding.PolymodHandler.forceReloadAssets(); + lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id); + LoadingState.loadPlayState(lastParams); } override function stepHit():Bool @@ -1417,17 +1364,6 @@ class PlayState extends MusicBeatSubState if (isGamePaused) return false; - if (!startingSong - && FlxG.sound.music != null - && (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200 - || Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200)) - { - trace("VOCALS NEED RESYNC"); - if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)); - trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)); - resyncVocals(); - } - if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep)); if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep)); @@ -1449,6 +1385,17 @@ class PlayState extends MusicBeatSubState // activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING); } + if (!startingSong + && FlxG.sound.music != null + && (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100 + || Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100)) + { + trace("VOCALS NEED RESYNC"); + if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)); + trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)); + resyncVocals(); + } + // Only bop camera if zoom level is below 135% if (Preferences.zoomCamera && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) @@ -1501,9 +1448,6 @@ class PlayState extends MusicBeatSubState if (playerStrumline != null) playerStrumline.onBeatHit(); if (opponentStrumline != null) opponentStrumline.onBeatHit(); - // Make the characters dance on the beat - danceOnBeat(); - return true; } @@ -1515,28 +1459,8 @@ class PlayState extends MusicBeatSubState } /** - * Handles characters dancing to the beat of the current song. - * - * TODO: Move some of this logic into `Bopper.hx`, or individual character scripts. - */ - function danceOnBeat():Void - { - if (currentStage == null) return; - - // TODO: Add HEY! song events to Tutorial. - if (Conductor.instance.currentBeat % 16 == 15 - && currentStage.getDad().characterId == 'gf' - && Conductor.instance.currentBeat > 16 - && Conductor.instance.currentBeat < 48) - { - currentStage.getBoyfriend().playAnimation('hey', true); - currentStage.getDad().playAnimation('cheer', true); - } - } - - /** - * Initializes the game and HUD cameras. - */ + * Initializes the game and HUD cameras. + */ function initCameras():Void { camGame = new FunkinCamera('playStateCamGame'); @@ -1560,8 +1484,8 @@ class PlayState extends MusicBeatSubState } /** - * Initializes the health bar on the HUD. - */ + * Initializes the health bar on the HUD. + */ function initHealthBar():Void { var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9; @@ -1592,8 +1516,8 @@ class PlayState extends MusicBeatSubState } /** - * Generates the stage and all its props. - */ + * Generates the stage and all its props. + */ function initStage():Void { loadStage(currentStageId); @@ -1613,10 +1537,10 @@ class PlayState extends MusicBeatSubState } /** - * Loads stage data from cache, assembles the props, - * and adds it to the state. - * @param id - */ + * Loads stage data from cache, assembles the props, + * and adds it to the state. + * @param id + */ function loadStage(id:String):Void { currentStage = StageRegistry.instance.fetchEntry(id); @@ -1634,7 +1558,7 @@ class PlayState extends MusicBeatSubState // Add the stage to the scene. this.add(currentStage); - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS FlxG.console.registerObject('stage', currentStage); #end } @@ -1657,8 +1581,8 @@ class PlayState extends MusicBeatSubState } /** - * Generates the character sprites and adds them to the stage. - */ + * Generates the character sprites and adds them to the stage. + */ function initCharacters():Void { if (currentSong == null || currentChart == null) @@ -1737,7 +1661,7 @@ class PlayState extends MusicBeatSubState { currentStage.addCharacter(girlfriend, GF); - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS FlxG.console.registerObject('gf', girlfriend); #end } @@ -1746,7 +1670,7 @@ class PlayState extends MusicBeatSubState { currentStage.addCharacter(boyfriend, BF); - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS FlxG.console.registerObject('bf', boyfriend); #end } @@ -1757,7 +1681,7 @@ class PlayState extends MusicBeatSubState // Camera starts at dad. cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y); - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS FlxG.console.registerObject('dad', dad); #end } @@ -1768,8 +1692,8 @@ class PlayState extends MusicBeatSubState } /** - * Constructs the strumlines for each player. - */ + * Constructs the strumlines for each player. + */ function initStrumlines():Void { var noteStyleId:String = currentChart.noteStyle; @@ -1801,11 +1725,26 @@ class PlayState extends MusicBeatSubState } /** - * Initializes the Discord Rich Presence. - */ + * Configures the judgement and combo popups. + */ + function initPopups():Void + { + var noteStyleId:String = currentChart.noteStyle; + var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); + if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); + // Initialize the judgements and combo meter. + comboPopUps = new PopUpStuff(noteStyle); + comboPopUps.zIndex = 900; + add(comboPopUps); + comboPopUps.cameras = [camHUD]; + } + + /** + * Initializes the Discord Rich Presence. + */ function initDiscord():Void { - #if discord_rpc + #if FEATURE_DISCORD_RPC storyDifficultyText = difficultyString(); iconRPC = currentSong.player2; @@ -1836,9 +1775,9 @@ class PlayState extends MusicBeatSubState } /** - * Initializes the song (applying the chart, generating the notes, etc.) - * Should be done before the countdown starts. - */ + * Initializes the song (applying the chart, generating the notes, etc.) + * Should be done before the countdown starts. + */ function generateSong():Void { if (currentChart == null) @@ -1869,8 +1808,8 @@ class PlayState extends MusicBeatSubState } /** - * Read note data from the chart and generate the notes. - */ + * Read note data from the chart and generate the notes. + */ function regenNoteData(startTime:Float = 0):Void { Highscore.tallies.combo = 0; @@ -1923,27 +1862,26 @@ class PlayState extends MusicBeatSubState } /** - * Prepares to start the countdown. - * Ends any running cutscenes, creates the strumlines, and starts the countdown. - * This is public so that scripts can call it. - */ + * Prepares to start the countdown. + * Ends any running cutscenes, creates the strumlines, and starts the countdown. + * This is public so that scripts can call it. + */ public function startCountdown():Void { // If Countdown.performCountdown returns false, then the countdown was canceled by a script. - var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school')); + var result:Bool = Countdown.performCountdown(); if (!result) return; isInCutscene = false; - camCutscene.visible = false; // TODO: Maybe tween in the camera after any cutscenes. camHUD.visible = true; } /** - * Displays a dialogue cutscene with the given ID. - * This is used by song scripts to display dialogue. - */ + * Displays a dialogue cutscene with the given ID. + * This is used by song scripts to display dialogue. + */ public function startConversation(conversationId:String):Void { isInCutscene = true; @@ -1963,8 +1901,8 @@ class PlayState extends MusicBeatSubState } /** - * Handler function called when a conversation ends. - */ + * Handler function called when a conversation ends. + */ function onConversationComplete():Void { isInCutscene = false; @@ -1983,8 +1921,8 @@ class PlayState extends MusicBeatSubState } /** - * Starts playing the song after the countdown has completed. - */ + * Starts playing the song after the countdown has completed. + */ function startSong():Void { startingSong = false; @@ -2000,7 +1938,9 @@ class PlayState extends MusicBeatSubState return; } - FlxG.sound.music.onComplete = endSong.bind(false); + FlxG.sound.music.onComplete = function() { + endSong(skipEndingTransition); + }; // A negative instrumental offset means the song skips the first few milliseconds of the track. // This just gets added into the startTimestamp behavior so we don't need to do anything extra. FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset); @@ -2017,7 +1957,7 @@ class PlayState extends MusicBeatSubState vocals.pitch = playbackRate; resyncVocals(); - #if discord_rpc + #if FEATURE_DISCORD_RPC // Updating Discord Rich Presence (with Time Left) DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, currentSongLengthMs); #end @@ -2032,26 +1972,28 @@ class PlayState extends MusicBeatSubState } /** - * Resyncronize the vocal tracks if they have become offset from the instrumental. - */ + * Resyncronize the vocal tracks if they have become offset from the instrumental. + */ function resyncVocals():Void { if (vocals == null) return; // Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.) - if (!FlxG.sound.music.playing) return; - + if (!(FlxG.sound.music?.playing ?? false)) return; + var timeToPlayAt:Float = Conductor.instance.songPosition - Conductor.instance.instrumentalOffset; + FlxG.sound.music.pause(); vocals.pause(); - FlxG.sound.music.play(FlxG.sound.music.time); + FlxG.sound.music.time = timeToPlayAt; + FlxG.sound.music.play(false, timeToPlayAt); - vocals.time = FlxG.sound.music.time; - vocals.play(false, FlxG.sound.music.time); + vocals.time = timeToPlayAt; + vocals.play(false, timeToPlayAt); } /** - * Updates the position and contents of the score display. - */ + * Updates the position and contents of the score display. + */ function updateScoreText():Void { // TODO: Add functionality for modules to update the score text. @@ -2068,8 +2010,8 @@ class PlayState extends MusicBeatSubState } /** - * Updates the values of the health bar. - */ + * Updates the values of the health bar. + */ function updateHealthBar():Void { if (isBotPlayMode) @@ -2083,8 +2025,8 @@ class PlayState extends MusicBeatSubState } /** - * Callback executed when one of the note keys is pressed. - */ + * Callback executed when one of the note keys is pressed. + */ function onKeyPress(event:PreciseInputEvent):Void { if (isGamePaused) return; @@ -2094,8 +2036,8 @@ class PlayState extends MusicBeatSubState } /** - * Callback executed when one of the note keys is released. - */ + * Callback executed when one of the note keys is released. + */ function onKeyRelease(event:PreciseInputEvent):Void { if (isGamePaused) return; @@ -2105,8 +2047,8 @@ class PlayState extends MusicBeatSubState } /** - * Handles opponent note hits and player note misses. - */ + * Handles opponent note hits and player note misses. + */ function processNotes(elapsed:Float):Void { if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return; @@ -2279,10 +2221,14 @@ class PlayState extends MusicBeatSubState // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) continue; + // Skip handling the miss in botplay! + if (!isBotPlayMode) + { // Judge the miss. // NOTE: This is what handles the scoring. trace('Missed note! ${note.noteData}'); onNoteMiss(note, event.playSound, event.healthChange); + } note.handledMiss = true; } @@ -2324,8 +2270,8 @@ class PlayState extends MusicBeatSubState } /** - * Spitting out the input for ravy 🙇‍♂️!! - */ + * Spitting out the input for ravy 🙇‍♂️!! + */ var inputSpitter:Array = []; function handleSkippedNotes():Void @@ -2349,9 +2295,9 @@ class PlayState extends MusicBeatSubState } /** - * PreciseInputEvents are put into a queue between update() calls, - * and then processed here. - */ + * PreciseInputEvents are put into a queue between update() calls, + * and then processed here. + */ function processInputQueue():Void { if (inputPressQueue.length + inputReleaseQueue.length == 0) return; @@ -2379,9 +2325,16 @@ class PlayState extends MusicBeatSubState playerStrumline.pressKey(input.noteDirection); + // Don't credit or penalize inputs in Bot Play. + if (isBotPlayMode) continue; + var notesInDirection:Array = notesByDirection[input.noteDirection]; - if (!Constants.GHOST_TAPPING && notesInDirection.length == 0) + #if FEATURE_GHOST_TAPPING + if ((!playerStrumline.mayGhostTap()) && notesInDirection.length == 0) + #else + if (notesInDirection.length == 0) + #end { // Pressed a wrong key with no notes nearby. // Perform a ghost miss (anti-spam). @@ -2391,16 +2344,6 @@ class PlayState extends MusicBeatSubState playerStrumline.playPress(input.noteDirection); trace('PENALTY Score: ${songScore}'); } - else if (Constants.GHOST_TAPPING && (!playerStrumline.mayGhostTap()) && notesInDirection.length == 0) - { - // Pressed a wrong key with notes visible on-screen. - // Perform a ghost miss (anti-spam). - ghostNoteMiss(input.noteDirection, notesInRange.length > 0); - - // Play the strumline animation. - playerStrumline.playPress(input.noteDirection); - trace('PENALTY Score: ${songScore}'); - } else if (notesInDirection.length == 0) { // Press a key with no penalty. @@ -2493,9 +2436,9 @@ class PlayState extends MusicBeatSubState } /** - * Called when a note leaves the screen and is considered missed by the player. - * @param note - */ + * Called when a note leaves the screen and is considered missed by the player. + * @param note + */ function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthChange:Float):Void { // If we are here, we already CALLED the onNoteMiss script hook! @@ -2551,13 +2494,13 @@ class PlayState extends MusicBeatSubState } /** - * Called when a player presses a key with no note present. - * Scripts can modify the amount of health/score lost, whether player animations or sounds are used, - * or even cancel the event entirely. - * - * @param direction - * @param hasPossibleNotes - */ + * Called when a player presses a key with no note present. + * Scripts can modify the amount of health/score lost, whether player animations or sounds are used, + * or even cancel the event entirely. + * + * @param direction + * @param hasPossibleNotes + */ function ghostNoteMiss(direction:NoteDirection, hasPossibleNotes:Bool = true):Void { var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in. @@ -2606,17 +2549,11 @@ class PlayState extends MusicBeatSubState } /** - * Debug keys. Disabled while in cutscenes. - */ + * Debug keys. Disabled while in cutscenes. + */ function debugKeyShit():Void { - #if !debug - perfectMode = false; - #else - if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible; - #end - - #if CHART_EDITOR_SUPPORTED + #if FEATURE_CHART_EDITOR // Open the stage editor overlaying the current state. if (controls.DEBUG_STAGE) { @@ -2646,7 +2583,10 @@ class PlayState extends MusicBeatSubState } #end - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS + // H: Hide the HUD. + if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible; + // 1: End the song immediately. if (FlxG.keys.justPressed.ONE) endSong(true); @@ -2660,7 +2600,7 @@ class PlayState extends MusicBeatSubState // 9: Toggle the old icon. if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon(); - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS // PAGEUP: Skip forward two sections. // SHIFT+PAGEUP: Skip forward twenty sections. if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 20 : 2); @@ -2673,8 +2613,8 @@ class PlayState extends MusicBeatSubState } /** - * Handles applying health, score, and ratings. - */ + * Handles applying health, score, and ratings. + */ function applyScore(score:Int, daRating:String, healthChange:Float, isComboBreak:Bool) { switch (daRating) @@ -2706,8 +2646,8 @@ class PlayState extends MusicBeatSubState } /** - * Handles rating popups when a note is hit. - */ + * Handles rating popups when a note is hit. + */ function popUpScore(daRating:String, ?combo:Int):Void { if (daRating == 'miss') @@ -2764,10 +2704,10 @@ class PlayState extends MusicBeatSubState } /** - * Handle keyboard inputs during cutscenes. - * This includes advancing conversations and skipping videos. - * @param elapsed Time elapsed since last game update. - */ + * Handle keyboard inputs during cutscenes. + * This includes advancing conversations and skipping videos. + * @param elapsed Time elapsed since last game update. + */ function handleCutsceneKeys(elapsed:Float):Void { if (isGamePaused) return; @@ -2811,20 +2751,20 @@ class PlayState extends MusicBeatSubState } /** - * Handle logic for actually skipping a video cutscene after it has been held. - */ + * Handle logic for actually skipping a video cutscene after it has been held. + */ function skipVideoCutscene():Void { VideoCutscene.finishVideo(); } /** - * End the song. Handle saving high scores and transitioning to the results screen. - * - * Broadcasts an `onSongEnd` event, which can be cancelled to prevent the song from ending (for a cutscene or something). - * Remember to call `endSong` again when the song should actually end! - * @param rightGoddamnNow If true, don't play the fancy animation where you zoom onto Girlfriend. Used after a cutscene. - */ + * End the song. Handle saving high scores and transitioning to the results screen. + * + * Broadcasts an `onSongEnd` event, which can be cancelled to prevent the song from ending (for a cutscene or something). + * Remember to call `endSong` again when the song should actually end! + * @param rightGoddamnNow If true, don't play the fancy animation where you zoom onto Girlfriend. Used after a cutscene. + */ public function endSong(rightGoddamnNow:Bool = false):Void { if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; @@ -2979,11 +2919,16 @@ class PlayState extends MusicBeatSubState FunkinSound.playOnce(Paths.sound('Lights_Shut_off'), function() { // no camFollow so it centers on horror tree var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId); + var targetVariation:String = currentVariation; + if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation)) + { + targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION; + } LoadingState.loadPlayState( { targetSong: targetSong, targetDifficulty: PlayStatePlaylist.campaignDifficulty, - targetVariation: currentVariation, + targetVariation: targetVariation, cameraFollowPoint: cameraFollowPoint.getPosition(), }); }); @@ -2991,11 +2936,16 @@ class PlayState extends MusicBeatSubState else { var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId); + var targetVariation:String = currentVariation; + if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation)) + { + targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION; + } LoadingState.loadPlayState( { targetSong: targetSong, targetDifficulty: PlayStatePlaylist.campaignDifficulty, - targetVariation: currentVariation, + targetVariation: targetVariation, cameraFollowPoint: cameraFollowPoint.getPosition(), }); } @@ -3029,8 +2979,8 @@ class PlayState extends MusicBeatSubState } /** - * Perform necessary cleanup before leaving the PlayState. - */ + * Perform necessary cleanup before leaving the PlayState. + */ function performCleanup():Void { // If the camera is being tweened, stop it. @@ -3081,14 +3031,15 @@ class PlayState extends MusicBeatSubState GameOverSubState.reset(); PauseSubState.reset(); + Countdown.reset(); // Clear the static reference to this state. instance = null; } /** - * Play the camera zoom animation and then move to the results screen once it's done. - */ + * Play the camera zoom animation and then move to the results screen once it's done. + */ function zoomIntoResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void { trace('WENT TO RESULTS SCREEN!'); @@ -3152,17 +3103,17 @@ class PlayState extends MusicBeatSubState // Zoom over to the Results screen. // TODO: Re-enable this. /* - FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1, - { - ease: FlxEase.expoIn, - }); - */ + FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1, + { + ease: FlxEase.expoIn, + }); + */ }); } /** - * Move to the results screen right goddamn now. - */ + * Move to the results screen right goddamn now. + */ function moveToResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void { persistentUpdate = false; @@ -3202,8 +3153,8 @@ class PlayState extends MusicBeatSubState } /** - * Pauses music and vocals easily. - */ + * Pauses music and vocals easily. + */ public function pauseMusic():Void { if (FlxG.sound.music != null) FlxG.sound.music.pause(); @@ -3211,8 +3162,8 @@ class PlayState extends MusicBeatSubState } /** - * Resets the camera's zoom level and focus point. - */ + * Resets the camera's zoom level and focus point. + */ public function resetCamera(?resetZoom:Bool = true, ?cancelTweens:Bool = true):Void { // Cancel camera tweens if any are active. @@ -3234,8 +3185,8 @@ class PlayState extends MusicBeatSubState } /** - * Sets the camera follow point's position and tweens the camera there. - */ + * Sets the camera follow point's position and tweens the camera there. + */ public function tweenCameraToPosition(?x:Float, ?y:Float, ?duration:Float, ?ease:NullFloat>):Void { cameraFollowPoint.setPosition(x, y); @@ -3243,8 +3194,8 @@ class PlayState extends MusicBeatSubState } /** - * Disables camera following and tweens the camera to the follow point manually. - */ + * Disables camera following and tweens the camera to the follow point manually. + */ public function tweenCameraToFollowPoint(?duration:Float, ?ease:NullFloat>):Void { // Cancel the current tween if it's active. @@ -3281,8 +3232,8 @@ class PlayState extends MusicBeatSubState } /** - * Tweens the camera zoom to the desired amount. - */ + * Tweens the camera zoom to the desired amount. + */ public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:NullFloat>):Void { // Cancel the current tween if it's active. @@ -3313,8 +3264,8 @@ class PlayState extends MusicBeatSubState } /** - * Cancel all active camera tweens simultaneously. - */ + * Cancel all active camera tweens simultaneously. + */ public function cancelAllCameraTweens() { cancelCameraFollowTween(); @@ -3324,8 +3275,8 @@ class PlayState extends MusicBeatSubState var prevScrollTargets:Array = []; // used to snap scroll speed when things go unruely /** - * The magical function that shall tween the scroll speed. - */ + * The magical function that shall tween the scroll speed. + */ public function tweenScrollSpeed(?speed:Float, ?duration:Float, ?ease:NullFloat>, strumlines:Array):Void { // Cancel the current tween if it's active. @@ -3375,12 +3326,12 @@ class PlayState extends MusicBeatSubState scrollSpeedTweens = []; } - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS /** - * Jumps forward or backward a number of sections in the song. - * Accounts for BPM changes, does not prevent death from skipped notes. - * @param sections The number of sections to jump, negative to go backwards. - */ + * Jumps forward or backward a number of sections in the song. + * Accounts for BPM changes, does not prevent death from skipped notes. + * @param sections The number of sections to jump, negative to go backwards. + */ function changeSection(sections:Int):Void { // FlxG.sound.music.pause(); diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index c2d9d42b3..b1ff69a3a 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -70,6 +70,8 @@ class ResultState extends MusicBeatSubState delay:Float }> = []; + var playerCharacterId:Null; + var rankBg:FunkinSprite; final cameraBG:FunkinCamera; final cameraScroll:FunkinCamera; @@ -164,7 +166,7 @@ class ResultState extends MusicBeatSubState add(soundSystem); // Fetch playable character data. Default to BF on the results screen if we can't find it. - var playerCharacterId:Null = PlayerRegistry.instance.getCharacterOwnerId(params.characterId); + playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId); var playerCharacter:Null = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); trace('Got playable character: ${playerCharacter?.getName()}'); @@ -189,7 +191,7 @@ class ResultState extends MusicBeatSubState if (!(animData.looped ?? true)) { // Animation is not looped. - animation.onAnimationFinish.add((_name:String) -> { + animation.onAnimationComplete.add((_name:String) -> { if (animation != null) { animation.anim.pause(); @@ -198,7 +200,7 @@ class ResultState extends MusicBeatSubState } else if (animData.loopFrameLabel != null) { - animation.onAnimationFinish.add((_name:String) -> { + animation.onAnimationComplete.add((_name:String) -> { if (animation != null) { animation.playAnimation(animData.loopFrameLabel ?? ''); // unpauses this anim, since it's on PlayOnce! @@ -207,7 +209,7 @@ class ResultState extends MusicBeatSubState } else if (animData.loopFrame != null) { - animation.onAnimationFinish.add((_name:String) -> { + animation.onAnimationComplete.add((_name:String) -> { if (animation != null) { animation.anim.curFrame = animData.loopFrame ?? 0; @@ -742,6 +744,7 @@ class ResultState extends MusicBeatSubState FlxG.switchState(FreeplayState.build( { { + character: playerCharacterId ?? "bf", fromResults: { oldRank: Scoring.calculateRank(params?.prevScoreData), @@ -799,8 +802,9 @@ typedef ResultsStateParams = /** * The character ID for the song we just played. + * @default `bf` */ - var characterId:String; + var ?characterId:String; /** * Whether the displayed score is a new highscore diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index ed58b92b5..32a4e765c 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -109,8 +109,6 @@ class AnimateAtlasCharacter extends BaseCharacter var loop:Bool = animData.looped; this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop); - - animFinished = false; } public override function hasAnimation(name:String):Bool @@ -124,17 +122,17 @@ class AnimateAtlasCharacter extends BaseCharacter */ public override function isAnimationFinished():Bool { - return animFinished; + return mainSprite.isAnimationFinished(); } function loadAtlasSprite():FlxAtlasSprite { trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.'); - var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath, 'shared')); + var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath)); - sprite.onAnimationFinish.removeAll(); - sprite.onAnimationFinish.add(this.onAnimationFinished); + // sprite.onAnimationComplete.removeAll(); + sprite.onAnimationComplete.add(this.onAnimationFinished); return sprite; } @@ -152,7 +150,6 @@ class AnimateAtlasCharacter extends BaseCharacter // Make the game hold on the last frame. this.mainSprite.cleanupAnimation(prefix); // currentAnimName = null; - animFinished = true; // Fallback to idle! // playAnimation('idle', true, false); @@ -165,6 +162,11 @@ class AnimateAtlasCharacter extends BaseCharacter this.mainSprite = sprite; + // This forces the atlas to recalcuate its width and height + this.mainSprite.alpha = 0.0001; + this.mainSprite.draw(); + this.mainSprite.alpha = 1.0; + var feetPos:FlxPoint = feetPosition; this.updateHitbox(); diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 4778e0c1c..28b2dbee2 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -118,22 +118,6 @@ class BaseCharacter extends Bopper */ public var cameraFocusPoint(default, null):FlxPoint = new FlxPoint(0, 0); - override function set_animOffsets(value:Array):Array - { - if (animOffsets == null) value = [0, 0]; - if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value; - - // Make sure animOffets are halved when scale is 0.5. - var xDiff = (animOffsets[0] * this.scale.x / (this.isPixel ? 6 : 1)) - value[0]; - var yDiff = (animOffsets[1] * this.scale.y / (this.isPixel ? 6 : 1)) - value[1]; - - // Call the super function so that camera focus point is not affected. - super.set_x(this.x + xDiff); - super.set_y(this.y + yDiff); - - return animOffsets = value; - } - /** * If the x position changes, other than via changing the animation offset, * then we need to update the camera focus point. @@ -434,7 +418,6 @@ class BaseCharacter extends Bopper else { // Play the idle animation. - // trace('${characterId}: attempting dance'); dance(true); } } @@ -528,6 +511,9 @@ class BaseCharacter extends Bopper { super.onNoteHit(event); + // If another script cancelled the event, don't do anything. + if (event.eventCanceled) return; + if (event.note.noteData.getMustHitNote() && characterType == BF) { // If the note is from the same strumline, play the sing animation. @@ -560,6 +546,9 @@ class BaseCharacter extends Bopper { super.onNoteMiss(event); + // If another script cancelled the event, don't do anything. + if (event.eventCanceled) return; + if (event.note.noteData.getMustHitNote() && characterType == BF) { // If the note is from the same strumline, play the sing animation. @@ -611,7 +600,7 @@ class BaseCharacter extends Bopper /** * Every time a wrong key is pressed, play the miss animation if we are Boyfriend. */ - public override function onNoteGhostMiss(event:GhostMissNoteScriptEvent) + public override function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void { super.onNoteGhostMiss(event); @@ -651,7 +640,6 @@ class BaseCharacter extends Bopper public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void { - // FlxG.watch.addQuick('playAnim(${characterName})', name); super.playAnimation(name, restart, ignoreOther, reversed); } } diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index d447eb97f..bac2c7141 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -305,6 +305,8 @@ class CharacterDataParser icon = "darnell"; case "senpai-angry": icon = "senpai"; + case "spooky-dark": + icon = "spooky"; case "tankman-atlas": icon = "tankman"; } diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx index 41c96fbfa..759d17a6b 100644 --- a/source/funkin/play/character/MultiSparrowCharacter.hx +++ b/source/funkin/play/character/MultiSparrowCharacter.hx @@ -62,11 +62,13 @@ class MultiSparrowCharacter extends BaseCharacter } } - var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath, 'shared'); + var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath); if (texture == null) { trace('Multi-Sparrow atlas could not load PRIMARY texture: ${_data.assetPath}'); + FlxG.log.error('Multi-Sparrow atlas could not load PRIMARY texture: ${_data.assetPath}'); + return; } else { @@ -76,7 +78,7 @@ class MultiSparrowCharacter extends BaseCharacter for (asset in assetList) { - var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset, 'shared'); + var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset); // If we don't do this, the unused textures will be removed as soon as they're loaded. if (subTexture == null) diff --git a/source/funkin/play/character/PackerCharacter.hx b/source/funkin/play/character/PackerCharacter.hx index 22edbe339..5d004606c 100644 --- a/source/funkin/play/character/PackerCharacter.hx +++ b/source/funkin/play/character/PackerCharacter.hx @@ -30,7 +30,7 @@ class PackerCharacter extends BaseCharacter { trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}'); - var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath, 'shared'); + var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath); if (tex == null) { trace('Could not load Packer sprite: ${_data.assetPath}'); diff --git a/source/funkin/play/character/SparrowCharacter.hx b/source/funkin/play/character/SparrowCharacter.hx index 81d98b138..eacf799d8 100644 --- a/source/funkin/play/character/SparrowCharacter.hx +++ b/source/funkin/play/character/SparrowCharacter.hx @@ -33,7 +33,7 @@ class SparrowCharacter extends BaseCharacter { trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}'); - var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared'); + var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath); if (tex == null) { trace('Could not load Sparrow sprite: ${_data.assetPath}'); diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx index 2442b0dc5..0e24d73fb 100644 --- a/source/funkin/play/components/HealthIcon.hx +++ b/source/funkin/play/components/HealthIcon.hx @@ -150,13 +150,17 @@ class HealthIcon extends FunkinSprite { if (characterId == 'bf-old') { + isPixel = PlayState.instance.currentStage.getBoyfriend().isPixel; PlayState.instance.currentStage.getBoyfriend().initHealthIcon(false); } else { characterId = 'bf-old'; + isPixel = false; loadCharacter(characterId); } + + lerpIconSize(true); } /** @@ -200,31 +204,45 @@ class HealthIcon extends FunkinSprite if (bopEvery != 0) { - // Lerp the health icon back to its normal size, - // while maintaining aspect ratio. - if (this.width > this.height) - { - // Apply linear interpolation while accounting for frame rate. - var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15)); - - setGraphicSize(targetSize, 0); - } - else - { - var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15)); - - setGraphicSize(0, targetSize); - } + lerpIconSize(); // Lerp the health icon back to its normal angle. this.angle = MathUtil.coolLerp(this.angle, 0, 0.15); - - this.updateHitbox(); } this.updatePosition(); } + /** + * Does the calculation to lerp the icon size. Usually called every frame, but can be forced to the target size. + * Mainly forced when changing to old icon to not have a weird lerp related to changing from pixel icon to non-pixel old icon + * @param force Force the icon immedialtely to be the target size. Defaults to false. + */ + function lerpIconSize(force:Bool = false):Void + { + // Lerp the health icon back to its normal size, + // while maintaining aspect ratio. + if (this.width > this.height) + { + // Apply linear interpolation while accounting for frame rate. + var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15)); + + if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.x); + + setGraphicSize(targetSize, 0); + } + else + { + var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15)); + + if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.y); + + setGraphicSize(0, targetSize); + } + + this.updateHitbox(); + } + /** * Update the position (and status) of the health icon. */ @@ -412,6 +430,8 @@ class HealthIcon extends FunkinSprite isLegacyStyle = !isNewSpritesheet(charId); + trace(' Loading health icon for character: $charId (legacy: $isLegacyStyle)'); + if (!isLegacyStyle) { loadSparrow('icons/icon-$charId'); diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx index 1bdfd98a8..911c3578c 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -8,58 +8,51 @@ import funkin.graphics.FunkinSprite; import funkin.play.PlayState; import funkin.util.TimerUtil; import funkin.util.EaseUtil; +import openfl.utils.Assets; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.notes.notestyle.NoteStyle; +@:nullSafety class PopUpStuff extends FlxTypedGroup { - public var offsets:Array = [0, 0]; + /** + * The current note style to use. This determines which graphics to display. + * For example, Week 6 uses the `pixel` note style, and mods can create their own. + */ + var noteStyle:NoteStyle; - override public function new() + override public function new(noteStyle:NoteStyle) { super(); + + this.noteStyle = noteStyle; } - public function displayRating(daRating:String):Void + public function displayRating(daRating:Null) { - var perfStart:Float = TimerUtil.start(); - if (daRating == null) daRating = "good"; - var ratingPath:String = daRating; - - if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel"; - - var rating:FunkinSprite = FunkinSprite.create(0, 0, ratingPath); - rating.scrollFactor.set(0.2, 0.2); + var rating:Null = noteStyle.buildJudgementSprite(daRating); + if (rating == null) return; rating.zIndex = 1000; - rating.x = (FlxG.width * 0.474) + offsets[0]; - // rating.x -= FlxG.camera.scroll.x * 0.2; - rating.y = (FlxG.camera.height * 0.45 - 60) + offsets[1]; + + rating.x = (FlxG.width * 0.474); + rating.x -= rating.width / 2; + rating.y = (FlxG.camera.height * 0.45 - 60); + rating.y -= rating.height / 2; + + var offsets = noteStyle.getJudgementSpriteOffsets(daRating); + rating.x += offsets[0]; + rating.y += offsets[1]; + rating.acceleration.y = 550; rating.velocity.y -= FlxG.random.int(140, 175); rating.velocity.x -= FlxG.random.int(0, 10); add(rating); - var fadeEase = null; - - if (PlayState.instance.currentStageId.startsWith('school')) - { - rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7)); - rating.antialiasing = false; - rating.pixelPerfectRender = true; - rating.pixelPerfectPosition = true; - fadeEase = EaseUtil.stepped(2); - } - else - { - rating.setGraphicSize(Std.int(rating.width * 0.65)); - rating.antialiasing = true; - } - rating.updateHitbox(); - - rating.x -= rating.width / 2; - rating.y -= rating.height / 2; + var fadeEase = noteStyle.isJudgementSpritePixel(daRating) ? EaseUtil.stepped(2) : null; FlxTween.tween(rating, {alpha: 0}, 0.2, { @@ -70,62 +63,10 @@ class PopUpStuff extends FlxTypedGroup startDelay: Conductor.instance.beatLengthMs * 0.001, ease: fadeEase }); - - trace('displayRating took: ${TimerUtil.seconds(perfStart)}'); } - public function displayCombo(?combo:Int = 0):Int + public function displayCombo(combo:Int = 0):Void { - var perfStart:Float = TimerUtil.start(); - - if (combo == null) combo = 0; - - var pixelShitPart1:String = ""; - var pixelShitPart2:String = ''; - - if (PlayState.instance.currentStageId.startsWith('school')) - { - pixelShitPart1 = 'weeb/pixelUI/'; - pixelShitPart2 = '-pixel'; - } - var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2); - comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1]; - comboSpr.x = (FlxG.width * 0.507) + offsets[0]; - // comboSpr.x -= FlxG.camera.scroll.x * 0.2; - - comboSpr.acceleration.y = 600; - comboSpr.velocity.y -= 150; - comboSpr.velocity.x += FlxG.random.int(1, 10); - - // add(comboSpr); - - var fadeEase = null; - - if (PlayState.instance.currentStageId.startsWith('school')) - { - comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 1)); - comboSpr.antialiasing = false; - comboSpr.pixelPerfectRender = true; - comboSpr.pixelPerfectPosition = true; - fadeEase = EaseUtil.stepped(2); - } - else - { - comboSpr.setGraphicSize(Std.int(comboSpr.width * 0.7)); - comboSpr.antialiasing = true; - } - comboSpr.updateHitbox(); - - FlxTween.tween(comboSpr, {alpha: 0}, 0.2, - { - onComplete: function(tween:FlxTween) { - remove(comboSpr, true); - comboSpr.destroy(); - }, - startDelay: Conductor.instance.beatLengthMs * 0.001, - ease: fadeEase - }); - var seperatedScore:Array = []; var tempCombo:Int = combo; @@ -140,31 +81,27 @@ class PopUpStuff extends FlxTypedGroup // seperatedScore.reverse(); var daLoop:Int = 1; - for (i in seperatedScore) + for (digit in seperatedScore) { - var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2); + var numScore:Null = noteStyle.buildComboNumSprite(digit); + if (numScore == null) continue; - if (PlayState.instance.currentStageId.startsWith('school')) - { - numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 1)); - numScore.antialiasing = false; - numScore.pixelPerfectRender = true; - numScore.pixelPerfectPosition = true; - } - else - { - numScore.setGraphicSize(Std.int(numScore.width * 0.45)); - numScore.antialiasing = true; - } - numScore.updateHitbox(); + numScore.x = (FlxG.width * 0.507) - (36 * daLoop) - 65; + trace('numScore($daLoop) = ${numScore.x}'); + numScore.y = (FlxG.camera.height * 0.44); + + var offsets = noteStyle.getComboNumSpriteOffsets(digit); + numScore.x += offsets[0]; + numScore.y += offsets[1]; - numScore.x = comboSpr.x - (36 * daLoop) - 65; //- 90; numScore.acceleration.y = FlxG.random.int(250, 300); numScore.velocity.y -= FlxG.random.int(130, 150); numScore.velocity.x = FlxG.random.float(-5, 5); add(numScore); + var fadeEase = noteStyle.isComboNumSpritePixel(digit) ? EaseUtil.stepped(2) : null; + FlxTween.tween(numScore, {alpha: 0}, 0.2, { onComplete: function(tween:FlxTween) { @@ -177,9 +114,5 @@ class PopUpStuff extends FlxTypedGroup daLoop++; } - - trace('displayCombo took: ${TimerUtil.seconds(perfStart)}'); - - return combo; } } diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx index abbcd4f54..60454b881 100644 --- a/source/funkin/play/cutscene/VideoCutscene.hx +++ b/source/funkin/play/cutscene/VideoCutscene.hx @@ -81,7 +81,6 @@ class VideoCutscene // Trigger the cutscene. Don't play the song in the background. PlayState.instance.isInCutscene = true; PlayState.instance.camHUD.visible = false; - PlayState.instance.camCutscene.visible = true; // Display a black screen to hide the game while the video is playing. blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); @@ -305,7 +304,6 @@ class VideoCutscene vid = null; #end - PlayState.instance.camCutscene.visible = true; PlayState.instance.camHUD.visible = true; FlxTween.tween(blackScreen, {alpha: 0}, transitionTime, diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index 748abda19..ee2eea8ad 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -114,7 +114,7 @@ class ZoomCameraSongEvent extends SongEvent name: 'zoom', title: 'Zoom Level', defaultValue: 1.0, - step: 0.1, + step: 0.05, type: SongEventFieldType.FLOAT, units: 'x' }, diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index b16b88466..e8cacaa4d 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -1,6 +1,7 @@ package funkin.play.notes; import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.NoteParamData; import funkin.play.notes.notestyle.NoteStyle; import flixel.graphics.frames.FlxAtlasFrames; import flixel.FlxSprite; @@ -65,6 +66,22 @@ class NoteSprite extends FunkinSprite return this.noteData.kind = value; } + /** + * An array of custom parameters for this note + */ + public var params(get, set):Array; + + function get_params():Array + { + return this.noteData?.params ?? []; + } + + function set_params(value:Array):Array + { + if (this.noteData == null) return value; + return this.noteData.params = value; + } + /** * The data of the note (i.e. the direction.) */ @@ -74,7 +91,7 @@ class NoteSprite extends FunkinSprite { if (frames == null) return value; - animation.play(DIRECTION_COLORS[value] + 'Scroll'); + playNoteAnimation(value); this.direction = value; return this.direction; @@ -135,19 +152,37 @@ class NoteSprite extends FunkinSprite this.hsvShader = new HSVShader(); setupNoteGraphic(noteStyle); - - // Disables the update() function for performance. - this.active = false; } - function setupNoteGraphic(noteStyle:NoteStyle):Void + /** + * Creates frames and animations + * @param noteStyle The `NoteStyle` instance + */ + public function setupNoteGraphic(noteStyle:NoteStyle):Void { noteStyle.buildNoteSprite(this); - setGraphicSize(Strumline.STRUMLINE_SIZE); - updateHitbox(); - this.shader = hsvShader; + + // `false` disables the update() function for performance. + this.active = noteStyle.isNoteAnimated(); + } + + /** + * Retrieve the value of the param with the given name + * @param name Name of the param + * @return Null + */ + public function getParam(name:String):Null + { + for (param in params) + { + if (param.name == name) + { + return param.value; + } + } + return null; } #if FLX_DEBUG @@ -173,6 +208,11 @@ class NoteSprite extends FunkinSprite } #end + function playNoteAnimation(value:Int):Void + { + animation.play(DIRECTION_COLORS[value] + 'Scroll'); + } + public function desaturate():Void { this.hsvShader.saturation = 0.2; diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index fdb32bb85..e894f9c62 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -16,6 +16,7 @@ import funkin.data.song.SongData.SongNoteData; import funkin.ui.options.PreferencesMenu; import funkin.util.SortUtil; import funkin.modding.events.ScriptEvent; +import funkin.play.notes.notekind.NoteKindManager; /** * A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player. @@ -93,6 +94,10 @@ class Strumline extends FlxSpriteGroup final noteStyle:NoteStyle; + #if FEATURE_GHOST_TAPPING + var ghostTapTimer:Float = 0.0; + #end + /** * The note data for the song. Should NOT be altered after the song starts, * so we can easily rewind. @@ -178,21 +183,36 @@ class Strumline extends FlxSpriteGroup super.update(elapsed); updateNotes(); + + #if FEATURE_GHOST_TAPPING + updateGhostTapTimer(elapsed); + #end } + #if FEATURE_GHOST_TAPPING /** * Returns `true` if no notes are in range of the strumline and the player can spam without penalty. */ public function mayGhostTap():Bool { - // TODO: Refine this. Only querying "can be hit" is too tight but "is being rendered" is too loose. - // Also, if you just hit a note, there should be a (short) period where this is off so you can't spam. + // Any notes in range of the strumline. + if (getNotesMayHit().length > 0) + { + return false; + } + // Any hold notes in range of the strumline. + if (getHoldNotesHitOrMissed().length > 0) + { + return false; + } - // If there are any notes on screen, we can't ghost tap. - return notes.members.filter(function(note:NoteSprite) { - return note != null && note.alive && !note.hasBeenHit; - }).length == 0; + // Note has been hit recently. + if (ghostTapTimer > 0.0) return false; + + // **yippee** + return true; } + #end /** * Return notes that are within `Constants.HIT_WINDOW` ms of the strumline. @@ -491,6 +511,32 @@ class Strumline extends FlxSpriteGroup } } + /** + * Return notes that are within, or way after, `Constants.HIT_WINDOW` ms of the strumline. + * @return An array of `NoteSprite` objects. + */ + public function getNotesOnScreen():Array + { + return notes.members.filter(function(note:NoteSprite) { + return note != null && note.alive && !note.hasBeenHit; + }); + } + + #if FEATURE_GHOST_TAPPING + function updateGhostTapTimer(elapsed:Float):Void + { + // If it's still our turn, don't update the ghost tap timer. + if (getNotesOnScreen().length > 0) return; + + ghostTapTimer -= elapsed; + + if (ghostTapTimer <= 0) + { + ghostTapTimer = 0; + } + } + #end + /** * Called when the PlayState skips a large amount of time forward or backward. */ @@ -562,6 +608,10 @@ class Strumline extends FlxSpriteGroup playStatic(dir); } resetScrollSpeed(); + + #if FEATURE_GHOST_TAPPING + ghostTapTimer = 0; + #end } public function applyNoteData(data:Array):Void @@ -601,6 +651,10 @@ class Strumline extends FlxSpriteGroup note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition; } + + #if FEATURE_GHOST_TAPPING + ghostTapTimer = Constants.GHOST_TAP_DELAY; + #end } public function killNote(note:NoteSprite):Void @@ -708,11 +762,15 @@ class Strumline extends FlxSpriteGroup if (noteSprite != null) { + var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle; + noteSprite.setupNoteGraphic(noteKindStyle); + noteSprite.direction = note.getDirection(); noteSprite.noteData = note; noteSprite.x = this.x; noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]); + noteSprite.x -= (noteSprite.width - Strumline.STRUMLINE_SIZE) / 2; // Center it noteSprite.x -= NUDGE; // noteSprite.x += INITIAL_OFFSET; noteSprite.y = -9999; @@ -727,6 +785,9 @@ class Strumline extends FlxSpriteGroup if (holdNoteSprite != null) { + var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle; + holdNoteSprite.setupHoldNoteGraphic(noteKindStyle); + holdNoteSprite.parentStrumline = this; holdNoteSprite.noteData = note; holdNoteSprite.strumTime = note.time; diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx index 40d893255..d8230aa28 100644 --- a/source/funkin/play/notes/StrumlineNote.hx +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -75,6 +75,13 @@ class StrumlineNote extends FlxSprite function setup(noteStyle:NoteStyle):Void { + if (noteStyle == null) + { + // If you get an exception on this line, check the debug console. + // You probably have a parsing error in your note style's JSON file. + throw "FATAL ERROR: Attempted to initialize PlayState with an invalid NoteStyle."; + } + noteStyle.applyStrumlineFrames(this); noteStyle.applyStrumlineAnimations(this, this.direction); diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index f6d43b33f..90b36b009 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -99,7 +99,27 @@ class SustainTrail extends FlxSprite */ public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle) { - super(0, 0, noteStyle.getHoldNoteAssetPath()); + super(0, 0); + + // BASIC SETUP + this.sustainLength = sustainLength; + this.fullSustainLength = sustainLength; + this.noteDirection = noteDirection; + + setupHoldNoteGraphic(noteStyle); + + indices = new DrawData(12, true, TRIANGLE_VERTEX_INDICES); + + this.active = true; // This NEEDS to be true for the note to be drawn! + } + + /** + * Creates hold note graphic and applies correct zooming + * @param noteStyle The note style + */ + public function setupHoldNoteGraphic(noteStyle:NoteStyle):Void + { + loadGraphic(noteStyle.getHoldNoteAssetPath()); antialiasing = true; @@ -109,13 +129,14 @@ class SustainTrail extends FlxSprite endOffset = bottomClip = 1; antialiasing = false; } + else + { + endOffset = 0.5; + bottomClip = 0.9; + } + + zoom = 1.0; zoom *= noteStyle.fetchHoldNoteScale(); - - // BASIC SETUP - this.sustainLength = sustainLength; - this.fullSustainLength = sustainLength; - this.noteDirection = noteDirection; - zoom *= 0.7; // CALCULATE SIZE @@ -131,9 +152,6 @@ class SustainTrail extends FlxSprite updateColorTransform(); updateClipping(); - indices = new DrawData(12, true, TRIANGLE_VERTEX_INDICES); - - this.active = true; // This NEEDS to be true for the note to be drawn! } function getBaseScrollSpeed() @@ -195,6 +213,11 @@ class SustainTrail extends FlxSprite */ public function updateClipping(songTime:Float = 0):Void { + if (graphic == null) + { + return; + } + var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight); if (clipHeight <= 0.1) { diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx new file mode 100644 index 000000000..c1c6e815a --- /dev/null +++ b/source/funkin/play/notes/notekind/NoteKind.hx @@ -0,0 +1,119 @@ +package funkin.play.notes.notekind; + +import funkin.modding.IScriptedClass.INoteScriptedClass; +import funkin.modding.events.ScriptEvent; +import flixel.math.FlxMath; + +/** + * Class for note scripts + */ +class NoteKind implements INoteScriptedClass +{ + /** + * The name of the note kind + */ + public var noteKind:String; + + /** + * Description used in chart editor + */ + public var description:String; + + /** + * Custom note style + */ + public var noteStyleId:Null; + + /** + * Custom parameters for the chart editor + */ + public var params:Array; + + public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array) + { + this.noteKind = noteKind; + this.description = description; + this.noteStyleId = noteStyleId; + this.params = params ?? []; + } + + public function toString():String + { + return noteKind; + } + + /** + * Retrieve all notes of this kind + * @return Array + */ + function getNotes():Array + { + var allNotes:Array = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members); + return allNotes.filter(function(note:NoteSprite) { + return note != null && note.noteData.kind == this.noteKind; + }); + } + + public function onScriptEvent(event:ScriptEvent):Void {} + + public function onCreate(event:ScriptEvent):Void {} + + public function onDestroy(event:ScriptEvent):Void {} + + public function onUpdate(event:UpdateScriptEvent):Void {} + + public function onNoteIncoming(event:NoteScriptEvent):Void {} + + public function onNoteHit(event:HitNoteScriptEvent):Void {} + + public function onNoteMiss(event:NoteScriptEvent):Void {} +} + +/** + * Abstract for setting the type of the `NoteKindParam` + * This was supposed to be an enum but polymod kept being annoying + */ +abstract NoteKindParamType(String) from String to String +{ + public static final STRING:String = 'String'; + + public static final INT:String = 'Int'; + + public static final FLOAT:String = 'Float'; +} + +typedef NoteKindParamData = +{ + /** + * If `min` is null, there is no minimum + */ + ?min:Null, + + /** + * If `max` is null, there is no maximum + */ + ?max:Null, + + /** + * If `step` is null, it will use 1.0 + */ + ?step:Null, + + /** + * If `precision` is null, there will be 0 decimal places + */ + ?precision:Null, + + ?defaultValue:Dynamic +} + +/** + * Typedef for creating custom parameters in the chart editor + */ +typedef NoteKindParam = +{ + name:String, + description:String, + type:NoteKindParamType, + ?data:NoteKindParamData +} diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx new file mode 100644 index 000000000..e17e103d1 --- /dev/null +++ b/source/funkin/play/notes/notekind/NoteKindManager.hx @@ -0,0 +1,121 @@ +package funkin.play.notes.notekind; + +import funkin.modding.events.ScriptEventDispatcher; +import funkin.modding.events.ScriptEvent; +import funkin.ui.debug.charting.util.ChartEditorDropdowns; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.notes.notestyle.NoteStyle; +import funkin.play.notes.notekind.ScriptedNoteKind; +import funkin.play.notes.notekind.NoteKind.NoteKindParam; + +class NoteKindManager +{ + static var noteKinds:Map = []; + + public static function loadScripts():Void + { + var scriptedClassName:Array = ScriptedNoteKind.listScriptClasses(); + if (scriptedClassName.length > 0) + { + trace('Instantiating ${scriptedClassName.length} scripted note kind(s)...'); + for (scriptedClass in scriptedClassName) + { + try + { + var script:NoteKind = ScriptedNoteKind.init(scriptedClass, "unknown"); + trace(' Initialized scripted note kind: ${script.noteKind}'); + noteKinds.set(script.noteKind, script); + ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description); + } + catch (e) + { + trace(' FAILED to instantiate scripted note kind: ${scriptedClass}'); + trace(e); + } + } + } + } + + /** + * Calls the given event for note kind scripts + * @param event The event + */ + public static function callEvent(event:ScriptEvent):Void + { + // if it is a note script event, + // then only call the event for the specific note kind script + if (Std.isOfType(event, NoteScriptEvent)) + { + var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent); + + var noteKind:NoteKind = noteKinds.get(noteEvent.note.kind); + + if (noteKind != null) + { + ScriptEventDispatcher.callEvent(noteKind, event); + } + } + else // call the event for all note kind scripts + { + for (noteKind in noteKinds.iterator()) + { + ScriptEventDispatcher.callEvent(noteKind, event); + } + } + } + + /** + * Retrieve the note style from the given note kind + * @param noteKind note kind name + * @param suffix Used for song note styles + * @return NoteStyle + */ + public static function getNoteStyle(noteKind:String, ?suffix:String):Null + { + var noteStyleId:Null = getNoteStyleId(noteKind, suffix); + + if (noteStyleId == null) + { + return null; + } + + return NoteStyleRegistry.instance.fetchEntry(noteStyleId); + } + + /** + * Retrieve the note style id from the given note kind + * @param noteKind Note kind name + * @param suffix Used for song note styles + * @return Null + */ + public static function getNoteStyleId(noteKind:String, ?suffix:String):Null + { + if (suffix == '') + { + suffix = null; + } + + var noteStyleId:Null = noteKinds.get(noteKind)?.noteStyleId; + if (noteStyleId != null && suffix != null) + { + noteStyleId = NoteStyleRegistry.instance.hasEntry('$noteStyleId-$suffix') ? '$noteStyleId-$suffix' : noteStyleId; + } + + return noteStyleId; + } + + /** + * Retrive custom params of the given note kind + * @param noteKind Name of the note kind + * @return Array + */ + public static function getParams(noteKind:Null):Array + { + if (noteKind == null) + { + return []; + } + + return noteKinds.get(noteKind)?.params ?? []; + } +} diff --git a/source/funkin/play/notes/notekind/ScriptedNoteKind.hx b/source/funkin/play/notes/notekind/ScriptedNoteKind.hx new file mode 100644 index 000000000..cd1781394 --- /dev/null +++ b/source/funkin/play/notes/notekind/ScriptedNoteKind.hx @@ -0,0 +1,9 @@ +package funkin.play.notes.notekind; + +/** + * A script that can be tied to a NoteKind. + * Create a scripted class that extends NoteKind, + * then call `super('noteKind')` in the constructor to use this. + */ +@:hscriptClass +class ScriptedNoteKind extends NoteKind implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index d0cc09f6a..ee07703f1 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -1,5 +1,6 @@ package funkin.play.notes.notestyle; +import funkin.play.Countdown; import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxFramesCollection; import funkin.data.animation.AnimationData; @@ -16,6 +17,7 @@ using funkin.data.animation.AnimationData.AnimationDataUtil; * Holds the data for what assets to use for a note style, * and provides convenience methods for building sprites based on them. */ +@:nullSafety class NoteStyle implements IRegistryEntry { /** @@ -42,12 +44,8 @@ class NoteStyle implements IRegistryEntry this.id = id; _data = _fetchData(id); - if (_data == null) - { - throw 'Could not parse note style data for id: $id'; - } - - this.fallback = NoteStyleRegistry.instance.fetchEntry(getFallbackID()); + var fallbackID = _data.fallback; + if (fallbackID != null) this.fallback = NoteStyleRegistry.instance.fetchEntry(fallbackID); } /** @@ -72,7 +70,7 @@ class NoteStyle implements IRegistryEntry * Get the note style ID of the parent note style. * @return The string ID, or `null` if there is no parent. */ - function getFallbackID():Null + public function getFallbackID():Null { return _data.fallback; } @@ -80,7 +78,7 @@ class NoteStyle implements IRegistryEntry public function buildNoteSprite(target:NoteSprite):Void { // Apply the note sprite frames. - var atlas:FlxAtlasFrames = buildNoteFrames(false); + var atlas:Null = buildNoteFrames(false); if (atlas == null) { @@ -89,29 +87,40 @@ class NoteStyle implements IRegistryEntry target.frames = atlas; - target.scale.x = _data.assets.note.scale; - target.scale.y = _data.assets.note.scale; - target.antialiasing = !_data.assets.note.isPixel; + target.antialiasing = !(_data.assets?.note?.isPixel ?? false); // Apply the animations. buildNoteAnimations(target); + + // Set the scale. + target.setGraphicSize(Strumline.STRUMLINE_SIZE * getNoteScale()); + target.updateHitbox(); } - var noteFrames:FlxAtlasFrames = null; + var noteFrames:Null = null; - function buildNoteFrames(force:Bool = false):FlxAtlasFrames + function buildNoteFrames(force:Bool = false):Null { - if (!FunkinSprite.isTextureCached(Paths.image(getNoteAssetPath()))) + var noteAssetPath = getNoteAssetPath(); + if (noteAssetPath == null) return null; + + if (!FunkinSprite.isTextureCached(Paths.image(noteAssetPath))) { - FlxG.log.warn('Note texture is not cached: ${getNoteAssetPath()}'); + FlxG.log.warn('Note texture is not cached: ${noteAssetPath}'); } // Purge the note frames if the cached atlas is invalid. - if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null; + @:nullSafety(Off) + { + if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null; + } if (noteFrames != null && !force) return noteFrames; - noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary()); + var noteAssetPath = getNoteAssetPath(); + if (noteAssetPath == null) return null; + + noteFrames = Paths.getSparrowAtlas(noteAssetPath, getNoteAssetLibrary()); if (noteFrames == null) { @@ -121,17 +130,18 @@ class NoteStyle implements IRegistryEntry return noteFrames; } - function getNoteAssetPath(raw:Bool = false):String + function getNoteAssetPath(raw:Bool = false):Null { if (raw) { var rawPath:Null = _data?.assets?.note?.assetPath; - if (rawPath == null) return fallback.getNoteAssetPath(true); + if (rawPath == null && fallback != null) return fallback.getNoteAssetPath(true); return rawPath; } // library:path - var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; if (parts.length == 1) return getNoteAssetPath(true); return parts[1]; } @@ -139,47 +149,63 @@ class NoteStyle implements IRegistryEntry function getNoteAssetLibrary():Null { // library:path - var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; if (parts.length == 1) return null; return parts[0]; } function buildNoteAnimations(target:NoteSprite):Void { - var leftData:AnimationData = fetchNoteAnimationData(LEFT); - target.animation.addByPrefix('purpleScroll', leftData.prefix, leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY); - var downData:AnimationData = fetchNoteAnimationData(DOWN); - target.animation.addByPrefix('blueScroll', downData.prefix, downData.frameRate, downData.looped, downData.flipX, downData.flipY); - var upData:AnimationData = fetchNoteAnimationData(UP); - target.animation.addByPrefix('greenScroll', upData.prefix, upData.frameRate, upData.looped, upData.flipX, upData.flipY); - var rightData:AnimationData = fetchNoteAnimationData(RIGHT); - target.animation.addByPrefix('redScroll', rightData.prefix, rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY); + var leftData:Null = fetchNoteAnimationData(LEFT); + if (leftData != null) target.animation.addByPrefix('purpleScroll', leftData.prefix ?? '', leftData.frameRate ?? 24, leftData.looped ?? false, + leftData.flipX, leftData.flipY); + var downData:Null = fetchNoteAnimationData(DOWN); + if (downData != null) target.animation.addByPrefix('blueScroll', downData.prefix ?? '', downData.frameRate ?? 24, downData.looped ?? false, + downData.flipX, downData.flipY); + var upData:Null = fetchNoteAnimationData(UP); + if (upData != null) target.animation.addByPrefix('greenScroll', upData.prefix ?? '', upData.frameRate ?? 24, upData.looped ?? false, upData.flipX, + upData.flipY); + var rightData:Null = fetchNoteAnimationData(RIGHT); + if (rightData != null) target.animation.addByPrefix('redScroll', rightData.prefix ?? '', rightData.frameRate ?? 24, rightData.looped ?? false, + rightData.flipX, rightData.flipY); } - function fetchNoteAnimationData(dir:NoteDirection):AnimationData + public function isNoteAnimated():Bool + { + return _data.assets?.note?.animated ?? false; + } + + public function getNoteScale():Float + { + return _data.assets?.note?.scale ?? 1.0; + } + + function fetchNoteAnimationData(dir:NoteDirection):Null { var result:Null = switch (dir) { - case LEFT: _data.assets.note.data.left.toNamed(); - case DOWN: _data.assets.note.data.down.toNamed(); - case UP: _data.assets.note.data.up.toNamed(); - case RIGHT: _data.assets.note.data.right.toNamed(); + case LEFT: _data.assets?.note?.data?.left?.toNamed(); + case DOWN: _data.assets?.note?.data?.down?.toNamed(); + case UP: _data.assets?.note?.data?.up?.toNamed(); + case RIGHT: _data.assets?.note?.data?.right?.toNamed(); }; - return (result == null) ? fallback.fetchNoteAnimationData(dir) : result; + return (result == null && fallback != null) ? fallback.fetchNoteAnimationData(dir) : result; } - public function getHoldNoteAssetPath(raw:Bool = false):String + public function getHoldNoteAssetPath(raw:Bool = false):Null { if (raw) { // TODO: figure out why ?. didn't work here var rawPath:Null = (_data?.assets?.holdNote == null) ? null : _data?.assets?.holdNote?.assetPath; - return (rawPath == null) ? fallback.getHoldNoteAssetPath(true) : rawPath; + return (rawPath == null && fallback != null) ? fallback.getHoldNoteAssetPath(true) : rawPath; } // library:path - var parts = getHoldNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + var parts = getHoldNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; if (parts.length == 1) return Paths.image(parts[0]); return Paths.image(parts[1], parts[0]); } @@ -187,15 +213,15 @@ class NoteStyle implements IRegistryEntry public function isHoldNotePixel():Bool { var data = _data?.assets?.holdNote; - if (data == null) return fallback.isHoldNotePixel(); - return data.isPixel; + if (data == null && fallback != null) return fallback.isHoldNotePixel(); + return data?.isPixel ?? false; } public function fetchHoldNoteScale():Float { var data = _data?.assets?.holdNote; - if (data == null) return fallback.fetchHoldNoteScale(); - return data.scale; + if (data == null && fallback != null) return fallback.fetchHoldNoteScale(); + return data?.scale ?? 1.0; } public function applyStrumlineFrames(target:StrumlineNote):Void @@ -203,7 +229,7 @@ class NoteStyle implements IRegistryEntry // TODO: Add support for multi-Sparrow. // Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772 - var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath(), getStrumlineAssetLibrary()); + var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath() ?? '', getStrumlineAssetLibrary()); if (atlas == null) { @@ -212,31 +238,30 @@ class NoteStyle implements IRegistryEntry target.frames = atlas; - target.scale.x = _data.assets.noteStrumline.scale; - target.scale.y = _data.assets.noteStrumline.scale; - target.antialiasing = !_data.assets.noteStrumline.isPixel; + target.scale.set(_data.assets.noteStrumline?.scale ?? 1.0); + target.antialiasing = !(_data.assets.noteStrumline?.isPixel ?? false); } - function getStrumlineAssetPath(raw:Bool = false):String + function getStrumlineAssetPath(raw:Bool = false):Null { if (raw) { var rawPath:Null = _data?.assets?.noteStrumline?.assetPath; - if (rawPath == null) return fallback.getStrumlineAssetPath(true); + if (rawPath == null && fallback != null) return fallback.getStrumlineAssetPath(true); return rawPath; } // library:path - var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR); - if (parts.length == 1) return getStrumlineAssetPath(true); + var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length <= 1) return getStrumlineAssetPath(true); return parts[1]; } function getStrumlineAssetLibrary():Null { // library:path - var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR); - if (parts.length == 1) return null; + var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length <= 1) return null; return parts[0]; } @@ -247,60 +272,592 @@ class NoteStyle implements IRegistryEntry function getStrumlineAnimationData(dir:NoteDirection):Array { - var result:Array = switch (dir) + var result:Array> = switch (dir) { case NoteDirection.LEFT: [ - _data.assets.noteStrumline.data.leftStatic.toNamed('static'), - _data.assets.noteStrumline.data.leftPress.toNamed('press'), - _data.assets.noteStrumline.data.leftConfirm.toNamed('confirm'), - _data.assets.noteStrumline.data.leftConfirmHold.toNamed('confirm-hold'), + _data.assets.noteStrumline?.data?.leftStatic?.toNamed('static'), + _data.assets.noteStrumline?.data?.leftPress?.toNamed('press'), + _data.assets.noteStrumline?.data?.leftConfirm?.toNamed('confirm'), + _data.assets.noteStrumline?.data?.leftConfirmHold?.toNamed('confirm-hold'), ]; case NoteDirection.DOWN: [ - _data.assets.noteStrumline.data.downStatic.toNamed('static'), - _data.assets.noteStrumline.data.downPress.toNamed('press'), - _data.assets.noteStrumline.data.downConfirm.toNamed('confirm'), - _data.assets.noteStrumline.data.downConfirmHold.toNamed('confirm-hold'), + _data.assets.noteStrumline?.data?.downStatic?.toNamed('static'), + _data.assets.noteStrumline?.data?.downPress?.toNamed('press'), + _data.assets.noteStrumline?.data?.downConfirm?.toNamed('confirm'), + _data.assets.noteStrumline?.data?.downConfirmHold?.toNamed('confirm-hold'), ]; case NoteDirection.UP: [ - _data.assets.noteStrumline.data.upStatic.toNamed('static'), - _data.assets.noteStrumline.data.upPress.toNamed('press'), - _data.assets.noteStrumline.data.upConfirm.toNamed('confirm'), - _data.assets.noteStrumline.data.upConfirmHold.toNamed('confirm-hold'), + _data.assets.noteStrumline?.data?.upStatic?.toNamed('static'), + _data.assets.noteStrumline?.data?.upPress?.toNamed('press'), + _data.assets.noteStrumline?.data?.upConfirm?.toNamed('confirm'), + _data.assets.noteStrumline?.data?.upConfirmHold?.toNamed('confirm-hold'), ]; case NoteDirection.RIGHT: [ - _data.assets.noteStrumline.data.rightStatic.toNamed('static'), - _data.assets.noteStrumline.data.rightPress.toNamed('press'), - _data.assets.noteStrumline.data.rightConfirm.toNamed('confirm'), - _data.assets.noteStrumline.data.rightConfirmHold.toNamed('confirm-hold'), + _data.assets.noteStrumline?.data?.rightStatic?.toNamed('static'), + _data.assets.noteStrumline?.data?.rightPress?.toNamed('press'), + _data.assets.noteStrumline?.data?.rightConfirm?.toNamed('confirm'), + _data.assets.noteStrumline?.data?.rightConfirmHold?.toNamed('confirm-hold'), ]; + default: []; }; - return result; + return thx.Arrays.filterNull(result); } - public function applyStrumlineOffsets(target:StrumlineNote) + public function applyStrumlineOffsets(target:StrumlineNote):Void { - target.x += _data.assets.noteStrumline.offsets[0]; - target.y += _data.assets.noteStrumline.offsets[1]; + var offsets = _data?.assets?.noteStrumline?.offsets ?? [0.0, 0.0]; + target.x += offsets[0]; + target.y += offsets[1]; } public function getStrumlineScale():Float { - return _data.assets.noteStrumline.scale; + return _data?.assets?.noteStrumline?.scale ?? 1.0; } public function isNoteSplashEnabled():Bool { var data = _data?.assets?.noteSplash?.data; - if (data == null) return fallback.isNoteSplashEnabled(); - return data.enabled; + if (data == null) return fallback?.isNoteSplashEnabled() ?? false; + return data.enabled ?? false; } public function isHoldNoteCoverEnabled():Bool { var data = _data?.assets?.holdNoteCover?.data; - if (data == null) return fallback.isHoldNoteCoverEnabled(); - return data.enabled; + if (data == null) return fallback?.isHoldNoteCoverEnabled() ?? false; + return data.enabled ?? false; + } + + /** + * Build a sprite for the given step of the countdown. + * @param step + * @return A `FunkinSprite`, or `null` if no graphic is available for this step. + */ + public function buildCountdownSprite(step:Countdown.CountdownStep):Null + { + var result = new FunkinSprite(); + + switch (step) + { + case THREE: + if (_data.assets.countdownThree == null) return fallback?.buildCountdownSprite(step); + var assetPath = buildCountdownSpritePath(step); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.countdownThree?.scale ?? 1.0; + result.scale.y = _data.assets.countdownThree?.scale ?? 1.0; + case TWO: + if (_data.assets.countdownTwo == null) return fallback?.buildCountdownSprite(step); + var assetPath = buildCountdownSpritePath(step); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.countdownTwo?.scale ?? 1.0; + result.scale.y = _data.assets.countdownTwo?.scale ?? 1.0; + case ONE: + if (_data.assets.countdownOne == null) return fallback?.buildCountdownSprite(step); + var assetPath = buildCountdownSpritePath(step); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.countdownOne?.scale ?? 1.0; + result.scale.y = _data.assets.countdownOne?.scale ?? 1.0; + case GO: + if (_data.assets.countdownGo == null) return fallback?.buildCountdownSprite(step); + var assetPath = buildCountdownSpritePath(step); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.countdownGo?.scale ?? 1.0; + result.scale.y = _data.assets.countdownGo?.scale ?? 1.0; + default: + // TODO: Do something here? + return null; + } + + result.scrollFactor.set(0, 0); + result.antialiasing = !isCountdownSpritePixel(step); + result.updateHitbox(); + + return result; + } + + function buildCountdownSpritePath(step:Countdown.CountdownStep):Null + { + var basePath:Null = null; + switch (step) + { + case THREE: + basePath = _data.assets.countdownThree?.assetPath; + case TWO: + basePath = _data.assets.countdownTwo?.assetPath; + case ONE: + basePath = _data.assets.countdownOne?.assetPath; + case GO: + basePath = _data.assets.countdownGo?.assetPath; + default: + basePath = null; + } + + if (basePath == null) return fallback?.buildCountdownSpritePath(step); + + var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length < 1) return null; + if (parts.length == 1) return parts[0]; + + return parts[1]; + } + + function buildCountdownSpriteLibrary(step:Countdown.CountdownStep):Null + { + var basePath:Null = null; + switch (step) + { + case THREE: + basePath = _data.assets.countdownThree?.assetPath; + case TWO: + basePath = _data.assets.countdownTwo?.assetPath; + case ONE: + basePath = _data.assets.countdownOne?.assetPath; + case GO: + basePath = _data.assets.countdownGo?.assetPath; + default: + basePath = null; + } + + if (basePath == null) return fallback?.buildCountdownSpriteLibrary(step); + + var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length <= 1) return null; + + return parts[0]; + } + + public function isCountdownSpritePixel(step:Countdown.CountdownStep):Bool + { + switch (step) + { + case THREE: + var result = _data.assets.countdownThree?.isPixel; + if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step); + return result ?? false; + case TWO: + var result = _data.assets.countdownTwo?.isPixel; + if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step); + return result ?? false; + case ONE: + var result = _data.assets.countdownOne?.isPixel; + if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step); + return result ?? false; + case GO: + var result = _data.assets.countdownGo?.isPixel; + if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step); + return result ?? false; + default: + return false; + } + } + + public function getCountdownSpriteOffsets(step:Countdown.CountdownStep):Array + { + switch (step) + { + case THREE: + var result = _data.assets.countdownThree?.offsets; + if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step); + return result ?? [0, 0]; + case TWO: + var result = _data.assets.countdownTwo?.offsets; + if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step); + return result ?? [0, 0]; + case ONE: + var result = _data.assets.countdownOne?.offsets; + if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step); + return result ?? [0, 0]; + case GO: + var result = _data.assets.countdownGo?.offsets; + if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step); + return result ?? [0, 0]; + default: + return [0, 0]; + } + } + + public function getCountdownSoundPath(step:Countdown.CountdownStep, raw:Bool = false):Null + { + if (raw) + { + // TODO: figure out why ?. didn't work here + var rawPath:Null = switch (step) + { + case Countdown.CountdownStep.THREE: + _data.assets.countdownThree?.data?.audioPath; + case Countdown.CountdownStep.TWO: + _data.assets.countdownTwo?.data?.audioPath; + case Countdown.CountdownStep.ONE: + _data.assets.countdownOne?.data?.audioPath; + case Countdown.CountdownStep.GO: + _data.assets.countdownGo?.data?.audioPath; + default: + null; + } + + return (rawPath == null && fallback != null) ? fallback.getCountdownSoundPath(step, true) : rawPath; + } + + // library:path + var parts = getCountdownSoundPath(step, true)?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length == 0) return null; + if (parts.length == 1) return Paths.image(parts[0]); + return Paths.sound(parts[1], parts[0]); + } + + public function buildJudgementSprite(rating:String):Null + { + var result = new FunkinSprite(); + + switch (rating) + { + case "sick": + if (_data.assets.judgementSick == null) return fallback?.buildJudgementSprite(rating); + var assetPath = buildJudgementSpritePath(rating); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.judgementSick?.scale ?? 1.0; + result.scale.y = _data.assets.judgementSick?.scale ?? 1.0; + case "good": + if (_data.assets.judgementGood == null) return fallback?.buildJudgementSprite(rating); + var assetPath = buildJudgementSpritePath(rating); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.judgementGood?.scale ?? 1.0; + result.scale.y = _data.assets.judgementGood?.scale ?? 1.0; + case "bad": + if (_data.assets.judgementBad == null) return fallback?.buildJudgementSprite(rating); + var assetPath = buildJudgementSpritePath(rating); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.judgementBad?.scale ?? 1.0; + result.scale.y = _data.assets.judgementBad?.scale ?? 1.0; + case "shit": + if (_data.assets.judgementShit == null) return fallback?.buildJudgementSprite(rating); + var assetPath = buildJudgementSpritePath(rating); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.judgementShit?.scale ?? 1.0; + result.scale.y = _data.assets.judgementShit?.scale ?? 1.0; + default: + return null; + } + + result.scrollFactor.set(0.2, 0.2); + var isPixel = isJudgementSpritePixel(rating); + result.antialiasing = !isPixel; + result.pixelPerfectRender = isPixel; + result.pixelPerfectPosition = isPixel; + result.updateHitbox(); + + return result; + } + + public function isJudgementSpritePixel(rating:String):Bool + { + switch (rating) + { + case "sick": + var result = _data.assets.judgementSick?.isPixel; + if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating); + return result ?? false; + case "good": + var result = _data.assets.judgementGood?.isPixel; + if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating); + return result ?? false; + case "bad": + var result = _data.assets.judgementBad?.isPixel; + if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating); + return result ?? false; + case "GO": + var result = _data.assets.judgementShit?.isPixel; + if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating); + return result ?? false; + default: + return false; + } + } + + function buildJudgementSpritePath(rating:String):Null + { + var basePath:Null = null; + switch (rating) + { + case "sick": + basePath = _data.assets.judgementSick?.assetPath; + case "good": + basePath = _data.assets.judgementGood?.assetPath; + case "bad": + basePath = _data.assets.judgementBad?.assetPath; + case "shit": + basePath = _data.assets.judgementShit?.assetPath; + default: + basePath = null; + } + + if (basePath == null) return fallback?.buildJudgementSpritePath(rating); + + var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length < 1) return null; + if (parts.length == 1) return parts[0]; + + return parts[1]; + } + + public function getJudgementSpriteOffsets(rating:String):Array + { + switch (rating) + { + case "sick": + var result = _data.assets.judgementSick?.offsets; + if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating); + return result ?? [0, 0]; + case "good": + var result = _data.assets.judgementGood?.offsets; + if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating); + return result ?? [0, 0]; + case "bad": + var result = _data.assets.judgementBad?.offsets; + if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating); + return result ?? [0, 0]; + case "shit": + var result = _data.assets.judgementShit?.offsets; + if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating); + return result ?? [0, 0]; + default: + return [0, 0]; + } + } + + public function buildComboNumSprite(digit:Int):Null + { + var result = new FunkinSprite(); + + switch (digit) + { + case 0: + if (_data.assets.comboNumber0 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber0?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber0?.scale ?? 1.0; + case 1: + if (_data.assets.comboNumber1 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber1?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber1?.scale ?? 1.0; + case 2: + if (_data.assets.comboNumber2 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber2?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber2?.scale ?? 1.0; + case 3: + if (_data.assets.comboNumber3 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber3?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber3?.scale ?? 1.0; + case 4: + if (_data.assets.comboNumber4 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber4?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber4?.scale ?? 1.0; + case 5: + if (_data.assets.comboNumber5 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber5?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber5?.scale ?? 1.0; + case 6: + if (_data.assets.comboNumber6 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber6?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber6?.scale ?? 1.0; + case 7: + if (_data.assets.comboNumber7 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber7?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber7?.scale ?? 1.0; + case 8: + if (_data.assets.comboNumber8 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber8?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber8?.scale ?? 1.0; + case 9: + if (_data.assets.comboNumber9 == null) return fallback?.buildComboNumSprite(digit); + var assetPath = buildComboNumSpritePath(digit); + if (assetPath == null) return null; + result.loadTexture(assetPath); + result.scale.x = _data.assets.comboNumber9?.scale ?? 1.0; + result.scale.y = _data.assets.comboNumber9?.scale ?? 1.0; + default: + return null; + } + + var isPixel = isComboNumSpritePixel(digit); + result.antialiasing = !isPixel; + result.pixelPerfectRender = isPixel; + result.pixelPerfectPosition = isPixel; + result.updateHitbox(); + + return result; + } + + public function isComboNumSpritePixel(digit:Int):Bool + { + switch (digit) + { + case 0: + var result = _data.assets.comboNumber0?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 1: + var result = _data.assets.comboNumber1?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 2: + var result = _data.assets.comboNumber2?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 3: + var result = _data.assets.comboNumber3?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 4: + var result = _data.assets.comboNumber4?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 5: + var result = _data.assets.comboNumber5?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 6: + var result = _data.assets.comboNumber6?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 7: + var result = _data.assets.comboNumber7?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 8: + var result = _data.assets.comboNumber8?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + case 9: + var result = _data.assets.comboNumber9?.isPixel; + if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit); + return result ?? false; + default: + return false; + } + } + + function buildComboNumSpritePath(digit:Int):Null + { + var basePath:Null = null; + switch (digit) + { + case 0: + basePath = _data.assets.comboNumber0?.assetPath; + case 1: + basePath = _data.assets.comboNumber1?.assetPath; + case 2: + basePath = _data.assets.comboNumber2?.assetPath; + case 3: + basePath = _data.assets.comboNumber3?.assetPath; + case 4: + basePath = _data.assets.comboNumber4?.assetPath; + case 5: + basePath = _data.assets.comboNumber5?.assetPath; + case 6: + basePath = _data.assets.comboNumber6?.assetPath; + case 7: + basePath = _data.assets.comboNumber7?.assetPath; + case 8: + basePath = _data.assets.comboNumber8?.assetPath; + case 9: + basePath = _data.assets.comboNumber9?.assetPath; + default: + basePath = null; + } + + if (basePath == null) return fallback?.buildComboNumSpritePath(digit); + + var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? []; + if (parts.length < 1) return null; + if (parts.length == 1) return parts[0]; + + return parts[1]; + } + + public function getComboNumSpriteOffsets(digit:Int):Array + { + switch (digit) + { + case 0: + var result = _data.assets.comboNumber0?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 1: + var result = _data.assets.comboNumber1?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 2: + var result = _data.assets.comboNumber2?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 3: + var result = _data.assets.comboNumber3?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 4: + var result = _data.assets.comboNumber4?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 5: + var result = _data.assets.comboNumber5?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 6: + var result = _data.assets.comboNumber6?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 7: + var result = _data.assets.comboNumber7?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 8: + var result = _data.assets.comboNumber8?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + case 9: + var result = _data.assets.comboNumber9?.offsets; + if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit); + return result ?? [0, 0]; + default: + return [0, 0]; + } } public function destroy():Void {} @@ -310,8 +867,17 @@ class NoteStyle implements IRegistryEntry return 'NoteStyle($id)'; } - static function _fetchData(id:String):Null + static function _fetchData(id:String):NoteStyleData { - return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id)); + var result = NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id)); + + if (result == null) + { + throw 'Could not parse note style data for id: $id'; + } + else + { + return result; + } } } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 91d35d8fa..841600848 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -277,7 +277,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry, ?showLocked:Bool, ?showHidden:Bool):Array + { + var result = []; + + for (variation in variationIds) + { + var difficulties = listDifficulties(variation, null, showLocked, showHidden); + for (difficulty in difficulties) + { + var suffixedDifficulty = (variation != Constants.DEFAULT_VARIATION + && variation != 'erect') ? '$difficulty-${variation}' : difficulty; + result.push(suffixedDifficulty); + } + } + + return result; + } + public function hasDifficulty(diffId:String, ?variationId:String, ?variationIds:Array):Bool { if (variationIds == null) variationIds = []; @@ -706,10 +725,11 @@ class SongDifficulty * Cache the vocals for a given character. * @param id The character we are about to play. */ - public inline function cacheVocals():Void + public function cacheVocals():Void { for (voice in buildVoiceList()) { + trace('Caching vocal track: $voice'); FlxG.sound.cache(voice); } } @@ -721,6 +741,20 @@ class SongDifficulty * @param id The character we are about to play. */ public function buildVoiceList():Array + { + var result:Array = []; + result = result.concat(buildPlayerVoiceList()); + result = result.concat(buildOpponentVoiceList()); + if (result.length == 0) + { + var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; + // Try to use `Voices.ogg` if no other voices are found. + if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix')); + } + return result; + } + + public function buildPlayerVoiceList():Array { var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; @@ -728,62 +762,88 @@ class SongDifficulty // For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`. // Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`. - var playerId:String = characters.player; - var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix'); - while (voicePlayer != null && !Assets.exists(voicePlayer)) + if (characters.playerVocals == null) { - // Remove the last suffix. - // For example, bf-car becomes bf. - playerId = playerId.split('-').slice(0, -1).join('-'); - // Try again. - voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix'); - } - if (voicePlayer == null) - { - // Try again without $suffix. - playerId = characters.player; - voicePlayer = Paths.voices(this.song.id, '-${playerId}'); - while (voicePlayer != null && !Assets.exists(voicePlayer)) + var playerId:String = characters.player; + var playerVoice:String = Paths.voices(this.song.id, '-${playerId}$suffix'); + + while (playerVoice != null && !Assets.exists(playerVoice)) { // Remove the last suffix. + // For example, bf-car becomes bf. playerId = playerId.split('-').slice(0, -1).join('-'); // Try again. - voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix'); + playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix'); + } + if (playerVoice == null) + { + // Try again without $suffix. + playerId = characters.player; + playerVoice = Paths.voices(this.song.id, '-${playerId}'); + while (playerVoice != null && !Assets.exists(playerVoice)) + { + // Remove the last suffix. + playerId = playerId.split('-').slice(0, -1).join('-'); + // Try again. + playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix'); + } } - } - var opponentId:String = characters.opponent; - var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix'); - while (voiceOpponent != null && !Assets.exists(voiceOpponent)) - { - // Remove the last suffix. - opponentId = opponentId.split('-').slice(0, -1).join('-'); - // Try again. - voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix'); + return playerVoice != null ? [playerVoice] : []; } - if (voiceOpponent == null) + else { - // Try again without $suffix. - opponentId = characters.opponent; - voiceOpponent = Paths.voices(this.song.id, '-${opponentId}'); - while (voiceOpponent != null && !Assets.exists(voiceOpponent)) + // The metadata explicitly defines the list of voices. + var playerIds:Array = characters?.playerVocals ?? [characters.player]; + var playerVoices:Array = playerIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix')); + + return playerVoices; + } + } + + public function buildOpponentVoiceList():Array + { + var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; + + // Automatically resolve voices by removing suffixes. + // For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`. + // Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`. + + if (characters.opponentVocals == null) + { + var opponentId:String = characters.opponent; + var opponentVoice:String = Paths.voices(this.song.id, '-${opponentId}$suffix'); + while (opponentVoice != null && !Assets.exists(opponentVoice)) { // Remove the last suffix. opponentId = opponentId.split('-').slice(0, -1).join('-'); // Try again. - voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix'); + opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix'); + } + if (opponentVoice == null) + { + // Try again without $suffix. + opponentId = characters.opponent; + opponentVoice = Paths.voices(this.song.id, '-${opponentId}'); + while (opponentVoice != null && !Assets.exists(opponentVoice)) + { + // Remove the last suffix. + opponentId = opponentId.split('-').slice(0, -1).join('-'); + // Try again. + opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix'); + } } - } - var result:Array = []; - if (voicePlayer != null) result.push(voicePlayer); - if (voiceOpponent != null) result.push(voiceOpponent); - if (voicePlayer == null && voiceOpponent == null) - { - // Try to use `Voices.ogg` if no other voices are found. - if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix')); + return opponentVoice != null ? [opponentVoice] : []; + } + else + { + // The metadata explicitly defines the list of voices. + var opponentIds:Array = characters?.opponentVocals ?? [characters.opponent]; + var opponentVoices:Array = opponentIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix')); + + return opponentVoices; } - return result; } /** @@ -795,26 +855,19 @@ class SongDifficulty { var result:VoicesGroup = new VoicesGroup(); - var voiceList:Array = buildVoiceList(); - - if (voiceList.length == 0) - { - trace('Could not find any voices for song ${this.song.id}'); - return result; - } + var playerVoiceList:Array = this.buildPlayerVoiceList(); + var opponentVoiceList:Array = this.buildOpponentVoiceList(); // Add player vocals. - if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(voiceList[0])); - // Add opponent vocals. - if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(voiceList[1])); - - // Add additional vocals. - if (voiceList.length > 2) + for (playerVoice in playerVoiceList) { - for (i in 2...voiceList.length) - { - result.add(FunkinSound.load(Assets.getSound(voiceList[i]))); - } + result.addPlayerVoice(FunkinSound.load(playerVoice)); + } + + // Add opponent vocals. + for (opponentVoice in opponentVoiceList) + { + result.addOpponentVoice(FunkinSound.load(opponentVoice)); } result.playerVoicesOffset = offsets.getVocalOffset(characters.player); diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 87151de21..96a217d31 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -1,6 +1,7 @@ package funkin.play.stage; import flixel.FlxSprite; +import flixel.FlxCamera; import flixel.math.FlxPoint; import flixel.util.FlxTimer; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; @@ -45,8 +46,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass public var idleSuffix(default, set):String = ''; /** - * If this bopper is rendered with pixel art, - * disable anti-aliasing and render at 6x scale. + * If this bopper is rendered with pixel art, disable anti-aliasing. + * @default `false` */ public var isPixel(default, set):Bool = false; @@ -79,11 +80,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass if (globalOffsets == null) globalOffsets = [0, 0]; if (globalOffsets == value) return value; - var xDiff = globalOffsets[0] - value[0]; - var yDiff = globalOffsets[1] - value[1]; - - this.x += xDiff; - this.y += yDiff; return globalOffsets = value; } @@ -97,12 +93,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass if (animOffsets == null) animOffsets = [0, 0]; if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value; - var xDiff = animOffsets[0] - value[0]; - var yDiff = animOffsets[1] - value[1]; - - this.x += xDiff; - this.y += yDiff; - return animOffsets = value; } @@ -320,19 +310,12 @@ class Bopper extends StageProp implements IPlayStateScriptedClass function applyAnimationOffsets(name:String):Void { var offsets = animationOffsets.get(name); - if (offsets != null && !(offsets[0] == 0 && offsets[1] == 0)) - { - this.animOffsets = [offsets[0] + globalOffsets[0], offsets[1] + globalOffsets[1]]; - } - else - { - this.animOffsets = globalOffsets; - } + this.animOffsets = offsets; } public function isAnimationFinished():Bool { - return this.animation.finished; + return this.animation?.finished ?? false; } public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void @@ -351,6 +334,15 @@ class Bopper extends StageProp implements IPlayStateScriptedClass return this.animation.curAnim.name; } + // override getScreenPosition (used by FlxSprite's draw method) to account for animation offsets. + override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint + { + var output:FlxPoint = super.getScreenPosition(result, camera); + output.x -= (animOffsets[0] - globalOffsets[0]) * this.scale.x; + output.y -= (animOffsets[1] - globalOffsets[1]) * this.scale.y; + return output; + } + public function onPause(event:PauseScriptEvent) {} public function onResume(event:ScriptEvent) {} diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 85b0056ca..c42e41cad 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -386,7 +386,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements { if (character == null) return; - #if debug + #if FEATURE_DEBUG_FUNCTIONS // Temporary marker that shows where the character's location is relative to. // Should display at the stage position of the character (before any offsets). // TODO: Make this a toggle? It's useful to turn on from time to time. @@ -436,8 +436,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements // Start with the per-stage character position. // Subtracting the origin ensures characters are positioned relative to their feet. // Subtracting the global offset allows positioning on a per-character basis. - character.x = stageCharData.position[0] - character.characterOrigin.x + character.globalOffsets[0]; - character.y = stageCharData.position[1] - character.characterOrigin.y + character.globalOffsets[1]; + // We previously applied the global offset here but that is now done elsewhere. + character.x = stageCharData.position[0] - character.characterOrigin.x; + character.y = stageCharData.position[1] - character.characterOrigin.y; @:privateAccess(funkin.play.stage.Bopper) { @@ -451,7 +452,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements character.cameraFocusPoint.x += stageCharData.cameraOffsets[0]; character.cameraFocusPoint.y += stageCharData.cameraOffsets[1]; - #if debug + #if FEATURE_DEBUG_FUNCTIONS // Draw the debug icon at the character's feet. if (charType == BF || charType == DAD) { @@ -468,7 +469,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements ScriptEventDispatcher.callEvent(character, new ScriptEvent(ADDED, false)); - #if debug + #if FEATURE_DEBUG_FUNCTIONS debugIconGroup.add(debugIcon); debugIconGroup.add(debugIcon2); #end @@ -769,39 +770,15 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements * A function that gets called once per step in the song. * @param curStep The current step number. */ - public function onStepHit(event:SongTimeScriptEvent):Void - { - // Override me in your scripted stage to perform custom behavior! - // Make sure to call super.onStepHit(event) if you want to keep the boppers dancing. - - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onStepHit(event:SongTimeScriptEvent):Void {} /** * A function that gets called once per beat in the song (once every four steps). * @param curStep The current beat number. */ - public function onBeatHit(event:SongTimeScriptEvent):Void - { - // Override me in your scripted stage to perform custom behavior! - // Make sure to call super.onBeatHit(event) if you want to keep the boppers dancing. + public function onBeatHit(event:SongTimeScriptEvent):Void {} - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } - - public function onUpdate(event:UpdateScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onUpdate(event:UpdateScriptEvent) {} public override function kill() { @@ -883,129 +860,41 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements public function onScriptEvent(event:ScriptEvent) { + // Ensure all custom events get broadcast to the elements of the stage. + // If we do it here, we don't have to add a handler to EACH script event function. for (bopper in boppers) { ScriptEventDispatcher.callEvent(bopper, event); } } - public function onPause(event:PauseScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onPause(event:PauseScriptEvent) {} - public function onResume(event:ScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onResume(event:ScriptEvent) {} - public function onSongStart(event:ScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onSongStart(event:ScriptEvent) {} - public function onSongEnd(event:ScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onSongEnd(event:ScriptEvent) {} - public function onGameOver(event:ScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onGameOver(event:ScriptEvent) {} - public function onCountdownStart(event:CountdownScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onCountdownStart(event:CountdownScriptEvent) {} - public function onCountdownStep(event:CountdownScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onCountdownStep(event:CountdownScriptEvent) {} - public function onCountdownEnd(event:CountdownScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onCountdownEnd(event:CountdownScriptEvent) {} - public function onNoteIncoming(event:NoteScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onNoteIncoming(event:NoteScriptEvent) {} - public function onNoteHit(event:HitNoteScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onNoteHit(event:HitNoteScriptEvent) {} - public function onNoteMiss(event:NoteScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onNoteMiss(event:NoteScriptEvent) {} - public function onSongEvent(event:SongEventScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onSongEvent(event:SongEventScriptEvent) {} - public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} - public function onSongLoaded(event:SongLoadScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onSongLoaded(event:SongLoadScriptEvent) {} - public function onSongRetry(event:ScriptEvent) - { - for (bopper in boppers) - { - ScriptEventDispatcher.callEvent(bopper, event); - } - } + public function onSongRetry(event:ScriptEvent) {} } diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 2900ce2be..a3d945594 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -121,6 +121,12 @@ class Save modOptions: [], }, + unlocks: + { + // Default to having seen the default character. + charactersSeen: ["bf"], + }, + optionsChartEditor: { // Reasonable defaults. @@ -393,6 +399,22 @@ class Save return data.optionsChartEditor.playbackSpeed; } + public var charactersSeen(get, never):Array; + + function get_charactersSeen():Array + { + return data.unlocks.charactersSeen; + } + + /** + * When we've seen a character unlock, add it to the list of characters seen. + * @param character + */ + public function addCharacterSeen(character:String):Void + { + data.unlocks.charactersSeen.push(character); + } + /** * Return the score the user achieved for a given level on a given difficulty. * @@ -471,10 +493,18 @@ class Save for (difficulty in difficultyList) { var score:Null = getLevelScore(levelId, difficulty); - // TODO: Do we need to check accuracy/score here? if (score != null) { - return true; + if (score.score > 0) + { + // Level has score data, which means we cleared it! + return true; + } + else + { + // Level has score data, but the score is 0. + return false; + } } } return false; @@ -630,10 +660,18 @@ class Save for (difficulty in difficultyList) { var score:Null = getSongScore(songId, difficulty); - // TODO: Do we need to check accuracy/score here? if (score != null) { - return true; + if (score.score > 0) + { + // Level has score data, which means we cleared it! + return true; + } + else + { + // Level has score data, but the score is 0. + return false; + } } } return false; @@ -956,6 +994,8 @@ typedef RawSaveData = */ var options:SaveDataOptions; + var unlocks:SaveDataUnlocks; + /** * The user's favorited songs in the Freeplay menu, * as a list of song IDs. @@ -980,6 +1020,15 @@ typedef SaveApiNewgroundsData = var sessionId:Null; } +typedef SaveDataUnlocks = +{ + /** + * Every time we see the unlock animation for a character, + * add it to this list so that we don't show it again. + */ + var charactersSeen:Array; +} + /** * An anoymous structure containing options about the user's high scores. */ diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx index 186d87c2a..ef74abc1e 100644 --- a/source/funkin/ui/AtlasText.hx +++ b/source/funkin/ui/AtlasText.hx @@ -152,6 +152,32 @@ class AtlasText extends FlxTypedSpriteGroup } } + public function getWidth():Int + { + var width = 0; + for (char in this.text.split("")) + { + switch (char) + { + case " ": + { + width += 40; + } + case "\n": + {} + case char: + { + var sprite = new AtlasChar(atlas, char); + sprite.revive(); + sprite.char = char; + sprite.alpha = 1; + width += Std.int(sprite.width); + } + } + } + return width; + } + override function toString() { return "InputItem, " + FlxStringUtil.getDebugString([ diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx index 92169df75..8668b64c1 100644 --- a/source/funkin/ui/MusicBeatState.hx +++ b/source/funkin/ui/MusicBeatState.hx @@ -78,9 +78,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler { // Emergency exit button. if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState()); - - // This can now be used in EVERY STATE YAY! - if (FlxG.keys.justPressed.F5) debug_refreshModules(); } override function update(elapsed:Float) @@ -114,12 +111,10 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler ModuleHandler.callEvent(event); } - function debug_refreshModules() + function reloadAssets() { PolymodHandler.forceReloadAssets(); - this.destroy(); - // Create a new instance of the current state, so old data is cleared. FlxG.resetState(); } diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx index 9035d12ff..5c40b37bc 100644 --- a/source/funkin/ui/MusicBeatSubState.hx +++ b/source/funkin/ui/MusicBeatSubState.hx @@ -72,9 +72,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler // Emergency exit button. if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState()); - // This can now be used in EVERY STATE YAY! - if (FlxG.keys.justPressed.F5) debug_refreshModules(); - // Display Conductor info in the watch window. FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); Conductor.watchQuick(conductorInUse); @@ -82,7 +79,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler dispatchEvent(new UpdateScriptEvent(elapsed)); } - function debug_refreshModules() + function reloadAssets() { PolymodHandler.forceReloadAssets(); diff --git a/source/funkin/ui/PixelatedIcon.hx b/source/funkin/ui/PixelatedIcon.hx index 8d9b97d9c..d1ea652c3 100644 --- a/source/funkin/ui/PixelatedIcon.hx +++ b/source/funkin/ui/PixelatedIcon.hx @@ -22,14 +22,26 @@ class PixelatedIcon extends FlxSprite switch (char) { - case 'monster-christmas': - charPath += 'monsterpixel'; - case 'mom-car': - charPath += 'mommypixel'; - case 'darnell-blazin': - charPath += 'darnellpixel'; - case 'senpai-angry': - charPath += 'senpaipixel'; + case "bf-christmas" | "bf-car" | "bf-pixel" | "bf-holding-gf": + charPath += "bfpixel"; + case "monster-christmas": + charPath += "monsterpixel"; + case "mom" | "mom-car": + charPath += "mommypixel"; + case "pico-blazin" | "pico-playable" | "pico-speaker": + charPath += "picopixel"; + case "gf-christmas" | "gf-car" | "gf-pixel" | "gf-tankmen": + charPath += "gfpixel"; + case "dad": + charPath += "dadpixel"; + case "darnell-blazin": + charPath += "darnellpixel"; + case "senpai-angry": + charPath += "senpaipixel"; + case "spooky-dark": + charPath += "spookypixel"; + case "tankman-atlas": + charPath += "tankmanpixel"; default: charPath += '${char}pixel'; } diff --git a/source/funkin/ui/charSelect/CharSelectPlayer.hx b/source/funkin/ui/charSelect/CharSelectPlayer.hx index 9322369ba..9052c60e9 100644 --- a/source/funkin/ui/charSelect/CharSelectPlayer.hx +++ b/source/funkin/ui/charSelect/CharSelectPlayer.hx @@ -9,7 +9,7 @@ class CharSelectPlayer extends FlxAtlasSprite { super(x, y, Paths.animateAtlas("charSelect/bfChill")); - onAnimationFinish.add(function(animLabel:String) { + onAnimationComplete.add(function(animLabel:String) { switch (animLabel) { case "slidein": diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx index 14a5b36e0..c02ee3c5a 100644 --- a/source/funkin/ui/charSelect/CharSelectSubState.hx +++ b/source/funkin/ui/charSelect/CharSelectSubState.hx @@ -1,27 +1,31 @@ package funkin.ui.charSelect; -import funkin.ui.freeplay.FreeplayState; -import flixel.text.FlxText; -import funkin.ui.PixelatedIcon; -import flixel.system.debug.watch.Tracker.TrackerProfile; -import flixel.math.FlxPoint; -import flixel.tweens.FlxTween; -import openfl.display.BlendMode; -import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.FlxObject; import flixel.FlxSprite; +import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup; -import funkin.play.stage.Stage; +import flixel.math.FlxPoint; +import flixel.sound.FlxSound; +import flixel.system.debug.watch.Tracker.TrackerProfile; +import flixel.text.FlxText; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxTimer; +import funkin.audio.FunkinSound; +import funkin.data.freeplay.player.PlayerData; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.FunkinCamera; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; -import funkin.graphics.adobeanimate.FlxAtlasSprite; -import flixel.FlxObject; -import openfl.display.BlendMode; -import flixel.group.FlxGroup; +import funkin.play.stage.Stage; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.freeplay.FreeplayState; +import funkin.ui.PixelatedIcon; import funkin.util.MathUtil; -import flixel.util.FlxTimer; -import flixel.tweens.FlxEase; -import flixel.sound.FlxSound; -import funkin.audio.FunkinSound; +import funkin.vis.dsp.SpectralAnalyzer; +import openfl.display.BlendMode; class CharSelectSubState extends MusicBeatSubState { @@ -67,8 +71,29 @@ class CharSelectSubState extends MusicBeatSubState { super(); - availableChars.set(4, "bf"); - availableChars.set(3, "pico"); + loadAvailableCharacters(); + } + + function loadAvailableCharacters():Void + { + var playerIds:Array = PlayerRegistry.instance.listEntryIds(); + + for (playerId in playerIds) + { + var player:Null = PlayerRegistry.instance.fetchEntry(playerId); + if (player == null) continue; + var playerData = player.getCharSelectData(); + if (playerData == null) continue; + + var targetPosition:Int = playerData.position ?? 0; + while (availableChars.exists(targetPosition)) + { + targetPosition += 1; + } + + trace('Placing player ${playerId} at position ${targetPosition}'); + availableChars.set(targetPosition, playerId); + } } override public function create():Void @@ -245,7 +270,7 @@ class CharSelectSubState extends MusicBeatSubState cursorBlue.scrollFactor.set(); cursorDarkBlue.scrollFactor.set(); - FlxTween.color(cursor, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: FlxTween.PINGPONG}); + FlxTween.color(cursor, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: PINGPONG}); // FlxG.debugger.track(cursor); @@ -269,7 +294,6 @@ class CharSelectSubState extends MusicBeatSubState } var grpIcons:FlxSpriteGroup; - var grpXSpread(default, set):Float = 107; var grpYSpread(default, set):Float = 127; @@ -600,7 +624,7 @@ class CharSelectSubState extends MusicBeatSubState playerChill.visible = false; playerChillOut.visible = true; playerChillOut.anim.goToFrameLabel("slideout"); - playerChillOut.anim.callback = (_, frame:Int) -> { + playerChillOut.onAnimationFrame.add((_, frame:Int) -> { if (frame == playerChillOut.anim.getFrameLabel("slideout").index + 1) { playerChill.visible = true; @@ -612,7 +636,7 @@ class CharSelectSubState extends MusicBeatSubState playerChillOut.switchChar(value); playerChillOut.visible = false; } - }; + }); return value; } diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx index 44769e9b3..b37985650 100644 --- a/source/funkin/ui/credits/CreditsState.hx +++ b/source/funkin/ui/credits/CreditsState.hx @@ -34,7 +34,13 @@ class CreditsState extends MusicBeatState * To use a font from the `assets` folder, use `Paths.font(...)`. * Choose something that will render Unicode properly. */ + #if windows static final CREDITS_FONT = 'Consolas'; + #elseif mac + static final CREDITS_FONT = 'Menlo'; + #else + static final CREDITS_FONT = "Courier New"; + #end /** * The size of the font. diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index 590cce88b..fc5f3aa37 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -54,7 +54,7 @@ class DebugMenuSubState extends MusicBeatSubState // Create each menu item. // Call onMenuChange when the first item is created to move the camera . - #if CHART_EDITOR_SUPPORTED + #if FEATURE_CHART_EDITOR onMenuChange(createItem("CHART EDITOR", openChartEditor)); #end // createItem("Input Offset Testing", openInputOffsetTesting); diff --git a/source/funkin/ui/debug/anim/DebugBoundingState.hx b/source/funkin/ui/debug/anim/DebugBoundingState.hx index 04784a5b7..7bb42c89e 100644 --- a/source/funkin/ui/debug/anim/DebugBoundingState.hx +++ b/source/funkin/ui/debug/anim/DebugBoundingState.hx @@ -1,33 +1,26 @@ package funkin.ui.debug.anim; +import flixel.addons.display.FlxBackdrop; import flixel.addons.display.FlxGridOverlay; -import flixel.addons.ui.FlxInputText; -import flixel.addons.ui.FlxUIDropDownMenu; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxState; -import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxFrame; import flixel.group.FlxGroup; import flixel.math.FlxPoint; import flixel.text.FlxText; import flixel.util.FlxColor; -import flixel.util.FlxSpriteUtil; -import flixel.util.FlxTimer; -import funkin.audio.FunkinSound; import funkin.input.Cursor; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; -import funkin.play.character.SparrowCharacter; import funkin.ui.mainmenu.MainMenuState; import funkin.util.MouseUtil; import funkin.util.SerializerUtil; import funkin.util.SortUtil; import haxe.ui.components.DropDown; -import haxe.ui.core.Component; +import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.core.Screen; -import haxe.ui.events.ItemEvent; import haxe.ui.events.UIEvent; import haxe.ui.RuntimeComponentBuilder; import lime.utils.Assets as LimeAssets; @@ -36,9 +29,6 @@ import openfl.events.Event; import openfl.events.IOErrorEvent; import openfl.geom.Rectangle; import openfl.net.FileReference; -import openfl.net.URLLoader; -import openfl.net.URLRequest; -import openfl.utils.ByteArray; using flixel.util.FlxSpriteUtil; @@ -55,10 +45,10 @@ class DebugBoundingState extends FlxState TODAY'S TO-DO - Cleaner UI */ - var bg:FlxSprite; + var bg:FlxBackdrop; var fileInfo:FlxText; - var txtGrp:FlxGroup; + var txtGrp:FlxTypedGroup; var hudCam:FlxCamera; @@ -66,16 +56,23 @@ class DebugBoundingState extends FlxState var spriteSheetView:FlxGroup; var offsetView:FlxGroup; - var animDropDownMenu:FlxUIDropDownMenu; var dropDownSetup:Bool = false; var onionSkinChar:FlxSprite; var txtOffsetShit:FlxText; - var uiStuff:Component; + var offsetEditorDialog:CollapsibleDialog; + var offsetAnimationDropdown:DropDown; var haxeUIFocused(get, default):Bool = false; + var currentAnimationName(get, never):String; + + function get_currentAnimationName():String + { + return offsetAnimationDropdown?.value?.id ?? "idle"; + } + function get_haxeUIFocused():Bool { // get the screen position, according to the HUD camera, temp default to FlxG.camera juuust in case? @@ -87,46 +84,35 @@ class DebugBoundingState extends FlxState { Paths.setCurrentLevel('week1'); - // lv. - // lv.onChange = function(e:UIEvent) - // { - // trace(e.type); - // // trace(e.data.curView); - // // var item:haxe.ui.core.ItemRenderer = cast e.target; - // trace(e.target); - // // if (e.type == "change") - // // { - // // curView = cast e.data; - // // } - // }; - hudCam = new FlxCamera(); hudCam.bgColor.alpha = 0; - bg = FlxGridOverlay.create(10, 10); - // bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.GREEN); - - bg.scrollFactor.set(); + bg = new FlxBackdrop(FlxGridOverlay.createGrid(10, 10, FlxG.width, FlxG.height, true, 0xffe7e6e6, 0xffd9d5d5)); add(bg); // we are setting this as the default draw camera only temporarily, to trick haxeui FlxG.cameras.add(hudCam); var str = Paths.xml('ui/animation-editor/offset-editor-view'); - uiStuff = RuntimeComponentBuilder.fromAsset(str); + offsetEditorDialog = cast RuntimeComponentBuilder.fromAsset(str); - // uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET; - var dropdown:DropDown = cast uiStuff.findComponent("swapper"); - dropdown.onChange = function(e:UIEvent) { + // offsetEditorDialog.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET; + var viewDropdown:DropDown = offsetEditorDialog.findComponent("swapper", DropDown); + viewDropdown.onChange = function(e:UIEvent) { trace(e.type); curView = cast e.data.curView; trace(e.data); // trace(e.data); }; - uiStuff.cameras = [hudCam]; + offsetAnimationDropdown = offsetEditorDialog.findComponent("animationDropdown", DropDown); - add(uiStuff); + offsetEditorDialog.cameras = [hudCam]; + + add(offsetEditorDialog); + + // Anchor to the right side by default + // offsetEditorDialog.x = FlxG.width - offsetEditorDialog.width; // sets the default camera back to FlxG.camera, since we set it to hudCamera for haxeui stuf FlxG.cameras.setDefaultDrawTarget(FlxG.camera, true); @@ -159,7 +145,7 @@ class DebugBoundingState extends FlxState generateOutlines(tex.frames); - txtGrp = new FlxGroup(); + txtGrp = new FlxTypedGroup(); txtGrp.cameras = [hudCam]; spriteSheetView.add(txtGrp); @@ -168,64 +154,6 @@ class DebugBoundingState extends FlxState addInfo('Height', bf.height); spriteSheetView.add(swagOutlines); - - FlxG.stage.window.onDropFile.add(function(path:String) { - // WACKY ASS TESTING SHIT FOR WEB FILE LOADING?? - #if web - var swagList:FileList = cast path; - - var objShit = js.html.URL.createObjectURL(swagList.item(0)); - trace(objShit); - - var funnysound = new FunkinSound().loadStream('https://cdn.discordapp.com/attachments/767500676166451231/817821618251759666/Flutter.mp3', false, false, - null, function() { - trace('LOADED SHIT??'); - }); - - funnysound.volume = 1; - funnysound.play(); - - var urlShit = new URLLoader(new URLRequest(objShit)); - - new FlxTimer().start(3, function(tmr:FlxTimer) { - // music lol! - if (urlShit.dataFormat == BINARY) - { - // var daSwagBytes:ByteArray = urlShit.data; - - // FlxG.sound.playMusic(); - - // trace('is binary!!'); - } - trace(urlShit.dataFormat); - }); - - // remove(bf); - // FlxG.bitmap.removeByKey(Paths.image('characters/temp')); - // Assets.cache.clear(); - - // bf.loadGraphic(objShit); - // add(bf); - - // trace(swagList.item(0).name); - // var urlShit = js.html.URL.createObjectURL(path); - #end - - #if sys - trace("DROPPED FILE FROM: " + Std.string(path)); - var newPath = "./" + Paths.image('characters/temp'); - File.copy(path, newPath); - - var swag = Paths.image('characters/temp'); - - if (bf != null) remove(bf); - FlxG.bitmap.removeByKey(Paths.image('characters/temp')); - Assets.cache.clear(); - - bf.loadGraphic(Paths.image('characters/temp')); - add(bf); - #end - }); } function generateOutlines(frameShit:Array):Void @@ -260,15 +188,9 @@ class DebugBoundingState extends FlxState txtOffsetShit = new FlxText(20, 20, 0, "", 20); txtOffsetShit.setFormat(Paths.font("vcr.ttf"), 26, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); txtOffsetShit.cameras = [hudCam]; + txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height; offsetView.add(txtOffsetShit); - animDropDownMenu = new FlxUIDropDownMenu(0, 0, FlxUIDropDownMenu.makeStrIdLabelArray(['weed'], true)); - animDropDownMenu.cameras = [hudCam]; - // Move to bottom right corner - animDropDownMenu.x = FlxG.width - animDropDownMenu.width - 20; - animDropDownMenu.y = FlxG.height - animDropDownMenu.height - 20; - offsetView.add(animDropDownMenu); - var characters:Array = CharacterDataParser.listCharacterIds(); characters = characters.filter(function(charId:String) { var char = CharacterDataParser.fetchCharacterData(charId); @@ -276,7 +198,7 @@ class DebugBoundingState extends FlxState }); characters.sort(SortUtil.alphabetically); - var charDropdown:DropDown = cast uiStuff.findComponent('characterDropdown'); + var charDropdown:DropDown = offsetEditorDialog.findComponent('characterDropdown', DropDown); for (char in characters) { charDropdown.dataSource.add({text: char}); @@ -289,32 +211,47 @@ class DebugBoundingState extends FlxState public var mouseOffset:FlxPoint = FlxPoint.get(0, 0); public var oldPos:FlxPoint = FlxPoint.get(0, 0); + public var movingCharacter:Bool = false; function mouseOffsetMovement() { if (swagChar != null) { - if (FlxG.mouse.justPressed) + if (FlxG.mouse.justPressed && !haxeUIFocused) { + movingCharacter = true; mouseOffset.set(FlxG.mouse.x - -swagChar.animOffsets[0], FlxG.mouse.y - -swagChar.animOffsets[1]); } + if (!movingCharacter) return; + if (FlxG.mouse.pressed) { swagChar.animOffsets = [(FlxG.mouse.x - mouseOffset.x) * -1, (FlxG.mouse.y - mouseOffset.y) * -1]; - swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, swagChar.animOffsets); + swagChar.animationOffsets.set(offsetAnimationDropdown.value.id, swagChar.animOffsets); txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; + txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height; + } + + if (FlxG.mouse.justReleased) + { + movingCharacter = false; } } } function addInfo(str:String, value:Dynamic) { - var swagText:FlxText = new FlxText(10, 10 + (28 * txtGrp.length)); + var swagText:FlxText = new FlxText(10, FlxG.height - 32); swagText.setFormat(Paths.font("vcr.ttf"), 26, FlxColor.WHITE, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); swagText.scrollFactor.set(); + + for (text in txtGrp.members) + { + text.y -= swagText.height; + } txtGrp.add(swagText); swagText.text = str + ": " + Std.string(value); @@ -345,14 +282,14 @@ class DebugBoundingState extends FlxState { if (FlxG.keys.justPressed.ONE) { - var lv:DropDown = cast uiStuff.findComponent("swapper"); + var lv:DropDown = offsetEditorDialog.findComponent("swapper", DropDown); lv.selectedIndex = 0; curView = SPRITESHEET; } if (FlxG.keys.justReleased.TWO) { - var lv:DropDown = cast uiStuff.findComponent("swapper"); + var lv:DropDown = offsetEditorDialog.findComponent("swapper", DropDown); lv.selectedIndex = 1; curView = ANIMATIONS; if (swagChar != null) @@ -368,12 +305,14 @@ class DebugBoundingState extends FlxState spriteSheetView.visible = true; offsetView.visible = false; offsetView.active = false; + offsetAnimationDropdown.visible = false; case ANIMATIONS: spriteSheetView.visible = false; offsetView.visible = true; offsetView.active = true; + offsetAnimationDropdown.visible = true; offsetControls(); - if (!haxeUIFocused) mouseOffsetMovement(); + mouseOffsetMovement(); } if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible; @@ -395,24 +334,36 @@ class DebugBoundingState extends FlxState { if (FlxG.keys.justPressed.RBRACKET || FlxG.keys.justPressed.E) { - if (Std.parseInt(animDropDownMenu.selectedId) + 1 <= animDropDownMenu.length) - animDropDownMenu.selectedId = Std.string(Std.parseInt(animDropDownMenu.selectedId) - + 1); + if (offsetAnimationDropdown.selectedIndex + 1 <= offsetAnimationDropdown.dataSource.size) + { + offsetAnimationDropdown.selectedIndex += 1; + } else - animDropDownMenu.selectedId = Std.string(0); - playCharacterAnimation(animDropDownMenu.selectedId, true); + { + offsetAnimationDropdown.selectedIndex = 0; + } + trace(offsetAnimationDropdown.selectedIndex); + trace(offsetAnimationDropdown.dataSource.size); + trace(offsetAnimationDropdown.value); + trace(currentAnimationName); + playCharacterAnimation(currentAnimationName, true); } if (FlxG.keys.justPressed.LBRACKET || FlxG.keys.justPressed.Q) { - if (Std.parseInt(animDropDownMenu.selectedId) - 1 >= 0) animDropDownMenu.selectedId = Std.string(Std.parseInt(animDropDownMenu.selectedId) - 1); + if (offsetAnimationDropdown.selectedIndex - 1 >= 0) + { + offsetAnimationDropdown.selectedIndex -= 1; + } else - animDropDownMenu.selectedId = Std.string(animDropDownMenu.length - 1); - playCharacterAnimation(animDropDownMenu.selectedId, true); + { + offsetAnimationDropdown.selectedIndex = offsetAnimationDropdown.dataSource.size - 1; + } + playCharacterAnimation(currentAnimationName, true); } // Keyboards controls for general WASD "movement" - // modifies the animDropDownMenu so that it's properly updated and shit - // and then it's just played and updated from the animDropDownMenu callback, which is set in the loadAnimShit() function probabbly + // modifies the animDrooffsetAnimationDropdownpDownMenu so that it's properly updated and shit + // and then it's just played and updated from the offsetAnimationDropdown callback, which is set in the loadAnimShit() function probabbly if (FlxG.keys.justPressed.W || FlxG.keys.justPressed.S || FlxG.keys.justPressed.D || FlxG.keys.justPressed.A) { var suffix:String = ''; @@ -425,18 +376,19 @@ class DebugBoundingState extends FlxState if (FlxG.keys.justPressed.A) targetLabel = 'singLEFT$suffix'; if (FlxG.keys.justPressed.D) targetLabel = 'singRIGHT$suffix'; - if (targetLabel != animDropDownMenu.selectedLabel) + if (targetLabel != currentAnimationName) { + offsetAnimationDropdown.value = {id: targetLabel, text: targetLabel}; + // Play the new animation if the IDs are the different. // Override the onion skin. - animDropDownMenu.selectedLabel = targetLabel; - playCharacterAnimation(animDropDownMenu.selectedId, true); + playCharacterAnimation(currentAnimationName, true); } else { // Replay the current animation if the IDs are the same. // Don't override the onion skin. - playCharacterAnimation(animDropDownMenu.selectedId, false); + playCharacterAnimation(currentAnimationName, false); } } @@ -448,16 +400,20 @@ class DebugBoundingState extends FlxState // Plays the idle animation if (FlxG.keys.justPressed.SPACE) { - animDropDownMenu.selectedLabel = 'idle'; - playCharacterAnimation(animDropDownMenu.selectedId, true); + offsetAnimationDropdown.value = {id: 'idle', text: 'idle'}; + + playCharacterAnimation(currentAnimationName, true); } // Playback the animation - if (FlxG.keys.justPressed.ENTER) playCharacterAnimation(animDropDownMenu.selectedId, false); + if (FlxG.keys.justPressed.ENTER) + { + playCharacterAnimation(currentAnimationName, false); + } if (FlxG.keys.justPressed.RIGHT || FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.UP || FlxG.keys.justPressed.DOWN) { - var animName = animDropDownMenu.selectedLabel; + var animName = currentAnimationName; var coolValues:Array = swagChar.animationOffsets.get(animName).copy(); var multiplier:Int = 5; @@ -471,10 +427,11 @@ class DebugBoundingState extends FlxState else if (FlxG.keys.justPressed.UP) coolValues[1] += 1 * multiplier; else if (FlxG.keys.justPressed.DOWN) coolValues[1] -= 1 * multiplier; - swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, coolValues); + swagChar.animationOffsets.set(currentAnimationName, coolValues); swagChar.playAnimation(animName); txtOffsetShit.text = 'Offset: ' + coolValues; + txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height; trace(animName); } @@ -529,7 +486,7 @@ class DebugBoundingState extends FlxState swagChar = CharacterDataParser.fetchCharacter(char); swagChar.x = 100; swagChar.y = 100; - // swagChar.debugMode = true; + swagChar.debug = true; offsetView.add(swagChar); if (swagChar == null || swagChar.frames == null) @@ -554,11 +511,25 @@ class DebugBoundingState extends FlxState trace(swagChar.animationOffsets[i]); } - animDropDownMenu.setData(FlxUIDropDownMenu.makeStrIdLabelArray(characterAnimNames, true)); - animDropDownMenu.callback = function(str:String) { - playCharacterAnimation(str, true); - }; + offsetAnimationDropdown.dataSource.clear(); + + for (charAnim in characterAnimNames) + { + trace('Adding ${charAnim} to HaxeUI dropdown'); + offsetAnimationDropdown.dataSource.add({id: charAnim, text: charAnim}); + } + + offsetAnimationDropdown.selectedIndex = 0; + + trace('Added ${offsetAnimationDropdown.dataSource.size} to HaxeUI dropdown'); + + offsetAnimationDropdown.onChange = function(event:UIEvent) { + trace('Selected animation ${event?.data?.id}'); + playCharacterAnimation(event.data.id, true); + } + txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; + txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height; dropDownSetup = true; } @@ -575,11 +546,13 @@ class DebugBoundingState extends FlxState onionSkinChar.alpha = 0.6; } - var animName = characterAnimNames[Std.parseInt(str)]; + // var animName = characterAnimNames[Std.parseInt(str)]; + var animName = str; swagChar.playAnimation(animName, true); // trace(); trace(swagChar.animationOffsets.get(animName)); txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; + txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height; } var _file:FileReference; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index f72cca77f..a1d7aca6b 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -35,6 +35,7 @@ import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongMetadata; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongOffsets; +import funkin.data.song.SongData.NoteParamData; import funkin.data.song.SongDataUtils; import funkin.data.song.SongRegistry; import funkin.data.stage.StageData; @@ -45,6 +46,7 @@ import funkin.input.TurboActionHandler; import funkin.input.TurboButtonHandler; import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; +import funkin.play.notes.notekind.NoteKindManager; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; @@ -282,6 +284,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0; + /** + * A map of the keys for every live input style. + */ + public static final LIVE_INPUT_KEYS:Map> = [ + NumberKeys => [ + FIVE, SIX, SEVEN, EIGHT, + ONE, TWO, THREE, FOUR + ], + WASDKeys => [ + LEFT, DOWN, UP, RIGHT, + A, S, W, D + ], + None => [] + ]; + /** * INSTANCE DATA */ @@ -538,6 +555,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var noteKindToPlace:Null = null; + /** + * The note params to use for notes being placed in the chart. Defaults to `[]`. + */ + var noteParamsToPlace:Array = []; + /** * The event type to use for events being placed in the chart. Defaults to `''`. */ @@ -1401,7 +1423,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function get_currentSongNoteStyle():String { - if (currentSongMetadata.playData.noteStyle == null) + if (currentSongMetadata.playData.noteStyle == null + || currentSongMetadata.playData.noteStyle == '' + || currentSongMetadata.playData.noteStyle == 'item') { // Initialize to the default value if not set. currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE; @@ -2436,7 +2460,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState gridGhostNote = new ChartEditorNoteSprite(this); gridGhostNote.alpha = 0.6; - gridGhostNote.noteData = new SongNoteData(0, 0, 0, ""); + gridGhostNote.noteData = new SongNoteData(0, 0, 0, "", []); gridGhostNote.visible = false; add(gridGhostNote); gridGhostNote.zIndex = 11; @@ -3303,7 +3327,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState handleTestKeybinds(); handleHelpKeybinds(); - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS handleQuickWatch(); #end @@ -3584,6 +3608,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // The note sprite handles animation playback and positioning. noteSprite.noteData = noteData; + noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle; noteSprite.overrideStepTime = null; noteSprite.overrideData = null; @@ -3607,6 +3632,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState holdNoteSprite.setHeightDirectly(noteLengthPixels); + holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle; + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height); @@ -3669,9 +3696,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState holdNoteSprite.noteData = noteData; holdNoteSprite.noteDirection = noteData.getDirection(); - holdNoteSprite.setHeightDirectly(noteLengthPixels); + holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle; + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); displayedHoldNoteData.push(noteData); @@ -4569,7 +4597,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState gridGhostHoldNote.noteData = currentPlaceNoteData; gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); - + gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle; gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); } else @@ -4726,7 +4754,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { // Create a note and place it in the chart. - var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace); + var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace, + ChartEditorState.cloneNoteParams(noteParamsToPlace)); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); @@ -4885,12 +4914,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()"; - var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace); + var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace, + ChartEditorState.cloneNoteParams(noteParamsToPlace)); - if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind) + if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind || noteParamsToPlace != noteData.params) { noteData.kind = noteKindToPlace; + noteData.params = noteParamsToPlace; noteData.data = cursorColumn; + gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle; gridGhostNote.playNoteAnimation(); } noteData.time = cursorSnappedMs; @@ -5129,46 +5161,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function handlePlayhead():Void { // Place notes at the playhead with the keyboard. - switch (currentLiveInputStyle) + for (note => key in LIVE_INPUT_KEYS[currentLiveInputStyle]) { - case ChartEditorLiveInputStyle.WASDKeys: - if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); - if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4); - if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5); - if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5); - if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6); - if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6); - if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7); - if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7); - - if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0); - if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0); - if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1); - if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1); - if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2); - if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2); - if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3); - if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3); - case ChartEditorLiveInputStyle.NumberKeys: - // Flipped because Dad is on the left but represents data 0-3. - if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4); - if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4); - if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5); - if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5); - if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6); - if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6); - if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7); - if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7); - - if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0); - if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0); - if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1); - if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2); - if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2); - if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3); - if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3); - case ChartEditorLiveInputStyle.None: - // Do nothing. + if (FlxG.keys.checkStatus(key, JUST_PRESSED)) placeNoteAtPlayhead(note) + else if (FlxG.keys.checkStatus(key, JUST_RELEASED)) finishPlaceNoteAtPlayhead(note); } // Place events at playhead. @@ -5196,7 +5192,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (notesAtPos.length == 0 && !removeNoteInstead) { trace('Placing note. ${column}'); - var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace); + var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace, ChartEditorState.cloneNoteParams(noteParamsToPlace)); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); currentLiveInputPlaceNoteData[column] = newNoteData; } @@ -5282,6 +5278,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState ghostHold.visible = true; ghostHold.alpha = 0.6; ghostHold.setHeightDirectly(0); + ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle; ghostHold.updateHoldNotePosition(renderedHoldNotes); } @@ -5648,6 +5645,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0); FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace); + FlxG.watch.addQuick('noteParamsToPlace', noteParamsToPlace); FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace); FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); @@ -5701,13 +5699,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // TODO: Rework asset system so we can remove this jank. switch (currentSongStage) { - case 'mainStage': + case 'mainStage' | 'mainStageErect': PlayStatePlaylist.campaignId = 'week1'; - case 'spookyMansion': + case 'spookyMansion' | 'spookyMansionErect': PlayStatePlaylist.campaignId = 'week2'; - case 'phillyTrain': + case 'phillyTrain' | 'phillyTrainErect': PlayStatePlaylist.campaignId = 'week3'; - case 'limoRide': + case 'limoRide' | 'limoRideErect': PlayStatePlaylist.campaignId = 'week4'; case 'mallXmas' | 'mallEvil': PlayStatePlaylist.campaignId = 'week5'; @@ -6511,6 +6509,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } return input; } + + public static function cloneNoteParams(paramsToClone:Array):Array + { + var params:Array = []; + for (param in paramsToClone) + { + params.push(param.clone()); + } + return params; + } } /** diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index ded48abe3..ff8446c49 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -2,6 +2,7 @@ package funkin.ui.debug.charting.components; import funkin.play.notes.Strumline; import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.notes.notestyle.NoteStyle; import flixel.FlxObject; import flixel.FlxSprite; import flixel.graphics.frames.FlxFramesCollection; @@ -15,6 +16,7 @@ import flixel.math.FlxMath; * A sprite that can be used to display the trail of a hold note in a chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ +@:access(funkin.ui.debug.charting.ChartEditorState) @:nullSafety class ChartEditorHoldNoteSprite extends SustainTrail { @@ -23,6 +25,22 @@ class ChartEditorHoldNoteSprite extends SustainTrail */ public var parentState:ChartEditorState; + @:isVar + public var noteStyle(get, set):Null; + + function get_noteStyle():Null + { + return this.noteStyle ?? this.parentState.currentSongNoteStyle; + } + + @:nullSafety(Off) + function set_noteStyle(value:Null):Null + { + this.noteStyle = value; + this.updateHoldNoteGraphic(); + return value; + } + public function new(parent:ChartEditorState) { var noteStyle = NoteStyleRegistry.instance.fetchDefault(); @@ -30,14 +48,52 @@ class ChartEditorHoldNoteSprite extends SustainTrail super(0, 100, noteStyle); this.parentState = parent; + } + + @:nullSafety(Off) + function updateHoldNoteGraphic():Void + { + var bruhStyle:Null = NoteStyleRegistry.instance.fetchEntry(noteStyle); + if (bruhStyle == null) bruhStyle = NoteStyleRegistry.instance.fetchDefault(); + setupHoldNoteGraphic(bruhStyle); + } + + override function setupHoldNoteGraphic(noteStyle:NoteStyle):Void + { + var graphicPath = noteStyle.getHoldNoteAssetPath(); + if (graphicPath == null) return; + loadGraphic(graphicPath); + + antialiasing = true; + + this.isPixel = noteStyle.isHoldNotePixel(); + if (isPixel) + { + endOffset = bottomClip = 1; + antialiasing = false; + } + else + { + endOffset = 0.5; + bottomClip = 0.9; + } zoom = 1.0; zoom *= noteStyle.fetchHoldNoteScale(); zoom *= 0.7; zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE; + graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2 + graphicHeight = sustainLength * 0.45; // sustainHeight + flipY = false; + alpha = 1.0; + + updateColorTransform(); + + updateClipping(); + setup(); } diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx index 98f5a47aa..c8f40da62 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx @@ -7,7 +7,11 @@ import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxFrame; import flixel.graphics.frames.FlxTileFrames; import flixel.math.FlxPoint; +import funkin.data.animation.AnimationData; import funkin.data.song.SongData.SongNoteData; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.notes.notestyle.NoteStyle; +import funkin.play.notes.NoteDirection; /** * A sprite that can be used to display a note in a chart. @@ -36,7 +40,8 @@ class ChartEditorNoteSprite extends FlxSprite /** * The name of the note style currently in use. */ - public var noteStyle(get, never):String; + @:isVar + public var noteStyle(get, set):Null; public var overrideStepTime(default, set):Null = null; @@ -66,72 +71,80 @@ class ChartEditorNoteSprite extends FlxSprite this.parentState = parent; + var entries:Array = NoteStyleRegistry.instance.listEntryIds(); + if (noteFrameCollection == null) { - initFrameCollection(); + buildEmptyFrameCollection(); + + for (entry in entries) + { + addNoteStyleFrames(fetchNoteStyle(entry)); + } } if (noteFrameCollection == null) throw 'ERROR: Could not initialize note sprite animations.'; this.frames = noteFrameCollection; - // Initialize all the animations, not just the one we're going to use immediately, - // so that later we can reuse the sprite without having to initialize more animations during scrolling. - this.animation.addByPrefix('tapLeftFunkin', 'purple instance'); - this.animation.addByPrefix('tapDownFunkin', 'blue instance'); - this.animation.addByPrefix('tapUpFunkin', 'green instance'); - this.animation.addByPrefix('tapRightFunkin', 'red instance'); - - this.animation.addByPrefix('holdLeftFunkin', 'LeftHoldPiece'); - this.animation.addByPrefix('holdDownFunkin', 'DownHoldPiece'); - this.animation.addByPrefix('holdUpFunkin', 'UpHoldPiece'); - this.animation.addByPrefix('holdRightFunkin', 'RightHoldPiece'); - - this.animation.addByPrefix('holdEndLeftFunkin', 'LeftHoldEnd'); - this.animation.addByPrefix('holdEndDownFunkin', 'DownHoldEnd'); - this.animation.addByPrefix('holdEndUpFunkin', 'UpHoldEnd'); - this.animation.addByPrefix('holdEndRightFunkin', 'RightHoldEnd'); - - this.animation.addByPrefix('tapLeftPixel', 'pixel4'); - this.animation.addByPrefix('tapDownPixel', 'pixel5'); - this.animation.addByPrefix('tapUpPixel', 'pixel6'); - this.animation.addByPrefix('tapRightPixel', 'pixel7'); + for (entry in entries) + { + addNoteStyleAnimations(fetchNoteStyle(entry)); + } } static var noteFrameCollection:Null = null; - /** - * We load all the note frames once, then reuse them. - */ - static function initFrameCollection():Void + function fetchNoteStyle(noteStyleId:String):NoteStyle { - buildEmptyFrameCollection(); - if (noteFrameCollection == null) return; + var result = NoteStyleRegistry.instance.fetchEntry(noteStyleId); + if (result != null) return result; + return NoteStyleRegistry.instance.fetchDefault(); + } - // TODO: Automatically iterate over the list of note skins. + @:access(funkin.play.notes.notestyle.NoteStyle) + @:nullSafety(Off) + static function addNoteStyleFrames(noteStyle:NoteStyle):Void + { + var prefix:String = noteStyle.id.toTitleCase(); - // Normal notes - var frameCollectionNormal:FlxAtlasFrames = Paths.getSparrowAtlas('NOTE_assets'); - - for (frame in frameCollectionNormal.frames) + var frameCollection:FlxAtlasFrames = Paths.getSparrowAtlas(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary()); + if (frameCollection == null) { - noteFrameCollection.pushFrame(frame); + trace('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}'); + FlxG.log.error('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}'); + return; } - - // Pixel notes - var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null); - if (graphicPixel == null) trace('ERROR: Could not load graphic: ' + Paths.image('weeb/pixelUI/arrows-pixels', 'week6')); - var frameCollectionPixel = FlxTileFrames.fromGraphic(graphicPixel, new FlxPoint(17, 17)); - for (i in 0...frameCollectionPixel.frames.length) + for (frame in frameCollection.frames) { - var frame:Null = frameCollectionPixel.frames[i]; - if (frame == null) continue; - - frame.name = 'pixel' + i; - noteFrameCollection.pushFrame(frame); + // cloning the frame because else + // we will fuck up the frame data used in game + var clonedFrame:FlxFrame = frame.copyTo(); + clonedFrame.name = '$prefix${clonedFrame.name}'; + noteFrameCollection.pushFrame(clonedFrame); } } + @:access(funkin.play.notes.notestyle.NoteStyle) + @:nullSafety(Off) + function addNoteStyleAnimations(noteStyle:NoteStyle):Void + { + var prefix:String = noteStyle.id.toTitleCase(); + var suffix:String = noteStyle.id.toTitleCase(); + + var leftData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.LEFT); + this.animation.addByPrefix('tapLeft$suffix', '$prefix${leftData.prefix}', leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY); + + var downData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.DOWN); + this.animation.addByPrefix('tapDown$suffix', '$prefix${downData.prefix}', downData.frameRate, downData.looped, downData.flipX, downData.flipY); + + var upData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.UP); + this.animation.addByPrefix('tapUp$suffix', '$prefix${upData.prefix}', upData.frameRate, upData.looped, upData.flipX, upData.flipY); + + var rightData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.RIGHT); + this.animation.addByPrefix('tapRight$suffix', '$prefix${rightData.prefix}', rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY); + } + @:nullSafety(Off) static function buildEmptyFrameCollection():Void { @@ -185,12 +198,24 @@ class ChartEditorNoteSprite extends FlxSprite } } - function get_noteStyle():String + function get_noteStyle():Null { - // Fall back to Funkin' if it's not a valid note style. - return if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle)) this.parentState.currentSongNoteStyle else 'funkin'; + if (this.noteStyle == null) + { + var result = this.parentState.currentSongNoteStyle; + return result; + } + return this.noteStyle; } + function set_noteStyle(value:Null):Null + { + this.noteStyle = value; + this.playNoteAnimation(); + return value; + } + + @:nullSafety(Off) public function playNoteAnimation():Void { if (this.noteData == null) return; @@ -200,6 +225,7 @@ class ChartEditorNoteSprite extends FlxSprite // Play the appropriate animation for the type, direction, and skin. var dirName:String = overrideData != null ? SongNoteData.buildDirectionName(overrideData) : this.noteData.getDirectionName(); + var noteStyleSuffix:String = this.noteStyle?.toTitleCase() ?? Constants.DEFAULT_NOTE_STYLE.toTitleCase(); var animationName:String = '${baseAnimationName}${dirName}${this.noteStyle.toTitleCase()}'; this.animation.play(animationName); @@ -209,12 +235,12 @@ class ChartEditorNoteSprite extends FlxSprite switch (baseAnimationName) { case 'tap': - this.setGraphicSize(0, ChartEditorState.GRID_SIZE); + this.setGraphicSize(ChartEditorState.GRID_SIZE, 0); + this.updateHitbox(); } - this.updateHitbox(); - // TODO: Make this an attribute of the note skin. - this.antialiasing = (this.parentState.currentSongNoteStyle != 'Pixel'); + var bruhStyle:NoteStyle = fetchNoteStyle(this.noteStyle); + this.antialiasing = !bruhStyle._data?.assets?.note?.isPixel ?? true; } /** diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx index 8f021840a..70580300e 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx @@ -190,8 +190,8 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox var numberStepper:NumberStepper = new NumberStepper(); numberStepper.id = field.name; numberStepper.step = field.step ?? 1.0; - numberStepper.min = field.min ?? 0.0; - numberStepper.max = field.max ?? 10.0; + if (field.min != null) numberStepper.min = field.min; + if (field.min != null) numberStepper.max = field.max; if (field.defaultValue != null) numberStepper.value = field.defaultValue; input = numberStepper; case FLOAT: diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx index d4fc69fc1..100654a02 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx @@ -2,8 +2,16 @@ package funkin.ui.debug.charting.toolboxes; import haxe.ui.components.DropDown; import haxe.ui.components.TextField; +import haxe.ui.components.Label; +import haxe.ui.components.NumberStepper; +import haxe.ui.containers.Grid; +import haxe.ui.core.Component; import haxe.ui.events.UIEvent; import funkin.ui.debug.charting.util.ChartEditorDropdowns; +import funkin.play.notes.notekind.NoteKindManager; +import funkin.play.notes.notekind.NoteKind.NoteKindParam; +import funkin.play.notes.notekind.NoteKind.NoteKindParamType; +import funkin.data.song.SongData.NoteParamData; /** * The toolbox which allows modifying information like Note Kind. @@ -12,8 +20,22 @@ import funkin.ui.debug.charting.util.ChartEditorDropdowns; @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml")) class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox { + // 100 is the height used in note-data.xml + static final DIALOG_HEIGHT:Int = 100; + + // toolboxNotesGrid.height + 45 + // this is what i found out by printing this.height and grid.height + // and then seeing that this.height is 100 and grid.height is 55 + static final HEIGHT_OFFSET:Int = 45; + + // minimizing creates a gray bar the bottom, which would obscure the components, + // which is why we use an extra offset of 20 + static final MINIMIZE_FIX:Int = 20; + + var toolboxNotesGrid:Grid; var toolboxNotesNoteKind:DropDown; var toolboxNotesCustomKind:TextField; + var toolboxNotesParams:Array = []; var _initializing:Bool = true; @@ -54,12 +76,35 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace; } + createNoteKindParams(noteKind); + if (!_initializing && chartEditorState.currentNoteSelection.length > 0) { - // Edit the note data of any selected notes. for (note in chartEditorState.currentNoteSelection) { + // Edit the note data of any selected notes. note.kind = chartEditorState.noteKindToPlace; + note.params = ChartEditorState.cloneNoteParams(chartEditorState.noteParamsToPlace); + + // update note sprites + for (noteSprite in chartEditorState.renderedNotes.members) + { + if (noteSprite.noteData == note) + { + noteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle; + break; + } + } + + // update hold note sprites + for (holdNoteSprite in chartEditorState.renderedHoldNotes.members) + { + if (holdNoteSprite.noteData == note) + { + holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle; + break; + } + } } chartEditorState.saveDataDirty = true; chartEditorState.noteDisplayDirty = true; @@ -94,6 +139,8 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox toolboxNotesNoteKind.value = ChartEditorDropdowns.lookupNoteKind(chartEditorState.noteKindToPlace); toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace; + + createNoteKindParams(chartEditorState.noteKindToPlace); } function showCustom():Void @@ -108,8 +155,149 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox toolboxNotesCustomKind.hidden = true; } + function createNoteKindParams(noteKind:Null):Void + { + clearNoteKindParams(); + + var setParamsToPlace:Bool = false; + if (!_initializing) + { + for (note in chartEditorState.currentNoteSelection) + { + if (note.kind == chartEditorState.noteKindToPlace) + { + chartEditorState.noteParamsToPlace = ChartEditorState.cloneNoteParams(note.params); + setParamsToPlace = true; + break; + } + } + } + + var noteKindParams:Array = NoteKindManager.getParams(noteKind); + + for (i in 0...noteKindParams.length) + { + var param:NoteKindParam = noteKindParams[i]; + + var paramLabel:Label = new Label(); + paramLabel.value = param.description; + paramLabel.verticalAlign = "center"; + paramLabel.horizontalAlign = "right"; + + var paramComponent:Component = null; + + switch (param.type) + { + case NoteKindParamType.INT | NoteKindParamType.FLOAT: + var paramStepper:NumberStepper = new NumberStepper(); + paramStepper.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? 0.0; + paramStepper.percentWidth = 100; + paramStepper.step = param.data?.step ?? 1.0; + + // this check should be unnecessary but for some reason + // even when these are null it will set it to 0 + if (param.data?.min != null) + { + paramStepper.min = param.data.min; + } + if (param.data?.max != null) + { + paramStepper.max = param.data.max; + } + if (param.data?.precision != null) + { + paramStepper.precision = param.data.precision; + } + paramComponent = paramStepper; + + case NoteKindParamType.STRING: + var paramTextField:TextField = new TextField(); + paramTextField.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? ''; + paramTextField.percentWidth = 100; + paramComponent = paramTextField; + } + + if (paramComponent == null) + { + continue; + } + + paramComponent.onChange = function(event:UIEvent) { + chartEditorState.noteParamsToPlace[i].value = paramComponent.value; + + for (note in chartEditorState.currentNoteSelection) + { + if (note.params.length != noteKindParams.length) + { + break; + } + + if (note.params[i].name == param.name) + { + note.params[i].value = paramComponent.value; + } + } + } + + addNoteKindParam(paramLabel, paramComponent); + } + + if (!setParamsToPlace) + { + var noteParamData:Array = []; + for (i in 0...noteKindParams.length) + { + noteParamData.push(new NoteParamData(noteKindParams[i].name, toolboxNotesParams[i].component.value)); + } + chartEditorState.noteParamsToPlace = noteParamData; + } + } + + function addNoteKindParam(label:Label, component:Component):Void + { + toolboxNotesParams.push({label: label, component: component}); + toolboxNotesGrid.addComponent(label); + toolboxNotesGrid.addComponent(component); + + this.height = Math.max(DIALOG_HEIGHT, DIALOG_HEIGHT - 30 + toolboxNotesParams.length * 30); + } + + function clearNoteKindParams():Void + { + for (param in toolboxNotesParams) + { + toolboxNotesGrid.removeComponent(param.component); + toolboxNotesGrid.removeComponent(param.label); + } + toolboxNotesParams = []; + this.height = DIALOG_HEIGHT; + } + + override function update(elapsed:Float):Void + { + super.update(elapsed); + + // current dialog is minimized, dont change the height + if (this.minimized) + { + return; + } + + var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, (toolboxNotesGrid?.height ?? 50.0) + HEIGHT_OFFSET)) + MINIMIZE_FIX; + if (this.height != heightToSet) + { + this.height = heightToSet; + } + } + public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox { return new ChartEditorNoteDataToolbox(chartEditorState); } } + +typedef ToolboxNoteKindParam = +{ + var label:Label; + var component:Component; +} diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx index 55aab0ab0..21938b005 100644 --- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx +++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx @@ -135,6 +135,14 @@ class ChartEditorDropdowns var noteStyle:Null = NoteStyleRegistry.instance.fetchEntry(noteStyleId); if (noteStyle == null) continue; + // check if the note style has all necessary assets (strums, notes, holdNotes) + if (noteStyle._data?.assets?.noteStrumline == null + || noteStyle._data?.assets?.note == null + || noteStyle._data?.assets?.holdNote == null) + { + continue; + } + var value = {id: noteStyleId, text: noteStyle.getName()}; if (startingStyleId == noteStyleId) returnValue = value; @@ -146,7 +154,7 @@ class ChartEditorDropdowns return returnValue; } - static final NOTE_KINDS:Map = [ + public static final NOTE_KINDS:Map = [ // Base "" => "Default", "~CUSTOM~" => "Custom", @@ -187,11 +195,11 @@ class ChartEditorDropdowns { dropDown.dataSource.clear(); - var returnValue:DropDownEntry = lookupNoteKind('~CUSTOM'); + var returnValue:DropDownEntry = lookupNoteKind(''); for (noteKindId in NOTE_KINDS.keys()) { - var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Default'; + var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Unknown'; var value:DropDownEntry = {id: noteKindId, text: noteKind}; if (startingKindId == noteKindId) returnValue = value; @@ -208,7 +216,7 @@ class ChartEditorDropdowns { if (noteKindId == null) return lookupNoteKind(''); if (!NOTE_KINDS.exists(noteKindId)) return {id: '~CUSTOM~', text: 'Custom'}; - return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Default'}; + return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Unknown'}; } /** diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index 49c588722..36dba0054 100644 --- a/source/funkin/ui/freeplay/AlbumRoll.hx +++ b/source/funkin/ui/freeplay/AlbumRoll.hx @@ -37,6 +37,7 @@ class AlbumRoll extends FlxSpriteGroup } var newAlbumArt:FlxAtlasSprite; + var albumTitle:FunkinSprite; var difficultyStars:DifficultyStars; var _exitMovers:Null; @@ -59,24 +60,27 @@ class AlbumRoll extends FlxSpriteGroup { super(); - newAlbumArt = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum")); + newAlbumArt = new FlxAtlasSprite(640, 350, Paths.animateAtlas("freeplay/albumRoll/freeplayAlbum")); newAlbumArt.visible = false; - newAlbumArt.onAnimationFinish.add(onAlbumFinish); + newAlbumArt.onAnimationComplete.add(onAlbumFinish); add(newAlbumArt); difficultyStars = new DifficultyStars(140, 39); difficultyStars.visible = false; add(difficultyStars); + + buildAlbumTitle("freeplay/albumRoll/volume1-text"); + albumTitle.visible = false; } function onAlbumFinish(animName:String):Void { // Play the idle animation for the current album. - newAlbumArt.playAnimation(animNames.get('$albumId-idle'), false, false, true); - - // End on the last frame and don't continue until playAnimation is called again. - // newAlbumArt.anim.pause(); + if (animName != "idle") + { + // newAlbumArt.playAnimation('idle', true); + } } /** @@ -104,6 +108,12 @@ class AlbumRoll extends FlxSpriteGroup return; }; + // Update the album art. + var albumGraphic = Paths.image(albumData.getAlbumArtAssetKey()); + newAlbumArt.replaceFrameGraphic(0, albumGraphic); + + buildAlbumTitle(albumData.getAlbumTitleAssetKey()); + applyExitMovers(); refresh(); @@ -146,19 +156,57 @@ class AlbumRoll extends FlxSpriteGroup */ public function playIntro():Void { + albumTitle.visible = false; newAlbumArt.visible = true; - newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false); + newAlbumArt.playAnimation('intro', true); difficultyStars.visible = false; new FlxTimer().start(0.75, function(_) { - // showTitle(); + showTitle(); showStars(); + albumTitle.animation.play('switch'); }); } public function skipIntro():Void { - newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false); + // Weird workaround + newAlbumArt.playAnimation('switch', true); + albumTitle.animation.play('switch'); + } + + public function showTitle():Void + { + albumTitle.visible = true; + } + + public function buildAlbumTitle(assetKey:String):Void + { + if (albumTitle != null) + { + remove(albumTitle); + albumTitle = null; + } + + albumTitle = FunkinSprite.createSparrow(925, 500, assetKey); + albumTitle.visible = albumTitle.frames != null && newAlbumArt.visible; + albumTitle.animation.addByPrefix('idle', 'idle0', 24, true); + albumTitle.animation.addByPrefix('switch', 'switch0', 24, false); + add(albumTitle); + + albumTitle.animation.finishCallback = (function(name) { + if (name == 'switch') albumTitle.animation.play('idle'); + }); + albumTitle.animation.play('idle'); + + albumTitle.zIndex = 1000; + + if (_exitMovers != null) _exitMovers.set([albumTitle], + { + x: FlxG.width, + speed: 0.4, + wait: 0 + }); } public function setDifficultyStars(?difficulty:Int):Void diff --git a/source/funkin/ui/freeplay/FreeplayDJ.hx b/source/funkin/ui/freeplay/FreeplayDJ.hx index 72eddd0ca..b1528d906 100644 --- a/source/funkin/ui/freeplay/FreeplayDJ.hx +++ b/source/funkin/ui/freeplay/FreeplayDJ.hx @@ -15,7 +15,7 @@ class FreeplayDJ extends FlxAtlasSprite { // Represents the sprite's current status. // Without state machines I would have driven myself crazy years ago. - public var currentState:DJBoyfriendState = Intro; + public var currentState:FreeplayDJState = Intro; // A callback activated when the intro animation finishes. public var onIntroDone:FlxSignal = new FlxSignal(); @@ -43,7 +43,7 @@ class FreeplayDJ extends FlxAtlasSprite super(x, y, playableCharData.getAtlasPath()); - anim.callback = function(name, number) { + onAnimationFrame.add(function(name, number) { if (name == playableCharData.getAnimationPrefix('cartoon')) { if (number == playableCharData.getCartoonSoundClickFrame()) @@ -55,12 +55,12 @@ class FreeplayDJ extends FlxAtlasSprite runTvLogic(); } } - }; + }); FlxG.debugger.track(this); FlxG.console.registerObject("dj", this); - anim.onComplete = onFinishAnim; + onAnimationComplete.add(onFinishAnim); FlxG.console.registerFunction("freeplayCartoon", function() { currentState = Cartoon; @@ -96,10 +96,10 @@ class FreeplayDJ extends FlxAtlasSprite var animPrefix = playableCharData.getAnimationPrefix('idle'); if (getCurrentAnimation() != animPrefix) { - playFlashAnimation(animPrefix, true); + playFlashAnimation(animPrefix, true, false, true); } - if (getCurrentAnimation() == animPrefix && this.isLoopFinished()) + if (getCurrentAnimation() == animPrefix && this.isLoopComplete()) { if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg) { @@ -111,18 +111,69 @@ class FreeplayDJ extends FlxAtlasSprite } } timeIdling += elapsed; + case NewUnlock: + var animPrefix = playableCharData.getAnimationPrefix('newUnlock'); + if (!hasAnimation(animPrefix)) + { + currentState = Idle; + } + if (getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true, false, true); + } case Confirm: var animPrefix = playableCharData.getAnimationPrefix('confirm'); if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false); timeIdling = 0; case FistPumpIntro: - var animPrefix = playableCharData.getAnimationPrefix('fistPump'); - if (getCurrentAnimation() != animPrefix) playFlashAnimation('Boyfriend DJ fist pump', false); - if (getCurrentAnimation() == animPrefix && anim.curFrame >= 4) + var animPrefixA = playableCharData.getAnimationPrefix('fistPump'); + var animPrefixB = playableCharData.getAnimationPrefix('loss'); + + if (getCurrentAnimation() == animPrefixA) { - anim.play("Boyfriend DJ fist pump", true, false, 0); + var endFrame = playableCharData.getFistPumpIntroEndFrame(); + if (endFrame > -1 && anim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixA, true, false, false, playableCharData.getFistPumpIntroStartFrame()); + } } + else if (getCurrentAnimation() == animPrefixB) + { + var endFrame = playableCharData.getFistPumpIntroBadEndFrame(); + if (endFrame > -1 && anim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixB, true, false, false, playableCharData.getFistPumpIntroBadStartFrame()); + } + } + else + { + FlxG.log.warn("Unrecognized animation in FistPumpIntro: " + getCurrentAnimation()); + } + case FistPump: + var animPrefixA = playableCharData.getAnimationPrefix('fistPump'); + var animPrefixB = playableCharData.getAnimationPrefix('loss'); + + if (getCurrentAnimation() == animPrefixA) + { + var endFrame = playableCharData.getFistPumpLoopEndFrame(); + if (endFrame > -1 && anim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixA, true, false, false, playableCharData.getFistPumpLoopStartFrame()); + } + } + else if (getCurrentAnimation() == animPrefixB) + { + var endFrame = playableCharData.getFistPumpLoopBadEndFrame(); + if (endFrame > -1 && anim.curFrame >= endFrame) + { + playFlashAnimation(animPrefixB, true, false, false, playableCharData.getFistPumpLoopBadStartFrame()); + } + } + else + { + FlxG.log.warn("Unrecognized animation in FistPump: " + getCurrentAnimation()); + } case IdleEasterEgg: var animPrefix = playableCharData.getAnimationPrefix('idleEasterEgg'); @@ -135,9 +186,12 @@ class FreeplayDJ extends FlxAtlasSprite timeIdling = 0; case Cartoon: var animPrefix = playableCharData.getAnimationPrefix('cartoon'); - if (animPrefix == null) { + if (animPrefix == null) + { currentState = IdleEasterEgg; - } else { + } + else + { if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); timeIdling = 0; } @@ -145,6 +199,7 @@ class FreeplayDJ extends FlxAtlasSprite // I shit myself. } + #if FEATURE_DEBUG_FUNCTIONS if (FlxG.keys.pressed.CONTROL) { if (FlxG.keys.justPressed.LEFT) @@ -167,20 +222,28 @@ class FreeplayDJ extends FlxAtlasSprite this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); } - if (FlxG.keys.justPressed.SPACE) + if (FlxG.keys.justPressed.C) { currentState = (currentState == Idle ? Cartoon : Idle); } } + #end } - function onFinishAnim():Void + function onFinishAnim(name:String):Void { - var name = anim.curSymbol.name; + // var name = anim.curSymbol.name; if (name == playableCharData.getAnimationPrefix('intro')) { - currentState = Idle; + if (PlayerRegistry.instance.hasNewCharacter()) + { + currentState = NewUnlock; + } + else + { + currentState = Idle; + } onIntroDone.dispatch(); } else if (name == playableCharData.getAnimationPrefix('idle')) @@ -220,9 +283,17 @@ class FreeplayDJ extends FlxAtlasSprite // runTvLogic(); } trace('Replay idle: ${frame}'); - anim.play(playableCharData.getAnimationPrefix('cartoon'), true, false, frame); + playFlashAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, frame); // trace('Finished confirm'); } + else if (name == playableCharData.getAnimationPrefix('newUnlock')) + { + // Animation should loop. + } + else if (name == playableCharData.getAnimationPrefix('charSelect')) + { + onCharSelectComplete(); + } else { trace('Finished ${name}'); @@ -235,6 +306,15 @@ class FreeplayDJ extends FlxAtlasSprite seenIdleEasterEgg = false; } + /** + * Dynamic function, it's actually a variable you can reassign! + * `dj.onCharSelectComplete = function() {};` + */ + public dynamic function onCharSelectComplete():Void + { + trace('onCharSelectComplete()'); + } + var offsetX:Float = 0.0; var offsetY:Float = 0.0; @@ -266,7 +346,7 @@ class FreeplayDJ extends FlxAtlasSprite function loadCartoon() { cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() { - anim.play("Boyfriend DJ watchin tv OG", true, false, 60); + playFlashAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, 60); }); // Fade out music to 40% volume over 1 second. @@ -296,21 +376,48 @@ class FreeplayDJ extends FlxAtlasSprite currentState = Confirm; } - public function fistPump():Void + public function toCharSelect():Void + { + if (hasAnimation('charSelect')) + { + currentState = CharSelect; + var animPrefix = playableCharData.getAnimationPrefix('charSelect'); + playFlashAnimation(animPrefix, true, false, false, 0); + } + else + { + currentState = Confirm; + // Call this immediately; otherwise, we get locked out of Character Select. + onCharSelectComplete(); + } + } + + public function fistPumpIntro():Void { currentState = FistPumpIntro; + var animPrefix = playableCharData.getAnimationPrefix('fistPump'); + playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpIntroStartFrame()); } - public function pumpFist():Void + public function fistPump():Void { currentState = FistPump; - anim.play("Boyfriend DJ fist pump", true, false, 4); + var animPrefix = playableCharData.getAnimationPrefix('fistPump'); + playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpLoopStartFrame()); } - public function pumpFistBad():Void + public function fistPumpLossIntro():Void + { + currentState = FistPumpIntro; + var animPrefix = playableCharData.getAnimationPrefix('loss'); + playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpIntroBadStartFrame()); + } + + public function fistPumpLoss():Void { currentState = FistPump; - anim.play("Boyfriend DJ loss reaction 1", true, false, 4); + var animPrefix = playableCharData.getAnimationPrefix('loss'); + playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpLoopBadStartFrame()); } override public function getCurrentAnimation():String @@ -319,9 +426,9 @@ class FreeplayDJ extends FlxAtlasSprite return this.anim.curSymbol.name; } - public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void + public function playFlashAnimation(id:String, Force:Bool = false, Reverse:Bool = false, Loop:Bool = false, Frame:Int = 0):Void { - anim.play(id, Force, Reverse, Frame); + playAnimation(id, Force, Reverse, Loop, Frame); applyAnimOffset(); } @@ -361,13 +468,53 @@ class FreeplayDJ extends FlxAtlasSprite } } -enum DJBoyfriendState +enum FreeplayDJState { + /** + * Character enters the frame and transitions to Idle. + */ Intro; + + /** + * Character loops in idle. + */ Idle; - Confirm; - FistPumpIntro; - FistPump; + + /** + * Plays an easter egg animation after a period in Idle, then reverts to Idle. + */ IdleEasterEgg; + + /** + * Plays an elaborate easter egg animation. Does not revert until another animation is triggered. + */ Cartoon; + + /** + * Player has selected a song. + */ + Confirm; + + /** + * Character preps to play the fist pump animation; plays after the Results screen. + * The actual frame label that gets played may vary based on the player's success. + */ + FistPumpIntro; + + /** + * Character plays the fist pump animation. + * The actual frame label that gets played may vary based on the player's success. + */ + FistPump; + + /** + * Plays an animation to indicate that the player has a new unlock in Character Select. + * Overrides all idle animations as well as the fist pump. Only Confirm and CharSelect will override this. + */ + NewUnlock; + + /** + * Plays an animation to transition to the Character Select screen. + */ + CharSelect; } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 416e79df6..19367a190 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1,7 +1,6 @@ package funkin.ui.freeplay; import flixel.addons.transition.FlxTransitionableState; -import flixel.addons.ui.FlxInputText; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.group.FlxGroup; @@ -307,14 +306,14 @@ class FreeplayState extends MusicBeatSubState stickerSubState.degenStickers(); } - #if discord_rpc + #if FEATURE_DISCORD_RPC // Updating Discord Rich Presence DiscordClient.changePresence('In the Menus', null); #end var isDebug:Bool = false; - #if debug + #if FEATURE_DEBUG_FUNCTIONS isDebug = true; #end @@ -354,7 +353,7 @@ class FreeplayState extends MusicBeatSubState // Only display songs which actually have available difficulties for the current character. var displayedVariations = song.getVariationsByCharacter(currentCharacter); trace('Displayed Variations (${songId}): $displayedVariations'); - var availableDifficultiesForSong:Array = song.listDifficulties(displayedVariations, false); + var availableDifficultiesForSong:Array = song.listSuffixedDifficulties(displayedVariations, false, false); trace('Available Difficulties: $availableDifficultiesForSong'); if (availableDifficultiesForSong.length == 0) continue; @@ -645,8 +644,8 @@ class FreeplayState extends MusicBeatSubState speed: 0.3 }); - var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls); - var diffSelRight:DifficultySelector = new DifficultySelector(325, grpDifficulties.y - 10, true, controls); + var diffSelLeft:DifficultySelector = new DifficultySelector(this, 20, grpDifficulties.y - 10, false, controls); + var diffSelRight:DifficultySelector = new DifficultySelector(this, 325, grpDifficulties.y - 10, true, controls); diffSelLeft.visible = false; diffSelRight.visible = false; add(diffSelLeft); @@ -884,7 +883,7 @@ class FreeplayState extends MusicBeatSubState return str.songName.toLowerCase().startsWith(songFilter.filterData ?? ''); }); case ALL: - // no filter! + // no filter! case FAVORITE: songsToFilter = songsToFilter.filter(str -> { if (str == null) return true; // Random @@ -914,7 +913,15 @@ class FreeplayState extends MusicBeatSubState changeSelection(); changeDiff(); - if (dj != null) dj.fistPump(); + if (fromResultsParams?.newRank == SHIT) + { + if (dj != null) dj.fistPumpLossIntro(); + } + else + { + if (dj != null) dj.fistPumpIntro(); + } + // rankCamera.fade(FlxColor.BLACK, 0.5, true); rankCamera.fade(0xFF000000, 0.5, true, null, true); if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; @@ -1096,11 +1103,11 @@ class FreeplayState extends MusicBeatSubState if (fromResultsParams?.newRank == SHIT) { - if (dj != null) dj.pumpFistBad(); + if (dj != null) dj.fistPumpLoss(); } else { - if (dj != null) dj.pumpFist(); + if (dj != null) dj.fistPump(); } rankCamera.zoom = 0.8; @@ -1132,7 +1139,7 @@ class FreeplayState extends MusicBeatSubState // NOW we can interact with the menu busy = false; - grpCapsules.members[curSelected].sparkle.alpha = 0.7; + capsule.sparkle.alpha = 0.7; playCurSongPreview(capsule); }, null); @@ -1203,7 +1210,7 @@ class FreeplayState extends MusicBeatSubState /** * If true, disable interaction with the interface. */ - var busy:Bool = false; + public var busy:Bool = false; var originalPos:FlxPoint = new FlxPoint(); @@ -1211,7 +1218,7 @@ class FreeplayState extends MusicBeatSubState { super.update(elapsed); - #if debug + #if FEATURE_DEBUG_FUNCTIONS if (FlxG.keys.justPressed.T) { rankAnimStart(fromResultsParams ?? @@ -1246,7 +1253,32 @@ class FreeplayState extends MusicBeatSubState if (controls.FREEPLAY_CHAR_SELECT && !busy) { - FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); + // Check if we have ACCESS to character select! + trace('Is Pico unlocked? ${PlayerRegistry.instance.fetchEntry('pico')?.isUnlocked()}'); + trace('Number of characters: ${PlayerRegistry.instance.countUnlockedCharacters()}'); + + if (PlayerRegistry.instance.countUnlockedCharacters() > 1) + { + if (dj != null) + { + busy = true; + // Transition to character select after animation + dj.onCharSelectComplete = function() { + FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); + } + dj.toCharSelect(); + } + else + { + // Transition to character select immediately + FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); + } + } + else + { + trace('Not enough characters unlocked to open character select!'); + FunkinSound.playOnce(Paths.sound('cancelMenu')); + } } if (controls.FREEPLAY_FAVORITE && !busy) @@ -1339,6 +1371,8 @@ class FreeplayState extends MusicBeatSubState } handleInputs(elapsed); + + if (dj != null) FlxG.watch.addQuick('dj-anim', dj.getCurrentAnimation()); } function handleInputs(elapsed:Float):Void @@ -1496,7 +1530,7 @@ class FreeplayState extends MusicBeatSubState generateSongList(currentFilter, true); } - if (controls.BACK) + if (controls.BACK && !busy) { busy = true; FlxTween.globalManager.clear(); @@ -1539,7 +1573,7 @@ class FreeplayState extends MusicBeatSubState var moveDataX = funnyMoveShit.x ?? spr.x; var moveDataY = funnyMoveShit.y ?? spr.y; var moveDataSpeed = funnyMoveShit.speed ?? 0.2; - var moveDataWait = funnyMoveShit.wait ?? 0; + var moveDataWait = funnyMoveShit.wait ?? 0.0; FlxTween.tween(spr, {x: moveDataX, y: moveDataY}, moveDataSpeed, {ease: FlxEase.expoIn}); @@ -1686,6 +1720,9 @@ class FreeplayState extends MusicBeatSubState songCapsule.init(null, null, null); } } + + // Reset the song preview in case we changed variations (normal->erect etc) + playCurSongPreview(); } // Set the album graphic and play the animation if relevant. @@ -1782,7 +1819,7 @@ class FreeplayState extends MusicBeatSubState var targetInstId:String = baseInstrumentalId; // TODO: Make this a UI element. - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL) { targetInstId = altInstrumentalIds[0]; @@ -1804,7 +1841,7 @@ class FreeplayState extends MusicBeatSubState confirmGlow.visible = true; confirmGlow2.visible = true; - backingTextYeah.anim.play("BF back card confirm raw", false, false, 0); + backingTextYeah.playAnimation("BF back card confirm raw", false, false, false, 0); confirmGlow2.alpha = 0; confirmGlow.alpha = 0; @@ -1843,7 +1880,7 @@ class FreeplayState extends MusicBeatSubState practiceMode: false, minimalMode: false, - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS botPlayMode: FlxG.keys.pressed.SHIFT, #else botPlayMode: false, @@ -1924,8 +1961,10 @@ class FreeplayState extends MusicBeatSubState } } - public function playCurSongPreview(daSongCapsule:SongMenuItem):Void + public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void { + if (daSongCapsule == null) daSongCapsule = grpCapsules.members[curSelected]; + if (curSelected == 0) { FunkinSound.playMusic('freeplayRandom', @@ -1950,7 +1989,7 @@ class FreeplayState extends MusicBeatSubState var instSuffix:String = baseInstrumentalId; // TODO: Make this a UI element. - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL) { instSuffix = altInstrumentalIds[0]; @@ -2008,10 +2047,13 @@ class DifficultySelector extends FlxSprite var controls:Controls; var whiteShader:PureColor; - public function new(x:Float, y:Float, flipped:Bool, controls:Controls) + var parent:FreeplayState; + + public function new(parent:FreeplayState, x:Float, y:Float, flipped:Bool, controls:Controls) { super(x, y); + this.parent = parent; this.controls = controls; frames = Paths.getSparrowAtlas('freeplay/freeplaySelector'); @@ -2027,8 +2069,8 @@ class DifficultySelector extends FlxSprite override function update(elapsed:Float):Void { - if (flipX && controls.UI_RIGHT_P) moveShitDown(); - if (!flipX && controls.UI_LEFT_P) moveShitDown(); + if (flipX && controls.UI_RIGHT_P && !parent.busy) moveShitDown(); + if (!flipX && controls.UI_LEFT_P && !parent.busy) moveShitDown(); super.update(elapsed); } @@ -2158,7 +2200,13 @@ class FreeplaySongData function updateValues(variations:Array):Void { this.songDifficulties = song.listDifficulties(null, variations, false, false); - if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY; + if (!this.songDifficulties.contains(currentDifficulty)) + { + currentDifficulty = Constants.DEFAULT_DIFFICULTY; + // This method gets called again by the setter-method + // or the difficulty didn't change, so there's no need to continue. + return; + } var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations); if (songDifficulty == null) return; @@ -2219,15 +2267,26 @@ class DifficultySprite extends FlxSprite difficultyId = diffId; - if (Assets.exists(Paths.file('images/freeplay/freeplay${diffId}.xml'))) + var assetDiffId:String = diffId; + while (!Assets.exists(Paths.image('freeplay/freeplay${assetDiffId}'))) { - this.frames = Paths.getSparrowAtlas('freeplay/freeplay${diffId}'); + // Remove the last suffix of the difficulty id until we find an asset or there are no more suffixes. + var assetDiffIdParts:Array = assetDiffId.split('-'); + assetDiffIdParts.pop(); + if (assetDiffIdParts.length == 0) break; + assetDiffId = assetDiffIdParts.join('-'); + } + + // Check for an XML to use an animation instead of an image. + if (Assets.exists(Paths.file('images/freeplay/freeplay${assetDiffId}.xml'))) + { + this.frames = Paths.getSparrowAtlas('freeplay/freeplay${assetDiffId}'); this.animation.addByPrefix('idle', 'idle0', 24, true); if (Preferences.flashingLights) this.animation.play('idle'); } else { - this.loadGraphic(Paths.image('freeplay/freeplay' + diffId)); + this.loadGraphic(Paths.image('freeplay/freeplay' + assetDiffId)); } } } diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index c46b4b930..d4dd7aaa4 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -88,6 +88,11 @@ class PlayableCharacter implements IRegistryEntry return _data.freeplayDJ.getFreeplayDJText(index); } + public function getCharSelectData():PlayerCharSelectData + { + return _data.charSelect; + } + /** * @param rank Which rank to get info for * @return An array of animations. For example, BF Great has two animations, one for BF and one for GF diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index 3f532d33c..83da967b0 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -27,7 +27,7 @@ import funkin.ui.title.TitleState; import funkin.ui.story.StoryMenuState; import funkin.ui.Prompt; import funkin.util.WindowUtil; -#if discord_rpc +#if FEATURE_DISCORD_RPC import Discord.DiscordClient; #end #if newgrounds @@ -54,7 +54,7 @@ class MainMenuState extends MusicBeatState override function create():Void { - #if discord_rpc + #if FEATURE_DISCORD_RPC // Updating Discord Rich Presence DiscordClient.changePresence("In the Menus", null); #end @@ -98,14 +98,7 @@ class MainMenuState extends MusicBeatState add(menuItems); menuItems.onChange.add(onMenuItemChange); menuItems.onAcceptPress.add(function(_) { - if (_.name == 'freeplay') - { - magenta.visible = true; - } - else - { - FlxFlicker.flicker(magenta, 1.1, 0.15, false, true); - } + FlxFlicker.flicker(magenta, 1.1, 0.15, false, true); }); menuItems.enabled = true; // can move on intro @@ -117,13 +110,7 @@ class MainMenuState extends MusicBeatState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - openSubState(new FreeplayState( - { - #if debug - // If SHIFT is held, toggle the selected character, else use the remembered character - character: (FlxG.keys.pressed.SHIFT) ? (FreeplayState.rememberedCharacterId == Constants.DEFAULT_CHARACTER ? 'pico' : 'bf') : null, - #end - })); + openSubState(new FreeplayState()); }); #if CAN_OPEN_LINKS @@ -347,7 +334,7 @@ class MainMenuState extends MusicBeatState } } - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS // Open the debug menu, defaults to ` / ~ if (controls.DEBUG_MENU) { @@ -358,6 +345,7 @@ class MainMenuState extends MusicBeatState if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.W) { + FunkinSound.playOnce(Paths.sound('confirmMenu')); // Give the user a score of 1 point on Weekend 1 story mode. // This makes the level count as cleared and displays the songs in Freeplay. funkin.save.Save.instance.setLevelScore('weekend1', 'easy', @@ -378,6 +366,29 @@ class MainMenuState extends MusicBeatState }); } + if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.L) + { + FunkinSound.playOnce(Paths.sound('confirmMenu')); + // Give the user a score of 0 points on Weekend 1 story mode. + // This makes the level count as uncleared and no longer displays the songs in Freeplay. + funkin.save.Save.instance.setLevelScore('weekend1', 'easy', + { + score: 1, + tallies: + { + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }); + } + if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.R) { // Give the user a hypothetical overridden score, diff --git a/source/funkin/ui/options/MenuItemEnums.hx b/source/funkin/ui/options/MenuItemEnums.hx new file mode 100644 index 000000000..4513a92af --- /dev/null +++ b/source/funkin/ui/options/MenuItemEnums.hx @@ -0,0 +1,10 @@ +package funkin.ui.options; + +// Add enums for use with `EnumPreferenceItem` here! +/* Example: + class MyOptionEnum + { + public static inline var YuhUh = "true"; // "true" is the value's ID + public static inline var NuhUh = "false"; + } + */ diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx index 783aef0ba..5fbefceed 100644 --- a/source/funkin/ui/options/PreferencesMenu.hx +++ b/source/funkin/ui/options/PreferencesMenu.hx @@ -8,6 +8,11 @@ import funkin.ui.AtlasText.AtlasFont; import funkin.ui.options.OptionsState.Page; import funkin.graphics.FunkinCamera; import funkin.ui.TextMenuList.TextMenuItem; +import funkin.audio.FunkinSound; +import funkin.ui.options.MenuItemEnums; +import funkin.ui.options.items.CheckboxPreferenceItem; +import funkin.ui.options.items.NumberPreferenceItem; +import funkin.ui.options.items.EnumPreferenceItem; class PreferencesMenu extends Page { @@ -69,11 +74,51 @@ class PreferencesMenu extends Page }, Preferences.autoPause); } + override function update(elapsed:Float):Void + { + super.update(elapsed); + + // Indent the selected item. + items.forEach(function(daItem:TextMenuItem) { + 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) + { + // Magic number because of the weird offset thats being added by default + thyOffset += thyTextWidth - 75; + } + + if (items.selectedItem == daItem) + { + thyOffset += 150; + } + else + { + thyOffset += 120; + } + + daItem.x = thyOffset; + }); + } + + // - Preference item creation methods - + // Should be moved into a separate PreferenceItems class but you can't access PreferencesMenu.items and PreferencesMenu.preferenceItems from outside. + + /** + * Creates a pref item that works with booleans + * @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 createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void { var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue); - items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() { + items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() { var value = !checkbox.currentValue; onChange(value); checkbox.currentValue = value; @@ -82,62 +127,54 @@ class PreferencesMenu extends Page preferenceItems.add(checkbox); } - override function update(elapsed:Float) + /** + * Creates a pref item that works with general numbers + * @param onChange Gets called every time the player changes the value; use this to apply the value + * @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed value looks + * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable) + * @param min Minimum value (example: 0) + * @param max Maximum value (example: 10) + * @param step The value to increment/decrement by (default = 0.1) + * @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12) + */ + function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Int, min:Int, max:Int, + step:Float = 0.1, precision:Int):Void { - super.update(elapsed); + var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter); + items.addItem(prefName, item); + preferenceItems.add(item.lefthandText); + } - // Indent the selected item. - // TODO: Only do this on menu change? - items.forEach(function(daItem:TextMenuItem) { - if (items.selectedItem == daItem) daItem.x = 150; - else - daItem.x = 120; - }); - } -} - -class CheckboxPreferenceItem extends FlxSprite -{ - public var currentValue(default, set):Bool; - - public function new(x:Float, y:Float, defaultValue:Bool = false) - { - super(x, y); - - frames = Paths.getSparrowAtlas('checkboxThingie'); - animation.addByPrefix('static', 'Check Box unselected', 24, false); - animation.addByPrefix('checked', 'Check Box selecting animation', 24, false); - - setGraphicSize(Std.int(width * 0.7)); - updateHitbox(); - - this.currentValue = defaultValue; - } - - override function update(elapsed:Float) - { - super.update(elapsed); - - switch (animation.curAnim.name) - { - case 'static': - offset.set(); - case 'checked': - offset.set(17, 70); - } - } - - function set_currentValue(value:Bool):Bool - { - if (value) - { - animation.play('checked', true); - } - else - { - animation.play('static'); - } - - return currentValue = value; + /** + * Creates a pref item that works with number percentages + * @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) + * @param min Minimum value (default = 0) + * @param max Maximum value (default = 100) + */ + function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void + { + var newCallback = function(value:Float) { + onChange(Std.int(value)); + }; + var formatter = function(value:Float) { + return '${value}%'; + }; + var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter); + items.addItem(prefName, item); + preferenceItems.add(item.lefthandText); + } + + /** + * Creates a pref item that works with enums + * @param values Maps enum values to display strings _(ex: `NoteHitSoundType.PingPong => "Ping pong"`)_ + * @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 + { + var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultValue, onChange); + items.addItem(prefName, item); + preferenceItems.add(item.lefthandText); } } diff --git a/source/funkin/ui/options/items/CheckboxPreferenceItem.hx b/source/funkin/ui/options/items/CheckboxPreferenceItem.hx new file mode 100644 index 000000000..88c4fb6b0 --- /dev/null +++ b/source/funkin/ui/options/items/CheckboxPreferenceItem.hx @@ -0,0 +1,49 @@ +package funkin.ui.options.items; + +import flixel.FlxSprite.FlxSprite; + +class CheckboxPreferenceItem extends FlxSprite +{ + public var currentValue(default, set):Bool; + + public function new(x:Float, y:Float, defaultValue:Bool = false) + { + super(x, y); + + frames = Paths.getSparrowAtlas('checkboxThingie'); + animation.addByPrefix('static', 'Check Box unselected', 24, false); + animation.addByPrefix('checked', 'Check Box selecting animation', 24, false); + + setGraphicSize(Std.int(width * 0.7)); + updateHitbox(); + + this.currentValue = defaultValue; + } + + override function update(elapsed:Float) + { + super.update(elapsed); + + switch (animation.curAnim.name) + { + case 'static': + offset.set(); + case 'checked': + offset.set(17, 70); + } + } + + function set_currentValue(value:Bool):Bool + { + if (value) + { + animation.play('checked', true); + } + else + { + animation.play('static'); + } + + return currentValue = value; + } +} diff --git a/source/funkin/ui/options/items/EnumPreferenceItem.hx b/source/funkin/ui/options/items/EnumPreferenceItem.hx new file mode 100644 index 000000000..02a273353 --- /dev/null +++ b/source/funkin/ui/options/items/EnumPreferenceItem.hx @@ -0,0 +1,84 @@ +package funkin.ui.options.items; + +import funkin.ui.TextMenuList; +import funkin.ui.AtlasText; +import funkin.input.Controls; +import funkin.ui.options.MenuItemEnums; +import haxe.EnumTools; + +/** + * Preference item that allows the player to pick a value from an enum (list of values) + */ +class EnumPreferenceItem extends TextMenuItem +{ + function controls():Controls + { + return PlayerSettings.player1.controls; + } + + public var lefthandText:AtlasText; + + public var currentValue:String; + public var onChangeCallback:NullVoid>; + 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) + { + super(x, y, name, function() { + callback(this.currentValue); + }); + + updateHitbox(); + + this.map = map; + this.currentValue = defaultValue; + this.onChangeCallback = callback; + + var i:Int = 0; + for (key in map.keys()) + { + this.keys.push(key); + if (this.currentValue == key) index = i; + i += 1; + } + + lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT); + } + + override function update(elapsed:Float):Void + { + super.update(elapsed); + + // var fancyTextFancyColor:Color; + if (selected) + { + var shouldDecrease:Bool = controls().UI_LEFT_P; + var shouldIncrease:Bool = controls().UI_RIGHT_P; + + if (shouldDecrease) index -= 1; + if (shouldIncrease) index += 1; + + if (index > keys.length - 1) index = 0; + if (index < 0) index = keys.length - 1; + + currentValue = keys[index]; + if (onChangeCallback != null && (shouldIncrease || shouldDecrease)) + { + onChangeCallback(currentValue); + } + } + + lefthandText.text = formatted(currentValue); + } + + function formatted(value: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}'; + } +} diff --git a/source/funkin/ui/options/items/NumberPreferenceItem.hx b/source/funkin/ui/options/items/NumberPreferenceItem.hx new file mode 100644 index 000000000..f3cd3cd46 --- /dev/null +++ b/source/funkin/ui/options/items/NumberPreferenceItem.hx @@ -0,0 +1,136 @@ +package funkin.ui.options.items; + +import funkin.ui.TextMenuList; +import funkin.ui.AtlasText; +import funkin.input.Controls; + +/** + * Preference item that allows the player to pick a value between min and max + */ +class NumberPreferenceItem extends TextMenuItem +{ + function controls():Controls + { + return PlayerSettings.player1.controls; + } + + // Widgets + public var lefthandText:AtlasText; + + // Constants + static final HOLD_DELAY:Float = 0.3; // seconds + static final CHANGE_RATE:Float = 0.08; // seconds + + // Constructor-initialized variables + public var currentValue:Float; + public var min:Float; + public var max:Float; + public var step:Float; + public var precision:Int; + public var onChangeCallback:NullVoid>; + public var valueFormatter:NullString>; + + // Variables + var holdDelayTimer:Float = HOLD_DELAY; // seconds + var changeRateTimer:Float = 0.0; // seconds + + /** + * @param min Minimum value (example: 0) + * @param max Maximum value (example: 100) + * @param step The value to increment/decrement by (example: 10) + * @param callback Will get called every time the user changes the setting; use this to apply/save the setting. + * @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed string looks + */ + public function new(x:Float, y:Float, name:String, defaultValue:Float, min:Float, max:Float, step:Float, precision:Int, ?callback:Float->Void, + ?valueFormatter:Float->String):Void + { + super(x, y, name, function() { + callback(this.currentValue); + }); + lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT); + + updateHitbox(); + + this.currentValue = defaultValue; + this.min = min; + this.max = max; + this.step = step; + this.precision = precision; + this.onChangeCallback = callback; + this.valueFormatter = valueFormatter; + } + + override function update(elapsed:Float):Void + { + super.update(elapsed); + + // var fancyTextFancyColor:Color; + if (selected) + { + holdDelayTimer -= elapsed; + if (holdDelayTimer <= 0.0) + { + changeRateTimer -= elapsed; + } + + var jpLeft:Bool = controls().UI_LEFT_P; + var jpRight:Bool = controls().UI_RIGHT_P; + + if (jpLeft || jpRight) + { + holdDelayTimer = HOLD_DELAY; + changeRateTimer = 0.0; + } + + var shouldDecrease:Bool = jpLeft; + var shouldIncrease:Bool = jpRight; + + if (controls().UI_LEFT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0) + { + shouldDecrease = true; + changeRateTimer = CHANGE_RATE; + } + else if (controls().UI_RIGHT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0) + { + shouldIncrease = true; + changeRateTimer = CHANGE_RATE; + } + + // Actually increasing/decreasing the value + if (shouldDecrease) + { + var isBelowMin:Bool = currentValue - step < min; + currentValue = (currentValue - step).clamp(min, max); + if (onChangeCallback != null && !isBelowMin) onChangeCallback(currentValue); + } + else if (shouldIncrease) + { + var isAboveMax:Bool = currentValue + step > max; + currentValue = (currentValue + step).clamp(min, max); + if (onChangeCallback != null && !isAboveMax) onChangeCallback(currentValue); + } + } + + lefthandText.text = formatted(currentValue); + } + + /** Turns the float into a string */ + function formatted(value:Float):String + { + var float:Float = toFixed(value); + if (valueFormatter != null) + { + return valueFormatter(float); + } + else + { + return '${float}'; + } + } + + function toFixed(value:Float):Float + { + var multiplier:Float = Math.pow(10, precision); + return Math.floor(value * multiplier) / multiplier; + } +} diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx index 0547404a1..dfb11dd20 100644 --- a/source/funkin/ui/story/LevelProp.hx +++ b/source/funkin/ui/story/LevelProp.hx @@ -16,7 +16,7 @@ class LevelProp extends Bopper this.propData = value; this.visible = this.propData != null; - danceEvery = this.propData?.danceEvery ?? 0; + danceEvery = this.propData?.danceEvery ?? 1.0; applyData(); } @@ -32,7 +32,7 @@ class LevelProp extends Bopper public function playConfirm():Void { - playAnimation('confirm', true, true); + if (hasAnimation('confirm')) playAnimation('confirm', true, true); } function applyData():Void diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 4e51fb229..18614d414 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -216,7 +216,7 @@ class StoryMenuState extends MusicBeatState changeLevel(); refresh(); - #if discord_rpc + #if FEATURE_DISCORD_RPC // Updating Discord Rich Presence DiscordClient.changePresence('In the Menus', null); #end diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index 0f2ce1076..5b82cc741 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -174,7 +174,7 @@ class LoadingState extends MusicBeatSubState FlxG.watch.addQuick('percentage?', callbacks.numRemaining / callbacks.length); } - #if debug + #if FEATURE_DEBUG_FUNCTIONS if (FlxG.keys.justPressed.SPACE) trace('fired: ' + callbacks.getFired() + ' unfired:' + callbacks.getUnfired()); #end } @@ -291,29 +291,51 @@ class LoadingState extends MusicBeatSubState FunkinSprite.preparePurgeCache(); FunkinSprite.cacheTexture(Paths.image('healthBar')); FunkinSprite.cacheTexture(Paths.image('menuDesat')); - FunkinSprite.cacheTexture(Paths.image('combo')); - FunkinSprite.cacheTexture(Paths.image('num0')); - FunkinSprite.cacheTexture(Paths.image('num1')); - FunkinSprite.cacheTexture(Paths.image('num2')); - FunkinSprite.cacheTexture(Paths.image('num3')); - FunkinSprite.cacheTexture(Paths.image('num4')); - FunkinSprite.cacheTexture(Paths.image('num5')); - FunkinSprite.cacheTexture(Paths.image('num6')); - FunkinSprite.cacheTexture(Paths.image('num7')); - FunkinSprite.cacheTexture(Paths.image('num8')); - FunkinSprite.cacheTexture(Paths.image('num9')); + // Lord have mercy on me and this caching -anysad + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/combo')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num0')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num1')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num2')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num3')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num4')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num5')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num6')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num7')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num8')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num9')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/combo')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num0')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num1')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num2')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num3')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num4')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num5')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num6')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num7')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num8')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num9')); + FunkinSprite.cacheTexture(Paths.image('notes', 'shared')); FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared')); FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared')); FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets')); - FunkinSprite.cacheTexture(Paths.image('ready', 'shared')); - FunkinSprite.cacheTexture(Paths.image('set', 'shared')); - FunkinSprite.cacheTexture(Paths.image('go', 'shared')); - FunkinSprite.cacheTexture(Paths.image('sick', 'shared')); - FunkinSprite.cacheTexture(Paths.image('good', 'shared')); - FunkinSprite.cacheTexture(Paths.image('bad', 'shared')); - FunkinSprite.cacheTexture(Paths.image('shit', 'shared')); - FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this + + FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/ready', 'shared')); + FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/set', 'shared')); + FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/go', 'shared')); + FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/ready', 'shared')); + FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/set', 'shared')); + FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/go', 'shared')); + + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/sick')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/good')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/bad')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/shit')); + + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/sick')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/good')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/bad')); + FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/shit')); // List all image assets in the level's library. // This is crude and I want to remove it when we have a proper asset caching system. diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 2d4fef1f4..fa03b229d 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -41,9 +41,9 @@ class Constants * A suffix to add to the game version. * Add a suffix to prototype builds and remove it for releases. */ - public static final VERSION_SUFFIX:String = #if (DEBUG || FORCE_DEBUG_VERSION) ' PROTOTYPE' #else '' #end; + public static final VERSION_SUFFIX:String = #if FEATURE_DEBUG_FUNCTIONS ' PROTOTYPE' #else '' #end; - #if (debug || FORCE_DEBUG_VERSION) + #if FEATURE_DEBUG_FUNCTIONS static function get_VERSION():String { return 'v${Application.current.meta.get('version')} (${GIT_BRANCH} : ${GIT_HASH}${GIT_HAS_LOCAL_CHANGES ? ' : MODIFIED' : ''})' + VERSION_SUFFIX; @@ -258,6 +258,11 @@ class Constants */ public static final DEFAULT_NOTE_STYLE:String = 'funkin'; + /** + * The default pixel note style for songs. + */ + public static final DEFAULT_PIXEL_NOTE_STYLE:String = 'pixel'; + /** * The default album for songs in Freeplay. */ @@ -379,11 +384,7 @@ class Constants * 1 = The preloader waits for 1 second before moving to the next step. * The progress bare is automatically rescaled to match. */ - #if debug - public static final PRELOADER_MIN_STAGE_TIME:Float = 0.0; - #else public static final PRELOADER_MIN_STAGE_TIME:Float = 0.1; - #end /** * HEALTH VALUES @@ -523,12 +524,16 @@ class Constants * OTHER */ // ============================== + #if FEATURE_GHOST_TAPPING + // Hey there, Eric here. + // This feature is currently still in development. You can test it out by creating a special debug build! + // lime build windows -DFEATURE_GHOST_TAPPING /** - * If true, the player will not receive the ghost miss penalty if there are no notes within the hit window. - * This is the thing people have been begging for forever lolol. + * Duration, in seconds, after the player's section ends before the player can spam without penalty. */ - public static final GHOST_TAPPING:Bool = false; + public static final GHOST_TAP_DELAY:Float = 3 / 8; + #end /** * The maximum number of previous file paths for the Chart Editor to remember. diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index 71d1ad394..1b607ddfd 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -265,9 +265,10 @@ class CrashHandler static function renderMethod():String { - try + var outputStr:String = 'UNKNOWN'; + outputStr = try { - return switch (FlxG.renderMethod) + switch (FlxG.renderMethod) { case FlxRenderMethod.DRAW_TILES: 'DRAW_TILES'; case FlxRenderMethod.BLITTING: 'BLITTING'; @@ -276,7 +277,9 @@ class CrashHandler } catch (e) { - return 'ERROR ON QUERY RENDER METHOD: ${e}'; + 'ERROR ON QUERY RENDER METHOD: ${e}'; } + + return outputStr; } } diff --git a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx index f69609531..0e1e238ac 100644 --- a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx +++ b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx @@ -1,6 +1,9 @@ package funkin.util.plugins; +import flixel.FlxG; import flixel.FlxBasic; +import funkin.ui.MusicBeatState; +import funkin.ui.MusicBeatSubState; /** * A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state. @@ -28,10 +31,15 @@ class ReloadAssetsDebugPlugin extends FlxBasic if (FlxG.keys.justPressed.F5) #end { - funkin.modding.PolymodHandler.forceReloadAssets(); + var state:Dynamic = FlxG.state; + if (state is MusicBeatState || state is MusicBeatSubState) state.reloadAssets(); + else + { + funkin.modding.PolymodHandler.forceReloadAssets(); - // Create a new instance of the current state, so old data is cleared. - FlxG.resetState(); + // Create a new instance of the current state, so old data is cleared. + FlxG.resetState(); + } } }