diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index efa81b56d..000000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Bug Report -about: Report a bug or critical performance issue -title: 'Bug Report: [DESCRIBE YOUR BUG IN DETAIL HERE]' -labels: bug ---- - -[weed]: <> (FILL THIS ISSUE THING OUT AS MUCH AS POSSIBLE) -[weed]: <> (OR ELSE YOUR ISSUE WILL BE LESS LIKELY TO BE SOLVED!) -[weed]: <> (DO NOT POST ABOUT ISSUES FROM OTHER FNF MOD ENGINES! I CANNOT AND PROBABLY WON'T SOLVE THOSE!) -[weed]: <> (GO TO THEIR RESPECTIVE GITHUB ISSUES AND REPORT THEM THERE LOL!) - -[weed]: <> (ALSO MAKE SURE THAT YOU USE PROPER LABELS, IF YOU'RE RUNNING INTO COMPILER ISSUES, USE THE compiler issue LABEL!!!) - -#### Please check for duplicates or similar issues, as well performing simple troubleshooting steps (such as clearing cookies, clearing AppData, trying another browser) before submitting an issue. -### If you are playing the game in a browser, what site are you playing it from? - -[weed]: <> (Put an X in the [ ] thingies to fill out checkbox!) -[weed]: <> (something like [x] pretty much, don't screw up or you will look stupid) - -- [ ] [Newgrounds](https://www.newgrounds.com/portal/view/770371) -- [ ] [Itch.io](https://ninja-muffin24.itch.io/funkin)? Specify below -- - [ ] Windows -- - [ ] Mac -- - [ ] Linux - -### If you are playing the game in a browser, what browser are you using? - -[weed]: <> (Again, put an x in the [ ] box!) - -- [ ] Google Chrome (or chomium based like Brave, vivaldi, MS Edge) -- [ ] Firefox -- [ ] Safari - -## What version of the game are you using? Look in the bottom left corner of the main menu. (ex: 0.2.7, 0.2.1, shit like that) - - -## Have you identified any steps to reproduce the bug? If so, please describe them below in as much detail as possible. Use images if possible. - -## Please describe your issue. Provide extensive detail and images if possible. - - - -## If you're game is FROZEN and you're playing a web version, press F12 to open up browser dev window, and go to console, and copy-paste whatever red error you're getting diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 000000000..6be3f1245 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,62 @@ +name: Bug Report +description: Report a bug or an issue in the game. +labels: ["type: minor bug", "status: pending triage"] +title: "Bug Report: " +body: + - type: checkboxes + attributes: + label: Issue Checklist + options: + - label: I have properly named the issue + - label: I have checked the issues/discussions pages to see if the issue has been previously reported + + - type: dropdown + attributes: + label: What platform are you using? + options: + - Newgrounds (Web) + - Itch.io (Web) + - Itch.io (Downloadable Build) - Windows + - Itch.io (Downloadable Build) - MacOS + - Itch.io (Downloadable Build) - Linux + validations: + required: true + + - type: dropdown + attributes: + label: If you are playing on a browser, which one are you using? + options: + - Google Chrome + - Microsoft Edge + - Firefox + - Opera + - Safari + - Other (Specify below) + + - type: input + attributes: + label: Version + description: What version are you using? + placeholder: ex. 0.4.1 + validations: + required: true + + - type: markdown + attributes: + value: "## Describe your bug." + + - type: markdown + attributes: + value: "### Please do not report issues from other engines. These must be reported in their respective repositories." + + - type: markdown + attributes: + value: "#### Provide as many details as you can." + + - type: textarea + attributes: + label: Context (Provide images, videos, etc.) + + - type: textarea + attributes: + label: Steps to reproduce (or crash logs, errors, etc.) diff --git a/.github/ISSUE_TEMPLATE/compiling.md b/.github/ISSUE_TEMPLATE/compiling.md deleted file mode 100644 index 14aea44a7..000000000 --- a/.github/ISSUE_TEMPLATE/compiling.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Compiling help -about: If you need help compiling the game, and you're running into issues. (Look through the 'compiling help' label in case it's been solved!) -title: 'Compiling help: [BRIEF DESCRIPTION / ERROR MESSAGE OUTPUT]' -labels: compiling help ---- - -[weed]: <> (FILL THIS ISSUE THING OUT AS MUCH AS POSSIBLE) -[weed]: <> (OR ELSE YOUR ISSUE WILL BE LESS LIKELY TO BE SOLVED!) -[weed]: <> (DO NOT POST ABOUT ISSUES FROM OTHER FNF MOD ENGINES! I CANNOT AND PROBABLY WON'T SOLVE THOSE!) -[weed]: <> (GO TO THEIR RESPECTIVE GITHUB ISSUES AND REPORT THEM THERE LOL!) - -#### Please check for duplicates or similar compiler issues by filtering for 'compiler help' - -[weed]: <> (Put an X in the [ ] thingies to fill out checkbox!) -[weed]: <> (something like [x] pretty much, don't screw up or you will look stupid) - - -- [ ] Windows -- [ ] Mac -- [ ] Linux -- [ ] HTML5 - -## Please describe your issue. Provide extensive detail and images if possible. - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3ba13e0ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml new file mode 100644 index 000000000..6fb7c9437 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -0,0 +1,70 @@ +name: Crash Report +description: Report a crash that occurred while playing the game. +labels: ["type: major bug", "status: pending triage"] +title: "Crash Report: " +body: + - type: checkboxes + attributes: + label: Issue Checklist + options: + - label: I have properly named the issue + - label: I have checked the issues/discussions pages to see if the issue has been previously reported + + - type: dropdown + attributes: + label: What platform are you using? + options: + - Newgrounds (Web) + - Itch.io (Web) + - Itch.io (Downloadable Build) - Windows + - Itch.io (Downloadable Build) - MacOS + - Itch.io (Downloadable Build) - Linux + validations: + required: true + + - type: dropdown + attributes: + label: If you are playing on a browser, which one are you using? + options: + - Google Chrome + - Microsoft Edge + - Firefox + - Opera + - Safari + - Other (Specify below) + + - type: input + attributes: + label: Version + description: What version are you using? + placeholder: ex. 0.4.1 + validations: + required: true + + - type: markdown + attributes: + value: "## Describe your issue." + + - type: markdown + attributes: + value: "### Please do not report issues from other engines. These must be reported in their respective repositories." + + - type: markdown + attributes: + value: "#### Provide as many details as you can." + + - type: textarea + attributes: + label: Context (Provide screenshots or videos of the crash happening) + + - type: textarea + attributes: + label: Steps to reproduce + validations: + required: true + + - type: textarea + attributes: + label: Crash logs (can be found in the logs folder where Funkin.exe is) + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md deleted file mode 100644 index e1cc3ae0d..000000000 --- a/.github/ISSUE_TEMPLATE/enhancement.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Enhancement -about: Suggest a new feature -title: 'Enhancement: ' -labels: enhancement ---- -#### Please check for duplicates or similar issues before creating this issue. -## What is your suggestion, and why should it be implemented? diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml new file mode 100644 index 000000000..816e4a12b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -0,0 +1,15 @@ +name: Enhancement +description: Suggest a new feature. +labels: ["type: enhancement", "status: pending triage"] +title: "Enhancement: " +body: + - type: checkboxes + attributes: + label: Issue Checklist + options: + - label: I have properly named the enhancement + - label: I have checked the issues/discussions pages to see if the enhancement has been previously suggested + + - type: textarea + attributes: + label: What is your suggestion, and why should it be implemented? diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index a257c217a..000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: Question -about: Ask a general question -title: 'Question: ' -labels: question ---- - -[weed]: <> (This isn't a place for AMA type questions, if you want to ask any of the devs something, reach out to them on twitter prob ) -[weed]: <> (any biz bullshit can go to cameron.taylor.ninja@gmail.com) - -#### Please check for duplicates or similar issues before asking your question. -## What is your question? diff --git a/.github/PULL_REQUEST_TEMPLATE/bug.md b/.github/PULL_REQUEST_TEMPLATE/bug.md deleted file mode 100644 index 41914c5ed..000000000 --- a/.github/PULL_REQUEST_TEMPLATE/bug.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Bug Fix -about: Fix a bug or critical performance issue -title: 'Bug Fix: ' -labels: bug ---- -#### Please check for duplicates or similar PRs before creating this issue. -## Does this PR close any issue(s)? If so, link them below. - -## Briefly describe the issue(s) fixed. diff --git a/.github/PULL_REQUEST_TEMPLATE/enhancement.md b/.github/PULL_REQUEST_TEMPLATE/enhancement.md deleted file mode 100644 index e208deefe..000000000 --- a/.github/PULL_REQUEST_TEMPLATE/enhancement.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Enhancement -about: Add a new feature -title: 'Enhancement: ' -labels: enhancement ---- -#### Please check for duplicates or similar PRs before creating this issue. -## Does this PR close any issue(s)? If so, link them below. - -## What do your change(s) add, and why should they be implemented? diff --git a/.github/changed-lines-count-labeler.yml b/.github/changed-lines-count-labeler.yml new file mode 100644 index 000000000..6f890f534 --- /dev/null +++ b/.github/changed-lines-count-labeler.yml @@ -0,0 +1,12 @@ +# Add 'small' to any changes below 10 lines +small: + max: 9 + +# Add 'medium' to any changes between 10 and 100 lines +medium: + min: 10 + max: 99 + +# Add 'large' to any changes for more than 100 lines +large: + min: 100 diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..e8e490865 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,12 @@ +# 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' + +# Adds Haxe tag to PR's changing haxe code files +Haxe: +- changed-files: + - any-glob-to-any-file: '**/*.hx' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..8e8a45e19 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,6 @@ + +## Does this PR close any issues? If so, link them below. + +## Briefly describe the issue(s) fixed. + +## Include any relevant screenshots or videos. diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..a861af578 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,27 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Set basic labels + uses: actions/labeler@v5 + with: + sync-labels: true + changed-lines-count-labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + name: An action for automatically labelling pull requests based on the changed lines count + steps: + - name: Set change count labels + uses: vkirilichev/changed-lines-count-labeler@v0.2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + configuration-path: .github/changed-lines-count-labeler.yml diff --git a/.vscode/launch.json b/.vscode/launch.json index 74f72b826..6dc1dc008 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,10 +3,17 @@ "configurations": [ { // Launch in native/CPP on Windows/OSX/Linux - "name": "Lime", + "name": "Lime Build+Debug", "type": "lime", "request": "launch" }, + { + // Launch in native/CPP on Windows/OSX/Linux + "name": "Lime Debug (No Build)", + "type": "lime", + "request": "launch", + "preLaunchTask": null + }, { // Launch in browser "name": "HTML5 Debug", diff --git a/.vscode/settings.json b/.vscode/settings.json index a8a67245b..26fe0b042 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -155,6 +155,11 @@ "target": "hl", "args": ["-debug", "-DDIALOGUE"] }, + { + "label": "Windows / Debug (Results Screen Test)", + "target": "windows", + "args": ["-debug", "-DRESULTS"] + }, { "label": "Windows / Debug (Straight to Chart Editor)", "target": "windows", diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f830047..653ee203f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,109 @@ 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.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 +- Freeplay menu controls (favoriting and switching categories) are now rebindable from the Options menu, and now have default binds on controllers. +### Changed +- Highscores and ranks are now saved separately, which fixes the issue where people would overwrite their saves with higher scores, +which would remove their rank if they had a lower one. +- A-Bot speaker now reacts to the user's volume preference on desktop (thanks to [M7theguy for the issue report/suggestion](https://github.com/FunkinCrew/Funkin/issues/2744)!) +- On Freeplay, heart icons are shifted to the right when you favorite a song that has no rank on it. +- Only play `scrollMenu` sound effect when there's a real change on the freeplay menu ([thanks gamerbross for the PR!](https://github.com/FunkinCrew/Funkin/pull/2741)) +- Gave antialiasing to the edge of the dad graphic on Freeplay +- Rearranged some controls in the controls menu +- Made several chart revisions + - Re-enabled custom camera events in Roses (Erect/Nightmare) + - Tweaked the chart for Lit Up (Hard) + - Corrected the difficulty ratings for M.I.L.F. (Easy/Normal/Hard) +### Fixed +- Fixed an issue in the controls menu where some control binds would overlap their names +- Fixed crash when attempting to exit the gameover screen when also attempting to retry the song ([thanks DMMaster636 for the PR!](https://github.com/FunkinCrew/Funkin/pull/2709)) +- Fix botplay sustain release bug ([thanks Hundrec!](Fix botplay sustain release bug #2683)) +- Fix for the camera not pausing during a gameplay pause ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2684)) +- Fixed issue where Pico's gameplay sprite would unintentionally appear on the gameover screen when dying on 2Hot from an explosion +- Freeplay previews properly fade volume during the BF idle animation +- Fixed bug where Dadbattle incorrectly appeared as Dadbattle Erect when returning to freeplay on Hard +- Fixed 2Hot not appearing under the "#" category in Freeplay menu +- Fixed a bug where the Chart Editor would crash when attempting to select an event with the Event toolbox open +- Improved offsets for Pico and Tankman opponents so they don't slide around as much. +- Fixed the black "temp" graphic on freeplay from being incorrectly sized / masked, now it's identical to the dad freeplay graphic + +## [0.4.0] - 2024-06-06 +### Added +- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu! +- Major visual improvements to the Results screen, with additional animations and audio based on your performance. +- Major visual improvements to the Freeplay screen, with song difficulty ratings and player rank displays. + - Freeplay now plays a preview of songs when you hover over them. +- Added a Charter field to the chart format, to allow for crediting the creator of a level's chart. + - You can see who charted a song from the Pause menu. +- Added a new Scroll Speed chart event to change the note speed mid-song (thanks burgerballs!) +### Changed +- Tweaked the charts for several songs: + - Tutorial (increased the note speed slightly) + - Spookeez + - Monster + - Winter Horrorland + - M.I.L.F. + - Senpai (increased the note speed) + - Roses + - Thorns (increased the note speed slightly) + - Ugh + - Stress + - Lit Up +- Favorite songs marked in Freeplay are now stored between sessions. +- The Freeplay easter eggs are now easier to see. +- In the event that the game cannot load your save data, it will now perform a backup before clearing it, so that we can try to repair it in the future. +- Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!) +- Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!) + - Remember that for more complex behaviors such as animations or transitions, you should use an XML file to define each frame. +- Improved the Event Toolbox in the Chart Editor; dropdowns are now bigger, include search field, and display elements in alphabetical order rather than a random order. +### Fixed +- Fixed an issue where Nene's visualizer would not play on Desktop builds +- Fixed a bug where the game would silently fail to load saves on HTML5 +- Fixed some bugs with the props on the Story Menu not bopping properly +- Additional fixes to the Loading bar on HTML5 (thanks lemz1!) +- Fixed several bugs with the TitleState, including missing music when returning from the Main Menu (thanks gamerbross!) +- Fixed a camera bug in the Main Menu (thanks richTrash21!) +- Fixed a bug where changing difficulties in Story mode wouldn't update the score (thanks sectorA!) +- Fixed a crash in Freeplay caused by a level referencing an invalid song (thanks gamerbross!) +- Fixed a bug where pressing the volume keys would stop the Toy commercial (thanks gamerbross!) +- Fixed a bug where the Chart Editor Playtest would crash when losing (thanks gamerbross!) +- Fixed a bug where hold notes would display improperly in the Chart Editor when downscroll was enabled for gameplay (thanks gamerbross!) +- Fixed a bug where hold notes would be positioned wrong on downscroll (thanks MaybeMaru!) +- Removed a large number of unused imports to optimize builds (thanks Ethan-makes-music!) +- Improved debug logging for unscripted stages (thanks gamerbross!) +- Made improvements to compiling documentation (thanks gedehari!) +- Fixed a crash on Linux caused by an old version of hxCodec (thanks Noobz4Life!) +- Optimized animation handling for characters (thanks richTrash21!) +- Made improvements to compiling documentation (thanks gedehari!) +- Fixed an issue where the Chart Editor would use an incorrect instrumental on imported Legacy songs (thanks gamerbross!) +- Fixed a camera bug in the Main Menu (thanks richTrash21!) +- Fixed a bug where opening the game from the command line would crash the preloader (thanks NotHyper474!) +- Fixed a bug where characters would sometimes use the wrong scale value (thanks PurSnake!) +- Additional bug fixes and optimizations. + +## [0.3.3] - 2024-05-14 +### Changed +- Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!) +### Fixed +- Fixes to the Loading bar on HTML5 (thanks lemz1!) +- Don't allow any more inputs when exiting freeplay (thanks gamerbros!) +- Fixed using mouse wheel to scroll on freeplay (thanks JugieNoob!) +- Fixed the reset's of the health icons, score, and notes when re-entering gameplay from gameover (thanks ImCodist!) +- Fixed the chart editor character selector's hitbox width (thanks MadBear422!) +- Fixed camera stutter once a wipe transition to the Main Menu completes (thanks ImCodist!) +- Fixed an issue where hold note would be invisible for a single frame (thanks ImCodist!) +- Fix tween accumulation on title screen when pressing Y multiple times (thanks TheGaloXx!) +- Fix a crash when querying FlxG.state in the crash handler +- Fix for a game over easter egg so you don't accidentally exit it when viewing +- Fix an issue where the Freeplay menu never displays 100% clear +- Fix an issue where Weekend 1 Pico attempted to retrieve a missing asset. +- Fix an issue where duplicate keybinds would be stoed, potentially causing a crash +- Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart (thanks nebulazorua!) +- Fix a crash on Freeplay found on AMD graphics cards + ## [0.3.2] - 2024-05-03 ### Added - Added `,` and `.` keybinds to the Chart Editor. These place Focus Camera events at the playhead, for the opponent and player respectively. diff --git a/Project.xml b/Project.xml index fcfcfb9f3..fae9c768b 100644 --- a/Project.xml +++ b/Project.xml @@ -1,7 +1,8 @@ - + - + @@ -28,7 +29,7 @@ - + @@ -125,9 +126,12 @@ + + + diff --git a/README.md b/README.md index 62794b924..b1e16f6de 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Friday Night Funkin' -Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludem Dare 47. +Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47. -This game was made with love to Newgrounds and it's community. Extra love to Tom Fulp. +This game was made with love to Newgrounds and its community. Extra love to Tom Fulp. - [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371) - [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin) @@ -23,7 +23,7 @@ Full credits can be found in-game, or wherever the credits.json file is. ## Programming - [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer -- [MasterEric](https://twitter.com/EliteMasterEric) - Programmer +- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer - [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming - [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming - Our contributors on GitHub diff --git a/art b/art index 66572f85d..faeba700c 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa +Subproject commit faeba700c5526bd4fd57ccc927d875c82b9d3553 diff --git a/assets b/assets index 962130b22..2e1594ee4 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 962130b2243a839106607d08a11599b1857bf8b3 +Subproject commit 2e1594ee4c04c7148628bae471bdd061c9deb6b7 diff --git a/checkstyle.json b/checkstyle.json index dc89409da..41f0a7998 100644 --- a/checkstyle.json +++ b/checkstyle.json @@ -79,7 +79,7 @@ { "props": { "ignoreExtern": true, - "format": "^[a-z][A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$", + "format": "^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$", "tokens": ["INLINE", "NOTINLINE"] }, "type": "ConstantName" diff --git a/docs/COMPILING.md b/docs/COMPILING.md index 7278e027c..1fc0c51c6 100644 --- a/docs/COMPILING.md +++ b/docs/COMPILING.md @@ -2,13 +2,19 @@ 0. Setup - Download Haxe from [Haxe.org](https://haxe.org) -1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo: - - `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git` - - If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way. -2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`) -3. Install all haxelibs of the current branch by running `hmm install` -4. Setup lime: `haxelib run lime setup` -5. Platform setup + - Download Git from [git-scm.com](https://www.git-scm.com) + - Do NOT download the repository using the Download ZIP button on GitHub or you may run into errors! + - Instead, open a command prompt and do the following steps... +1. Run `cd the\directory\you\want\the\source\code\in` to specify which folder the command prompt is working in. + - For example, `cd C:\Users\YOURNAME\Documents` would instruct the command prompt to perform the next steps in your Documents folder. +2. Run `git clone https://github.com/FunkinCrew/funkin.git` to clone the base repository. +3. Run `cd funkin` to enter the cloned repository's directory. +4. Run `git submodule update --init --recursive` to download the game's assets. + - NOTE: By performing this operation, you are downloading Content which is proprietary and protected by national and international copyright and trademark laws. See [the LICENSE.md file for the Funkin.assets](https://github.com/FunkinCrew/funkin.assets/blob/main/LICENSE.md) repo for more information. +5. Run `haxelib --global install hmm` and then `haxelib --global run hmm setup` to install hmm.json +6. Run `hmm install` to install all haxelibs of the current branch +7. Run `haxelib run lime setup` to set up lime +8. Platform setup - For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe) - When prompted, select "Individual Components" and make sure to download the following: - MSVC v143 VS 2022 C++ x64/x86 build tools @@ -16,5 +22,12 @@ - Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/) - Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/) - HTML5: Compiles without any extra setup -6. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug` -7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State). +9. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug` +10. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State). + +# Troubleshooting - GO THROUGH THESE STEPS BEFORE OPENING ISSUES ON GITHUB! + +- During the cloning process, you may experience an error along the lines of `error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)` due to poor connectivity. A common fix is to run ` git config --global http.postBuffer 4096M`. +- Make sure your game directory has an `assets` folder! If it's missing, copy the path to your `funkin` folder and run `cd the\path\you\copied`. Then follow the guide starting from **Step 4**. +- Check that your `assets` folder is not empty! If it is, go back to **Step 4** and follow the guide from there. +- The compilation process often fails due to having the wrong versions of the required libraries. Many errors can be resolved by deleting the `.haxelib` folder and following the guide starting from **Step 5**. diff --git a/docs/Funkin' Debug Hotkeys.md b/docs/Funkin' Debug Hotkeys.md index c560c5422..1287d5a1b 100644 --- a/docs/Funkin' Debug Hotkeys.md +++ b/docs/Funkin' Debug Hotkeys.md @@ -1,31 +1,31 @@ # Funkin' Debug Hotkeys -`F4` (EVERYWHERE) - Leave Current State and move to Main Menu -`F5` (EVERYWHERE) - Hot Reload Data Files +Most of this functionality is only available on debug builds of the game! -`Y` (Title Screen) - WOAH +## Any State +- `F2`: ***OVERLAY***: Enables the Flixel debug overlay, which has partial support for scripting. +- `F3`: ***SCREENSHOT***: Takes a screenshot of the game and saves it to the local `screenshots` directory. Works outside of debug builds too! +- `F4`: ***EJECT***: Forcibly switch state to the Main Menu (with no extra transition). Useful if you're stuck in a level and you need to get out! +- `F5`: ***HOT RELOAD***: Forcibly reload the game's scripts and data files, then restart the current state. If any files in the `assets` folder have been modified, the game should process the changes for you! NOTE: Known bug, this does not reset song charts or song scripts, but it should reset everything else (such as stage layout data and character animation data). +- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace. -`~` (Main Menu) - Access Debug Menu +## **Play State** +- `H`: ***HIDE UI***: Makes the user interface invisible. Works in Pause Menu, great for screenshots. +- `1`: ***END SONG***: Immediately ends the song and moves to Results Screen on Freeplay, or next song on Story Mode. +- `2`: ***GAIN HEALTH***: Debug function, add 10% to the player's health. +- `3`: ***LOSE HEALTH***: Debug function, subtract 5% to the player's health. +- `9`: NEATO! +- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL****: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead. +- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL****: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead. -`U` (Play) - Open Stage Editor State -`H` (Play) - Show/Hide HUD -`1` (Play) - End Song -`2` (Play) - Add 10% Health -`3` (Play) - Subtract 5% Health -`7` (Play) - (NOT WORKING) Open Chart Editor -`8` (Play) - Open Animation Editor -`9` (Play) - (Easter Egg) Classic Health Icon -`PGUP`/`Fn+Up` (Play) - Skip Forward In Time -`PGDN`/`Fn+Down` (Play) - 🦃 That's right, we're going to go BACK IN TIME +## **Freeplay State** +- `F` (Freeplay Menu) - Move to Favorites +- `Q` (Freeplay Menu) - Back one category +- `E` (Freeplay Menu) - Forward one category -`F` (Freeplay Menu) - Move to Favorites -`P` (Freeplay Menu) - Switch to Pico (probably doesn't work) -`T` (Freeplay Menu) - Start typing in search bar -`Q` (Freeplay Menu) - Back one letter -`E` (Freeplay Menu) - Forward one letter +## **Title State** +- `Y` - WOAH -`Arrows` (Stage Editor) - Move Prop -`Ctrl-Z` (Stage Editor) - Undo -`Y` (Stage Editor) - Leave Stage Editor - -`H` (Pause Menu) - Hide the Pause Menu UI (good for screenshots!) +## **Main Menu** +- `~`: ***DEBUG****: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu. +- `CTRL-ALT-SHIFT-W`: ***ALL ACCESS***: Unlocks all songs in Freeplay. Only available on debug builds. diff --git a/example_mods/introMod/_polymod_meta.json b/example_mods/introMod/_polymod_meta.json index e0b03f1cd..4dc0cd804 100644 --- a/example_mods/introMod/_polymod_meta.json +++ b/example_mods/introMod/_polymod_meta.json @@ -3,7 +3,7 @@ "description": "An introductory mod.", "contributors": [ { - "name": "MasterEric" + "name": "EliteMasterEric" } ], "api_version": "0.1.0", diff --git a/example_mods/testing123/_polymod_meta.json b/example_mods/testing123/_polymod_meta.json index 4c0f177f9..0a2ed042c 100644 --- a/example_mods/testing123/_polymod_meta.json +++ b/example_mods/testing123/_polymod_meta.json @@ -3,7 +3,7 @@ "description": "Newgrounds? More like OLDGROUNDS lol.", "contributors": [ { - "name": "MasterEric" + "name": "EliteMasterEric" } ], "api_version": "0.1.0", diff --git a/hmm.json b/hmm.json index a6e4467a9..68e0c5cb0 100644 --- a/hmm.json +++ b/hmm.json @@ -40,6 +40,13 @@ "ref": "17e0d59fdbc2b6283a5c0e4df41f1c7f27b71c49", "url": "https://github.com/FunkinCrew/flxanimate" }, + { + "name": "FlxPartialSound", + "type": "git", + "dir": null, + "ref": "f986332ba5ab02abd386ce662578baf04904604a", + "url": "https://github.com/FunkinCrew/FlxPartialSound.git" + }, { "name": "format", "type": "haxelib", @@ -49,9 +56,16 @@ "name": "funkin.vis", "type": "git", "dir": null, - "ref": "2aa654b974507ab51ab1724d2d97e75726fd7d78", + "ref": "38261833590773cb1de34ac5d11e0825696fc340", "url": "https://github.com/FunkinCrew/funkVis" }, + { + "name": "grig.audio", + "type": "git", + "dir": "src", + "ref": "57f5d47f2533fd0c3dcd025a86cb86c0dfa0b6d2", + "url": "https://gitlab.com/haxe-grig/grig.audio.git" + }, { "name": "hamcrest", "type": "haxelib", @@ -80,7 +94,7 @@ "name": "hxCodec", "type": "git", "dir": null, - "ref": "c0c7f2680cc190c932a549c2e2fdd9b0ba2bd10e", + "ref": "61b98a7a353b7f529a8fec84ed9afc919a2dffdd", "url": "https://github.com/FunkinCrew/hxCodec" }, { @@ -153,7 +167,7 @@ "name": "polymod", "type": "git", "dir": null, - "ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac", + "ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7", "url": "https://github.com/larsiusprime/polymod" }, { diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 0d63cb6cc..e73b2860c 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -430,7 +430,7 @@ class Conductor else if (currentTimeChange != null && this.songPosition > 0.0) { // roundDecimal prevents representing 8 as 7.9999999 - this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6); + this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * Constants.STEPS_PER_BEAT) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6); this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT; this.currentMeasureTime = currentStepTime / stepsPerMeasure; this.currentStep = Math.floor(currentStepTime); @@ -564,7 +564,7 @@ class Conductor if (ms >= timeChange.timeStamp) { lastTimeChange = timeChange; - resultStep = lastTimeChange.beatTime * 4; + resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT; } else { @@ -600,7 +600,7 @@ class Conductor var lastTimeChange:SongTimeChange = timeChanges[0]; for (timeChange in timeChanges) { - if (stepTime >= timeChange.beatTime * 4) + if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT) { lastTimeChange = timeChange; resultMs = lastTimeChange.timeStamp; @@ -613,7 +613,7 @@ class Conductor } var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator; - resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs; + resultMs += (stepTime - lastTimeChange.beatTime * Constants.STEPS_PER_BEAT) * lastStepLengthMs; return resultMs; } diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 00d34fadb..49b15ddf6 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -214,6 +214,32 @@ class InitState extends FlxState #elseif STAGEBUILD // -DSTAGEBUILD FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState()); + #elseif RESULTS + // -DRESULTS + FlxG.switchState(() -> new funkin.play.ResultState( + { + storyMode: false, + title: "Cum Song Erect by Kawai Sprite", + songId: "cum", + difficultyId: "nightmare", + isNewHighscore: true, + scoreData: + { + score: 1_234_567, + tallies: + { + sick: 130, + good: 60, + bad: 69, + shit: 69, + missed: 69, + combo: 69, + maxCombo: 69, + totalNotesHit: 140, + totalNotes: 200 // 0, + } + }, + })); #elseif ANIMDEBUG // -DANIMDEBUG FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState()); diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index 54a4b7acf..b0a97c4fa 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -123,9 +123,17 @@ class Paths return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}'; } - public static function inst(song:String, ?suffix:String = ''):String + /** + * Gets the path to an `Inst.mp3/ogg` song instrumental from songs:assets/songs/`song`/ + * @param song name of the song to get instrumental for + * @param suffix any suffix to add to end of song name, used for `-erect` variants usually + * @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`. + * @return String + */ + public static function inst(song:String, ?suffix:String = '', ?withExtension:Bool = true):String { - return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}'; + var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : ''; + return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext'; } public static function image(key:String, ?library:String):String @@ -153,3 +161,11 @@ class Paths return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library)); } } + +enum abstract PathsFunction(String) +{ + var MUSIC; + var INST; + var VOICES; + var SOUND; +} diff --git a/source/funkin/api/newgrounds/NGUnsafe.hx b/source/funkin/api/newgrounds/NGUnsafe.hx index 9616dfe18..77e44bd1d 100644 --- a/source/funkin/api/newgrounds/NGUnsafe.hx +++ b/source/funkin/api/newgrounds/NGUnsafe.hx @@ -1,9 +1,5 @@ package funkin.api.newgrounds; -import flixel.util.FlxSignal; -import flixel.util.FlxTimer; -import lime.app.Application; -import openfl.display.Stage; #if newgrounds import io.newgrounds.NG; import io.newgrounds.NGLite; diff --git a/source/funkin/api/newgrounds/NGio.hx b/source/funkin/api/newgrounds/NGio.hx index c1f8ad3ba..3f5fc078a 100644 --- a/source/funkin/api/newgrounds/NGio.hx +++ b/source/funkin/api/newgrounds/NGio.hx @@ -2,19 +2,11 @@ package funkin.api.newgrounds; #if newgrounds import flixel.util.FlxSignal; -import flixel.util.FlxTimer; import io.newgrounds.NG; import io.newgrounds.NGLite; -import io.newgrounds.components.ScoreBoardComponent.Period; import io.newgrounds.objects.Error; -import io.newgrounds.objects.Medal; import io.newgrounds.objects.Score; -import io.newgrounds.objects.ScoreBoard; -import io.newgrounds.objects.events.Response; -import io.newgrounds.objects.events.Result.GetCurrentVersionResult; -import io.newgrounds.objects.events.Result.GetVersionResult; import lime.app.Application; -import openfl.display.Stage; #end /** diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index df05cc3ef..c70f195d2 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -11,10 +11,14 @@ import funkin.audio.waveform.WaveformDataParser; import funkin.data.song.SongData.SongMusicData; import funkin.data.song.SongRegistry; import funkin.util.tools.ICloneable; +import funkin.util.flixel.sound.FlxPartialSound; +import funkin.Paths.PathsFunction; import openfl.Assets; +import lime.app.Future; +import lime.app.Promise; import openfl.media.SoundMixer; + #if (openfl >= "8.0.0") -import openfl.utils.AssetType; #end /** @@ -223,12 +227,12 @@ class FunkinSound extends FlxSound implements ICloneable // already paused before we lost focus. if (_lostFocus && !_alreadyPaused) { - trace('Resuming audio (${this._label}) on focus!'); + // trace('Resuming audio (${this._label}) on focus!'); resume(); } else { - trace('Not resuming audio (${this._label}) on focus!'); + // trace('Not resuming audio (${this._label}) on focus!'); } _lostFocus = false; } @@ -238,7 +242,7 @@ class FunkinSound extends FlxSound implements ICloneable */ override function onFocusLost():Void { - trace('Focus lost, pausing audio!'); + // trace('Focus lost, pausing audio!'); _lostFocus = true; _alreadyPaused = _paused; pause(); @@ -342,23 +346,76 @@ class FunkinSound extends FlxSound implements ICloneable FlxG.log.warn('Tried and failed to find music metadata for $key'); } } - - var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true); - if (music != null) + var pathsFunction = params.pathsFunction ?? MUSIC; + var suffix = params.suffix ?? ''; + var pathToUse = switch (pathsFunction) { - FlxG.sound.music = music; + case MUSIC: Paths.music('$key/$key'); + case INST: Paths.inst('$key', suffix); + default: Paths.music('$key/$key'); + } - // Prevent repeat update() and onFocus() calls. - FlxG.sound.list.remove(FlxG.sound.music); + var shouldLoadPartial = params.partialParams?.loadPartial ?? false; - return true; + // even if we arent' trying to partial load a song, we want to error out any songs in progress, + // so we don't get overlapping music if someone were to load a new song while a partial one is loading! + + emptyPartialQueue(); + + if (shouldLoadPartial) + { + var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0.0, params.partialParams?.end ?? 1.0, params?.startingVolume ?? 1.0, + params.loop ?? true, false, false, params.onComplete); + + if (music != null) + { + partialQueue.push(music); + + @:nullSafety(Off) + music.future.onComplete(function(partialMusic:Null) { + FlxG.sound.music = partialMusic; + FlxG.sound.list.remove(FlxG.sound.music); + + if (FlxG.sound.music != null && params.onLoad != null) params.onLoad(); + }); + + return true; + } + else + { + return false; + } } else { - return false; + var music = FunkinSound.load(pathToUse, params?.startingVolume ?? 1.0, params.loop ?? true, false, true); + if (music != null) + { + FlxG.sound.music = music; + + // Prevent repeat update() and onFocus() calls. + FlxG.sound.list.remove(FlxG.sound.music); + + return true; + } + else + { + return false; + } } } + public static function emptyPartialQueue():Void + { + while (partialQueue.length > 0) + { + @:nullSafety(Off) + partialQueue.pop().error("Cancel loading partial sound"); + } + } + + static var partialQueue:Array>> = []; + /** * Creates a new `FunkinSound` object synchronously. * @@ -415,6 +472,49 @@ class FunkinSound extends FlxSound implements ICloneable return sound; } + /** + * Will load a section of a sound file, useful for Freeplay where we don't want to load all the bytes of a song + * @param path The path to the sound file + * @param start The start time of the sound file + * @param end The end time of the sound file + * @param volume Volume to start at + * @param looped Whether the sound file should loop + * @param autoDestroy Whether the sound file should be destroyed after it finishes playing + * @param autoPlay Whether the sound file should play immediately + * @param onComplete Callback when the sound finishes playing + * @param onLoad Callback when the sound finishes loading + * @return A FunkinSound object + */ + public static function loadPartial(path:String, start:Float = 0, end:Float = 1, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, + autoPlay:Bool = true, ?onComplete:Void->Void, ?onLoad:Void->Void):Promise> + { + var promise:lime.app.Promise> = new lime.app.Promise>(); + + // split the path and get only after first : + // we are bypassing the openfl/lime asset library fuss + path = Paths.stripLibrary(path); + + var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end); + + if (soundRequest == null) + { + promise.complete(null); + } + else + { + promise.future.onError(function(e) { + soundRequest.error("Sound loading was errored or cancelled"); + }); + + soundRequest.future.onComplete(function(partialSound) { + var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad); + promise.complete(snd); + }); + } + + return promise; + } + @:nullSafety(Off) public override function destroy():Void { @@ -475,6 +575,12 @@ typedef FunkinSoundPlayMusicParams = */ var ?startingVolume:Float; + /** + * The suffix of the music file to play. Usually for "-erect" tracks when loading an INST file + * @default `` + */ + var ?suffix:String; + /** * Whether to override music if a different track is already playing. * @default `false` @@ -498,4 +604,22 @@ typedef FunkinSoundPlayMusicParams = * @default `true` */ var ?mapTimeChanges:Bool; + + /** + * Which Paths function to use to load a song + * @default `MUSIC` + */ + var ?pathsFunction:PathsFunction; + + var ?partialParams:PartialSoundParams; + + var ?onComplete:Void->Void; + var ?onLoad:Void->Void; +} + +typedef PartialSoundParams = +{ + var loadPartial:Bool; + var start:Float; + var end:Float; } diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 5037ee1d0..9a1e0e0c1 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -1,9 +1,7 @@ package funkin.audio; -import funkin.audio.FunkinSound; import flixel.group.FlxGroup.FlxTypedGroup; import funkin.audio.waveform.WaveformData; -import funkin.audio.waveform.WaveformDataParser; class VoicesGroup extends SoundGroup { diff --git a/source/funkin/audio/visualize/ABotVis.hx b/source/funkin/audio/visualize/ABotVis.hx index ca77dd58a..cf43a8add 100644 --- a/source/funkin/audio/visualize/ABotVis.hx +++ b/source/funkin/audio/visualize/ABotVis.hx @@ -1,13 +1,9 @@ package funkin.audio.visualize; -import funkin.audio.visualize.dsp.FFT; import flixel.FlxSprite; -import flixel.addons.plugin.taskManager.FlxTask; import flixel.graphics.frames.FlxAtlasFrames; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; -import flixel.math.FlxMath; import flixel.sound.FlxSound; -import funkin.util.MathUtil; import funkin.vis.dsp.SpectralAnalyzer; import funkin.vis.audioclip.frontends.LimeAudioClip; @@ -58,8 +54,15 @@ class ABotVis extends FlxTypedSpriteGroup public function initAnalyzer() { @:privateAccess - analyzer = new SpectralAnalyzer(7, new LimeAudioClip(cast snd._channel.__source), 0.01, 30); - analyzer.maxDb = -35; + analyzer = new SpectralAnalyzer(snd._channel.__source, 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 + // So we want to manually change it! + analyzer.fftN = 256; + #end + + // analyzer.maxDb = -35; // analyzer.fftN = 2048; } @@ -83,9 +86,7 @@ class ABotVis extends FlxTypedSpriteGroup override function draw() { - #if web if (analyzer != null) drawFFT(); - #end super.draw(); } @@ -94,12 +95,16 @@ class ABotVis extends FlxTypedSpriteGroup */ function drawFFT():Void { - var levels = analyzer.getLevels(false); + var levels = analyzer.getLevels(); for (i in 0...min(group.members.length, levels.length)) { var animFrame:Int = Math.round(levels[i].value * 5); + #if desktop + animFrame = Math.round(animFrame * FlxG.sound.volume); + #end + animFrame = Math.floor(Math.min(5, animFrame)); animFrame = Math.floor(Math.max(0, animFrame)); diff --git a/source/funkin/audio/visualize/PolygonVisGroup.hx b/source/funkin/audio/visualize/PolygonVisGroup.hx index cc68f4ae0..bff845796 100644 --- a/source/funkin/audio/visualize/PolygonVisGroup.hx +++ b/source/funkin/audio/visualize/PolygonVisGroup.hx @@ -1,6 +1,5 @@ package funkin.audio.visualize; -import funkin.audio.visualize.PolygonSpectogram; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.sound.FlxSound; diff --git a/source/funkin/audio/visualize/SpectogramSprite.hx b/source/funkin/audio/visualize/SpectogramSprite.hx index 636c0726a..615e80d95 100644 --- a/source/funkin/audio/visualize/SpectogramSprite.hx +++ b/source/funkin/audio/visualize/SpectogramSprite.hx @@ -8,8 +8,6 @@ import flixel.sound.FlxSound; import flixel.util.FlxColor; import funkin.audio.visualize.PolygonSpectogram.VISTYPE; import funkin.audio.visualize.VisShit.CurAudioInfo; -import funkin.audio.visualize.dsp.FFT; -import lime.system.ThreadPool; import lime.utils.Int16Array; using Lambda; @@ -38,8 +36,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup lengthOfShit = amnt; regenLineShit(); - - // makeGraphic(200, 200, FlxColor.BLACK); } public function regenLineShit():Void @@ -89,8 +85,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup { checkAndSetBuffer(); - // vis.checkAndSetBuffer(); - if (setBuffer) { var samplesToGen:Int = Std.int(sampleRate * seconds); @@ -191,7 +185,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup // a value between 10hz and 100Khz var hzPicker:Float = Math.pow(10, powedShit); - // var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, startingSample, startingSample + samplesToGen)); var remappedFreq:Int = Std.int(FlxMath.remapToRange(hzPicker, 0, 10000, 0, freqShit[0].length - 1)); group.members[i].x = prevLine.x; @@ -211,8 +204,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup var line = FlxPoint.get(prevLine.x - group.members[i].x, prevLine.y - group.members[i].y); // dont draw a line until i figure out a nicer way to view da spikes and shit idk lol! - // group.members[i].setGraphicSize(Std.int(Math.max(line.length, 1)), Std.int(1)); - // group.members[i].angle = line.degrees; } } } @@ -261,9 +252,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup group.members[Std.int(remappedSample)].x = prevLine.x; group.members[Std.int(remappedSample)].y = prevLine.y; - // group.members[0].y = prevLine.y; - - // FlxSpriteUtil.drawLine(this, prevLine.x, prevLine.y, width * remappedSample, left * height / 2 + height / 2); prevLine.x = (curAud.balanced * swagheight / 2 + swagheight / 2) + x; prevLine.y = (Std.int(remappedSample) / lengthOfShit * daHeight) + y; diff --git a/source/funkin/audio/visualize/VisShit.hx b/source/funkin/audio/visualize/VisShit.hx index 204ced1e1..ba235fe89 100644 --- a/source/funkin/audio/visualize/VisShit.hx +++ b/source/funkin/audio/visualize/VisShit.hx @@ -3,7 +3,6 @@ package funkin.audio.visualize; import flixel.math.FlxMath; import flixel.sound.FlxSound; import funkin.audio.visualize.dsp.FFT; -import lime.system.ThreadPool; import lime.utils.Int16Array; import funkin.util.MathUtil; @@ -73,9 +72,6 @@ class VisShit freqOutput.push([]); - // if (FlxG.keys.justPressed.M) - // trace(FFT.rfft(chunk).map(z -> z.scale(1 / fs).magnitude)); - // find spectral peaks and their instantaneous frequencies for (k => s in freqs) { @@ -91,7 +87,6 @@ class VisShit if (freq < maxFreq) freqOutput[indexOfArray].push(power); // } - // haxe.Log.trace("", null); indexOfArray++; // move to next (overlapping) chunk diff --git a/source/funkin/audio/visualize/dsp/FFT.hx b/source/funkin/audio/visualize/dsp/FFT.hx index dc75acb81..40ee9cb8c 100644 --- a/source/funkin/audio/visualize/dsp/FFT.hx +++ b/source/funkin/audio/visualize/dsp/FFT.hx @@ -1,7 +1,5 @@ package funkin.audio.visualize.dsp; -import funkin.audio.visualize.dsp.Complex; - using funkin.audio.visualize.dsp.OffsetArray; using funkin.audio.visualize.dsp.Signal; diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx index 1f649b472..a939f91bf 100644 --- a/source/funkin/audio/waveform/WaveformData.hx +++ b/source/funkin/audio/waveform/WaveformData.hx @@ -1,7 +1,5 @@ package funkin.audio.waveform; -import funkin.util.MathUtil; - @:nullSafety class WaveformData { diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx index 32ced2fbd..8eaba8117 100644 --- a/source/funkin/audio/waveform/WaveformSprite.hx +++ b/source/funkin/audio/waveform/WaveformSprite.hx @@ -1,7 +1,5 @@ package funkin.audio.waveform; -import funkin.audio.waveform.WaveformData; -import funkin.audio.waveform.WaveformDataParser; import funkin.graphics.rendering.MeshRender; import flixel.util.FlxColor; diff --git a/source/funkin/data/dialogue/conversation/ConversationData.hx b/source/funkin/data/dialogue/conversation/ConversationData.hx index 30e3f451b..650519836 100644 --- a/source/funkin/data/dialogue/conversation/ConversationData.hx +++ b/source/funkin/data/dialogue/conversation/ConversationData.hx @@ -1,7 +1,5 @@ package funkin.data.dialogue.conversation; -import funkin.data.animation.AnimationData; - /** * A type definition for the data for a specific conversation. * It includes things like what dialogue boxes to use, what text to display, and what animations to play. diff --git a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx index ca072897f..fad1e43ad 100644 --- a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx +++ b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx @@ -1,7 +1,6 @@ package funkin.data.dialogue.conversation; import funkin.play.cutscene.dialogue.Conversation; -import funkin.data.dialogue.conversation.ConversationData; import funkin.play.cutscene.dialogue.ScriptedConversation; class ConversationRegistry extends BaseRegistry diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md index 3cd3af070..4f1c66ade 100644 --- a/source/funkin/data/song/CHANGELOG.md +++ b/source/funkin/data/song/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.3] +### Added +- Added `charter` field to denote authorship of a chart. + ## [2.2.2] ### Added - Added `playData.previewStart` and `playData.previewEnd` fields to specify when in the song should the song's audio should be played as a preview in Freeplay. diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 26380947a..769af8f08 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -30,6 +30,9 @@ class SongMetadata implements ICloneable @:default("Unknown") public var artist:String; + @:optional + public var charter:Null = null; + @:optional @:default(96) public var divisions:Null; // Optional field @@ -53,6 +56,8 @@ class SongMetadata implements ICloneable @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) public var generatedBy:String; + @:optional + @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS) public var timeFormat:SongTimeFormat; public var timeChanges:Array; @@ -112,14 +117,23 @@ class SongMetadata implements ICloneable */ public function serialize(pretty:Bool = true):String { + // Update generatedBy and version before writing. + updateVersionToLatest(); + var ignoreNullOptionals = true; var writer = new json2object.JsonWriter(ignoreNullOptionals); - // I believe @:jignored should be iggnored by the writer? + // I believe @:jignored should be ignored by the writer? // var output = this.clone(); // output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer. return writer.write(this, pretty ? ' ' : null); } + public function updateVersionToLatest():Void + { + this.version = SongRegistry.SONG_METADATA_VERSION; + this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; + } + /** * Produces a string representation suitable for debugging. */ @@ -368,6 +382,12 @@ class SongMusicData implements ICloneable this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation; } + public function updateVersionToLatest():Void + { + this.version = SongRegistry.SONG_MUSIC_DATA_VERSION; + this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; + } + public function clone():SongMusicData { var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation); @@ -600,11 +620,20 @@ class SongChartData implements ICloneable */ public function serialize(pretty:Bool = true):String { + // Update generatedBy and version before writing. + updateVersionToLatest(); + var ignoreNullOptionals = true; var writer = new json2object.JsonWriter(ignoreNullOptionals); return writer.write(this, pretty ? ' ' : null); } + public function updateVersionToLatest():Void + { + this.version = SongRegistry.SONG_CHART_DATA_VERSION; + this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; + } + public function clone():SongChartData { // We have to manually perform the deep clone here because Map.deepClone() doesn't work. diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 277dcd9e1..a3305c4ec 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.2"; + public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.3"; public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x"; diff --git a/source/funkin/data/song/importer/ChartManifestData.hx b/source/funkin/data/song/importer/ChartManifestData.hx index dd0d28479..04b5a1b69 100644 --- a/source/funkin/data/song/importer/ChartManifestData.hx +++ b/source/funkin/data/song/importer/ChartManifestData.hx @@ -61,10 +61,18 @@ class ChartManifestData */ public function serialize(pretty:Bool = true):String { + // Update generatedBy and version before writing. + updateVersionToLatest(); + var writer = new json2object.JsonWriter(); return writer.write(this, pretty ? ' ' : null); } + public function updateVersionToLatest():Void + { + this.version = CHART_MANIFEST_DATA_VERSION; + } + public static function deserialize(contents:String):Null { var parser = new json2object.JsonParser(); diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx index ab2abda8e..acbb99342 100644 --- a/source/funkin/data/song/importer/FNFLegacyImporter.hx +++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx @@ -36,7 +36,7 @@ class FNFLegacyImporter { trace('Migrating song metadata from FNF Legacy.'); - var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default'); + var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default'); var hadError:Bool = false; @@ -65,7 +65,7 @@ class FNFLegacyImporter songMetadata.timeChanges = rebuildTimeChanges(songData); - songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad', 'mom'); + songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad'); return songMetadata; } diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx index 22b883c75..bebd86d02 100644 --- a/source/funkin/data/stage/StageData.hx +++ b/source/funkin/data/stage/StageData.hx @@ -58,9 +58,17 @@ class StageData */ public function serialize(pretty:Bool = true):String { + // Update generatedBy and version before writing. + updateVersionToLatest(); + var writer = new json2object.JsonWriter(); return writer.write(this, pretty ? ' ' : null); } + + public function updateVersionToLatest():Void + { + this.version = StageRegistry.STAGE_DATA_VERSION; + } } typedef StageDataCharacters = diff --git a/source/funkin/effects/IntervalShake.hx b/source/funkin/effects/IntervalShake.hx new file mode 100644 index 000000000..545739cc3 --- /dev/null +++ b/source/funkin/effects/IntervalShake.hx @@ -0,0 +1,240 @@ +package funkin.effects; + +import flixel.FlxObject; +import flixel.util.FlxDestroyUtil.IFlxDestroyable; +import flixel.util.FlxPool; +import flixel.util.FlxTimer; +import flixel.math.FlxPoint; +import flixel.util.FlxAxes; +import flixel.tweens.FlxEase.EaseFunction; +import flixel.math.FlxMath; + +/** + * pretty much a copy of FlxFlicker geared towards making sprites + * shake around at a set interval and slow down over time. + */ +class IntervalShake implements IFlxDestroyable +{ + static var _pool:FlxPool = new FlxPool(IntervalShake.new); + + /** + * Internal map for looking up which objects are currently shaking and getting their shake data. + */ + static var _boundObjects:Map = new Map(); + + /** + * An effect that shakes the sprite on a set interval and a starting intensity that goes down over time. + * + * @param Object The object to shake. + * @param Duration How long to shake for (in seconds). `0` means "forever". + * @param Interval In what interval to update the shake position. Set to `FlxG.elapsed` if `<= 0`! + * @param StartIntensity The starting intensity of the shake. + * @param EndIntensity The ending intensity of the shake. + * @param Ease Control the easing of the intensity over the shake. + * @param CompletionCallback Callback on shake completion + * @param ProgressCallback Callback on each shake interval + * @return The `IntervalShake` object. `IntervalShake`s are pooled internally, so beware of storing references. + */ + public static function shake(Object:FlxObject, Duration:Float = 1, Interval:Float = 0.04, StartIntensity:Float = 0, EndIntensity:Float = 0, + Ease:EaseFunction, ?CompletionCallback:IntervalShake->Void, ?ProgressCallback:IntervalShake->Void):IntervalShake + { + if (isShaking(Object)) + { + // if (ForceRestart) + // { + // stopShaking(Object); + // } + // else + // { + // Ignore this call if object is already flickering. + return _boundObjects[Object]; + // } + } + + if (Interval <= 0) + { + Interval = FlxG.elapsed; + } + + var shake:IntervalShake = _pool.get(); + shake.start(Object, Duration, Interval, StartIntensity, EndIntensity, Ease, CompletionCallback, ProgressCallback); + return _boundObjects[Object] = shake; + } + + /** + * Returns whether the object is shaking or not. + * + * @param Object The object to test. + */ + public static function isShaking(Object:FlxObject):Bool + { + return _boundObjects.exists(Object); + } + + /** + * Stops shaking the object. + * + * @param Object The object to stop shaking. + */ + public static function stopShaking(Object:FlxObject):Void + { + var boundShake:IntervalShake = _boundObjects[Object]; + if (boundShake != null) + { + boundShake.stop(); + } + } + + /** + * The shaking object. + */ + public var object(default, null):FlxObject; + + /** + * The shaking timer. You can check how many seconds has passed since shaking started etc. + */ + public var timer(default, null):FlxTimer; + + /** + * The starting intensity of the shake. + */ + public var startIntensity(default, null):Float; + + /** + * The ending intensity of the shake. + */ + public var endIntensity(default, null):Float; + + /** + * How long to shake for (in seconds). `0` means "forever". + */ + public var duration(default, null):Float; + + /** + * The interval of the shake. + */ + public var interval(default, null):Float; + + /** + * Defines on what axes to `shake()`. Default value is `XY` / both. + */ + public var axes(default, null):FlxAxes; + + /** + * Defines the initial position of the object at the beginning of the shake effect. + */ + public var initialOffset(default, null):FlxPoint; + + /** + * The callback that will be triggered after the shake has completed. + */ + public var completionCallback(default, null):IntervalShake->Void; + + /** + * The callback that will be triggered every time the object shakes. + */ + public var progressCallback(default, null):IntervalShake->Void; + + /** + * The easing of the intensity over the shake. + */ + public var ease(default, null):EaseFunction; + + /** + * Nullifies the references to prepare object for reuse and avoid memory leaks. + */ + public function destroy():Void + { + object = null; + timer = null; + ease = null; + completionCallback = null; + progressCallback = null; + } + + /** + * Starts shaking behavior. + */ + function start(Object:FlxObject, Duration:Float = 1, Interval:Float = 0.04, StartIntensity:Float = 0, EndIntensity:Float = 0, Ease:EaseFunction, + ?CompletionCallback:IntervalShake->Void, ?ProgressCallback:IntervalShake->Void):Void + { + object = Object; + duration = Duration; + interval = Interval; + completionCallback = CompletionCallback; + startIntensity = StartIntensity; + endIntensity = EndIntensity; + initialOffset = new FlxPoint(Object.x, Object.y); + ease = Ease; + axes = FlxAxes.XY; + _secondsSinceStart = 0; + timer = new FlxTimer().start(interval, shakeProgress, Std.int(duration / interval)); + } + + /** + * Prematurely ends shaking. + */ + public function stop():Void + { + timer.cancel(); + // object.visible = true; + object.x = initialOffset.x; + object.y = initialOffset.y; + release(); + } + + /** + * Unbinds the object from shaking and releases it into pool for reuse. + */ + function release():Void + { + _boundObjects.remove(object); + _pool.put(this); + } + + public var _secondsSinceStart(default, null):Float = 0; + + public var scale(default, null):Float = 0; + + /** + * Just a helper function for shake() to update object's position. + */ + function shakeProgress(timer:FlxTimer):Void + { + _secondsSinceStart += interval; + scale = _secondsSinceStart / duration; + if (ease != null) + { + scale = 1 - ease(scale); + // trace(scale); + } + + var curIntensity:Float = 0; + curIntensity = FlxMath.lerp(endIntensity, startIntensity, scale); + + if (axes.x) object.x = initialOffset.x + FlxG.random.float((-curIntensity) * object.width, (curIntensity) * object.width); + if (axes.y) object.y = initialOffset.y + FlxG.random.float((-curIntensity) * object.width, (curIntensity) * object.width); + + // object.visible = !object.visible; + + if (progressCallback != null) progressCallback(this); + + if (timer.loops > 0 && timer.loopsLeft == 0) + { + object.x = initialOffset.x; + object.y = initialOffset.y; + if (completionCallback != null) + { + completionCallback(this); + } + + if (this.timer == timer) release(); + } + } + + /** + * Internal constructor. Use static methods. + */ + @:keep + function new() {} +} diff --git a/source/funkin/graphics/shaders/AngleMask.hx b/source/funkin/graphics/shaders/AngleMask.hx index 30e508a58..c5ef87b72 100644 --- a/source/funkin/graphics/shaders/AngleMask.hx +++ b/source/funkin/graphics/shaders/AngleMask.hx @@ -5,35 +5,73 @@ import flixel.system.FlxAssets.FlxShader; class AngleMask extends FlxShader { @:glFragmentSource(' - #pragma header - uniform vec2 endPosition; - void main() - { - vec4 base = texture2D(bitmap, openfl_TextureCoordv); + #pragma header - vec2 uv = openfl_TextureCoordv.xy; + uniform vec2 endPosition; + vec2 hash22(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973)); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.xx + p3.yz) * p3.zy); + } - vec2 start = vec2(0.0, 0.0); - vec2 end = vec2(endPosition.x / openfl_TextureSize.x, 1.0); + // ====== GAMMA CORRECTION ====== // + // Helps with color mixing -- good to have by default in almost any shader + // See https://www.shadertoy.com/view/lscSzl + vec3 gamma(in vec3 color) { + return pow(color, vec3(1.0 / 2.2)); + } - float dx = end.x - start.x; - float dy = end.y - start.y; + vec4 mainPass(vec2 fragCoord) { + vec4 base = texture2D(bitmap, fragCoord); - float angle = atan(dy, dx); + vec2 uv = fragCoord.xy; - uv.x -= start.x; - uv.y -= start.y; + vec2 start = vec2(0.0, 0.0); + vec2 end = vec2(endPosition.x / openfl_TextureSize.x, 1.0); - float uvA = atan(uv.y, uv.x); + float dx = end.x - start.x; + float dy = end.y - start.y; - if (uvA < angle) - gl_FragColor = base; - else - gl_FragColor = vec4(0.0); + float angle = atan(dy, dx); - }') + uv.x -= start.x; + uv.y -= start.y; + + float uvA = atan(uv.y, uv.x); + + if (uvA < angle) + return base; + else + return vec4(0.0); + } + + vec4 antialias(vec2 fragCoord) { + + const float AA_STAGES = 2.0; + + const float AA_TOTAL_PASSES = AA_STAGES * AA_STAGES + 1.0; + const float AA_JITTER = 0.5; + + // Run the shader multiple times with a random subpixel offset each time and average the results + vec4 color = mainPass(fragCoord); + for (float x = 0.0; x < AA_STAGES; x++) + { + for (float y = 0.0; y < AA_STAGES; y++) + { + vec2 offset = AA_JITTER * (2.0 * hash22(vec2(x, y)) - 1.0) / openfl_TextureSize.xy; + color += mainPass(fragCoord + offset); + } + } + return color / AA_TOTAL_PASSES; + } + + void main() { + vec4 col = antialias(openfl_TextureCoordv); + // col.xyz = gamma(col.xyz); + gl_FragColor = col; + }') public function new() { super(); diff --git a/source/funkin/graphics/shaders/StrokeShader.hx b/source/funkin/graphics/shaders/StrokeShader.hx index fd133ac0a..d110e7de9 100644 --- a/source/funkin/graphics/shaders/StrokeShader.hx +++ b/source/funkin/graphics/shaders/StrokeShader.hx @@ -40,8 +40,8 @@ class StrokeShader extends FlxShader void main() { - vec4 sample = flixel_texture2D(bitmap, openfl_TextureCoordv); - if (sample.a == 0.) { + vec4 gay = flixel_texture2D(bitmap, openfl_TextureCoordv); + if (gay.a == 0.) { float w = size.x / openfl_TextureSize.x; float h = size.y / openfl_TextureSize.y; @@ -49,9 +49,9 @@ class StrokeShader extends FlxShader || flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x - w, openfl_TextureCoordv.y)).a != 0. || flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x, openfl_TextureCoordv.y + h)).a != 0. || flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x, openfl_TextureCoordv.y - h)).a != 0.) - sample = color; + gay = color; } - gl_FragColor = sample; + gl_FragColor = gay; } ') public function new(color:FlxColor = 0xFFFFFFFF, width:Float = 1, height:Float = 1) diff --git a/source/funkin/import.hx b/source/funkin/import.hx index 250de99cb..c8431be33 100644 --- a/source/funkin/import.hx +++ b/source/funkin/import.hx @@ -11,6 +11,7 @@ import flixel.system.debug.watch.Tracker; // These are great. using Lambda; using StringTools; +using thx.Arrays; using funkin.util.tools.ArraySortTools; using funkin.util.tools.ArrayTools; using funkin.util.tools.FloatTools; diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx index 1983d413b..f6c881f6d 100644 --- a/source/funkin/input/Controls.hx +++ b/source/funkin/input/Controls.hx @@ -31,6 +31,7 @@ class Controls extends FlxActionSet * Uses FlxActions to funnel various inputs to a single action. */ var _ui_up = new FunkinAction(Action.UI_UP); + var _ui_left = new FunkinAction(Action.UI_LEFT); var _ui_right = new FunkinAction(Action.UI_RIGHT); var _ui_down = new FunkinAction(Action.UI_DOWN); @@ -58,7 +59,11 @@ class Controls extends FlxActionSet var _back = new FunkinAction(Action.BACK); var _pause = new FunkinAction(Action.PAUSE); var _reset = new FunkinAction(Action.RESET); - var _screenshot = new FunkinAction(Action.SCREENSHOT); + var _window_screenshot = new FunkinAction(Action.WINDOW_SCREENSHOT); + var _window_fullscreen = new FunkinAction(Action.WINDOW_FULLSCREEN); + var _freeplay_favorite = new FunkinAction(Action.FREEPLAY_FAVORITE); + var _freeplay_left = new FunkinAction(Action.FREEPLAY_LEFT); + var _freeplay_right = new FunkinAction(Action.FREEPLAY_RIGHT); var _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE); var _debug_menu = new FunkinAction(Action.DEBUG_MENU); var _debug_chart = new FunkinAction(Action.DEBUG_CHART); @@ -66,7 +71,6 @@ class Controls extends FlxActionSet var _volume_up = new FunkinAction(Action.VOLUME_UP); var _volume_down = new FunkinAction(Action.VOLUME_DOWN); var _volume_mute = new FunkinAction(Action.VOLUME_MUTE); - var _fullscreen = new FunkinAction(Action.FULLSCREEN); var byName:Map = new Map(); @@ -233,10 +237,30 @@ class Controls extends FlxActionSet inline function get_RESET() return _reset.check(); - public var SCREENSHOT(get, never):Bool; + public var WINDOW_FULLSCREEN(get, never):Bool; - inline function get_SCREENSHOT() - return _screenshot.check(); + inline function get_WINDOW_FULLSCREEN() + return _window_fullscreen.check(); + + public var WINDOW_SCREENSHOT(get, never):Bool; + + inline function get_WINDOW_SCREENSHOT() + return _window_screenshot.check(); + + public var FREEPLAY_FAVORITE(get, never):Bool; + + inline function get_FREEPLAY_FAVORITE() + return _freeplay_favorite.check(); + + public var FREEPLAY_LEFT(get, never):Bool; + + inline function get_FREEPLAY_LEFT() + return _freeplay_left.check(); + + public var FREEPLAY_RIGHT(get, never):Bool; + + inline function get_FREEPLAY_RIGHT() + return _freeplay_right.check(); public var CUTSCENE_ADVANCE(get, never):Bool; @@ -273,11 +297,6 @@ class Controls extends FlxActionSet inline function get_VOLUME_MUTE() return _volume_mute.check(); - public var FULLSCREEN(get, never):Bool; - - inline function get_FULLSCREEN() - return _fullscreen.check(); - public function new(name, scheme:KeyboardScheme = null) { super(name); @@ -294,7 +313,11 @@ class Controls extends FlxActionSet add(_back); add(_pause); add(_reset); - add(_screenshot); + add(_window_screenshot); + add(_window_fullscreen); + add(_freeplay_favorite); + add(_freeplay_left); + add(_freeplay_right); add(_cutscene_advance); add(_debug_menu); add(_debug_chart); @@ -302,21 +325,19 @@ class Controls extends FlxActionSet add(_volume_up); add(_volume_down); add(_volume_mute); - add(_fullscreen); - for (action in digitalActions) { - if (Std.isOfType(action, FunkinAction)) { + for (action in digitalActions) + { + if (Std.isOfType(action, FunkinAction)) + { var funkinAction:FunkinAction = cast action; byName[funkinAction.name] = funkinAction; - if (funkinAction.namePressed != null) - byName[funkinAction.namePressed] = funkinAction; - if (funkinAction.nameReleased != null) - byName[funkinAction.nameReleased] = funkinAction; + if (funkinAction.namePressed != null) byName[funkinAction.namePressed] = funkinAction; + if (funkinAction.nameReleased != null) byName[funkinAction.nameReleased] = funkinAction; } } - if (scheme == null) - scheme = None; + if (scheme == null) scheme = None; setKeyboardScheme(scheme, false); } @@ -329,38 +350,38 @@ class Controls extends FlxActionSet public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool { #if debug - if (!byName.exists(name)) - throw 'Invalid name: $name'; + if (!byName.exists(name)) throw 'Invalid name: $name'; #end var action = byName[name]; - if (gamepadOnly) - return action.checkFiltered(trigger, GAMEPAD); + if (gamepadOnly) return action.checkFiltered(trigger, GAMEPAD); else return action.checkFiltered(trigger); } - public function getKeysForAction(name:Action):Array { + public function getKeysForAction(name:Action):Array + { #if debug - if (!byName.exists(name)) - throw 'Invalid name: $name'; + if (!byName.exists(name)) throw 'Invalid name: $name'; #end // TODO: Revert to `.map().filter()` once HashLink doesn't complain anymore. var result:Array = []; - for (input in byName[name].inputs) { + for (input in byName[name].inputs) + { if (input.device == KEYBOARD) result.push(input.inputID); } return result; } - public function getButtonsForAction(name:Action):Array { + public function getButtonsForAction(name:Action):Array + { #if debug - if (!byName.exists(name)) - throw 'Invalid name: $name'; + if (!byName.exists(name)) throw 'Invalid name: $name'; #end var result:Array = []; - for (input in byName[name].inputs) { + for (input in byName[name].inputs) + { if (input.device == GAMEPAD) result.push(input.inputID); } return result; @@ -384,7 +405,7 @@ class Controls extends FlxActionSet function getActionFromControl(control:Control):FlxActionDigital { - return switch(control) + return switch (control) { case UI_UP: _ui_up; case UI_DOWN: _ui_down; @@ -398,7 +419,11 @@ class Controls extends FlxActionSet case BACK: _back; case PAUSE: _pause; case RESET: _reset; - case SCREENSHOT: _screenshot; + case WINDOW_SCREENSHOT: _window_screenshot; + case WINDOW_FULLSCREEN: _window_fullscreen; + case FREEPLAY_FAVORITE: _freeplay_favorite; + case FREEPLAY_LEFT: _freeplay_left; + case FREEPLAY_RIGHT: _freeplay_right; case CUTSCENE_ADVANCE: _cutscene_advance; case DEBUG_MENU: _debug_menu; case DEBUG_CHART: _debug_chart; @@ -406,7 +431,6 @@ class Controls extends FlxActionSet case VOLUME_UP: _volume_up; case VOLUME_DOWN: _volume_down; case VOLUME_MUTE: _volume_mute; - case FULLSCREEN: _fullscreen; } } @@ -424,7 +448,7 @@ class Controls extends FlxActionSet */ function forEachBound(control:Control, func:FlxActionDigital->FlxInputState->Void) { - switch(control) + switch (control) { case UI_UP: func(_ui_up, PRESSED); @@ -466,8 +490,16 @@ class Controls extends FlxActionSet func(_pause, JUST_PRESSED); case RESET: func(_reset, JUST_PRESSED); - case SCREENSHOT: - func(_screenshot, JUST_PRESSED); + case WINDOW_SCREENSHOT: + func(_window_screenshot, JUST_PRESSED); + case WINDOW_FULLSCREEN: + func(_window_fullscreen, JUST_PRESSED); + case FREEPLAY_FAVORITE: + func(_freeplay_favorite, JUST_PRESSED); + case FREEPLAY_LEFT: + func(_freeplay_left, JUST_PRESSED); + case FREEPLAY_RIGHT: + func(_freeplay_right, JUST_PRESSED); case CUTSCENE_ADVANCE: func(_cutscene_advance, JUST_PRESSED); case DEBUG_MENU: @@ -482,17 +514,14 @@ class Controls extends FlxActionSet func(_volume_down, JUST_PRESSED); case VOLUME_MUTE: func(_volume_mute, JUST_PRESSED); - case FULLSCREEN: - func(_fullscreen, JUST_PRESSED); } } public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int) { - if (toAdd == toRemove) - return; + if (toAdd == toRemove) return; - switch(device) + switch (device) { case Keys: forEachBound(control, function(action, state) replaceKey(action, toAdd, toRemove, state)); @@ -504,7 +533,8 @@ class Controls extends FlxActionSet function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState) { - if (action.inputs.length == 0) { + if (action.inputs.length == 0) + { // Add the keybind, don't replace. addKeys(action, [toAdd], state); return; @@ -518,26 +548,44 @@ class Controls extends FlxActionSet if (input.device == KEYBOARD && input.inputID == toRemove) { - if (toAdd == FlxKey.NONE) { + if (toAdd == FlxKey.NONE) + { // Remove the keybind, don't replace. action.inputs.remove(input); - } else { + } + else + { // Replace the keybind. @:privateAccess action.inputs[i].inputID = toAdd; } hasReplaced = true; } + else if (input.device == KEYBOARD && input.inputID == toAdd) + { + // This key is already bound! + if (hasReplaced) + { + // Remove the duplicate keybind, don't replace. + action.inputs.remove(input); + } + else + { + hasReplaced = true; + } + } } - if (!hasReplaced) { + if (!hasReplaced) + { addKeys(action, [toAdd], state); } } function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState) { - if (action.inputs.length == 0) { + if (action.inputs.length == 0) + { addButtons(action, [toAdd], state, deviceID); return; } @@ -556,7 +604,8 @@ class Controls extends FlxActionSet } } - if (!hasReplaced) { + if (!hasReplaced) + { addButtons(action, [toAdd], state, deviceID); } } @@ -568,18 +617,16 @@ class Controls extends FlxActionSet var action = controls.byName[name]; for (input in action.inputs) { - if (device == null || isDevice(input, device)) - byName[name].add(cast input); + if (device == null || isDevice(input, device)) byName[name].add(cast input); } } - switch(device) + switch (device) { case null: // add all for (gamepad in controls.gamepadsAdded) - if (gamepadsAdded.indexOf(gamepad) == -1) - gamepadsAdded.push(gamepad); + if (gamepadsAdded.indexOf(gamepad) == -1) gamepadsAdded.push(gamepad); mergeKeyboardScheme(controls.keyboardScheme); @@ -599,7 +646,7 @@ class Controls extends FlxActionSet { if (scheme != None) { - switch(keyboardScheme) + switch (keyboardScheme) { case None: keyboardScheme = scheme; @@ -634,7 +681,8 @@ class Controls extends FlxActionSet static function addKeys(action:FlxActionDigital, keys:Array, state:FlxInputState) { - for (key in keys) { + for (key in keys) + { if (key == FlxKey.NONE) continue; // Ignore unbound keys. action.addKey(key, state); } @@ -646,15 +694,13 @@ class Controls extends FlxActionSet while (i-- > 0) { var input = action.inputs[i]; - if (input.device == KEYBOARD && keys.indexOf(cast input.inputID) != -1) - action.remove(input); + if (input.device == KEYBOARD && keys.indexOf(cast input.inputID) != -1) action.remove(input); } } public function setKeyboardScheme(scheme:KeyboardScheme, reset = true) { - if (reset) - removeKeyboard(); + if (reset) removeKeyboard(); keyboardScheme = scheme; @@ -670,7 +716,11 @@ class Controls extends FlxActionSet bindKeys(Control.BACK, getDefaultKeybinds(scheme, Control.BACK)); bindKeys(Control.PAUSE, getDefaultKeybinds(scheme, Control.PAUSE)); bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET)); - bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT)); + bindKeys(Control.WINDOW_SCREENSHOT, getDefaultKeybinds(scheme, Control.WINDOW_SCREENSHOT)); + bindKeys(Control.WINDOW_FULLSCREEN, getDefaultKeybinds(scheme, Control.WINDOW_FULLSCREEN)); + bindKeys(Control.FREEPLAY_FAVORITE, getDefaultKeybinds(scheme, Control.FREEPLAY_FAVORITE)); + bindKeys(Control.FREEPLAY_LEFT, getDefaultKeybinds(scheme, Control.FREEPLAY_LEFT)); + bindKeys(Control.FREEPLAY_RIGHT, getDefaultKeybinds(scheme, Control.FREEPLAY_RIGHT)); bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE)); bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU)); bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART)); @@ -678,15 +728,17 @@ class Controls extends FlxActionSet bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP)); bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN)); bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE)); - bindKeys(Control.FULLSCREEN, getDefaultKeybinds(scheme, Control.FULLSCREEN)); bindMobileLol(); } - function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array { - switch (scheme) { + function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array + { + switch (scheme) + { case Solo: - switch (control) { + switch (control) + { case Control.UI_UP: return [W, FlxKey.UP]; case Control.UI_DOWN: return [S, FlxKey.DOWN]; case Control.UI_LEFT: return [A, FlxKey.LEFT]; @@ -699,7 +751,11 @@ class Controls extends FlxActionSet case Control.BACK: return [X, BACKSPACE, ESCAPE]; case Control.PAUSE: return [P, ENTER, ESCAPE]; case Control.RESET: return [R]; - case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen + case Control.WINDOW_FULLSCREEN: return [F11]; // We use F for other things LOL. + case Control.WINDOW_SCREENSHOT: return [F3]; + case Control.FREEPLAY_FAVORITE: return [F]; // Favorite a song on the menu + case Control.FREEPLAY_LEFT: return [Q]; // Switch tabs on the menu + case Control.FREEPLAY_RIGHT: return [E]; // Switch tabs on the menu case Control.CUTSCENE_ADVANCE: return [Z, ENTER]; case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_CHART: return []; @@ -707,11 +763,10 @@ class Controls extends FlxActionSet case Control.VOLUME_UP: return [PLUS, NUMPADPLUS]; case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS]; case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO]; - case Control.FULLSCREEN: return [FlxKey.F]; - } case Duo(true): - switch (control) { + switch (control) + { case Control.UI_UP: return [W]; case Control.UI_DOWN: return [S]; case Control.UI_LEFT: return [A]; @@ -724,7 +779,11 @@ class Controls extends FlxActionSet case Control.BACK: return [H, X]; case Control.PAUSE: return [ONE]; case Control.RESET: return [R]; - case Control.SCREENSHOT: return [PRINTSCREEN]; + case Control.WINDOW_SCREENSHOT: return [F3]; + case Control.WINDOW_FULLSCREEN: return [F11]; + case Control.FREEPLAY_FAVORITE: return [F]; // Favorite a song on the menu + case Control.FREEPLAY_LEFT: return [Q]; // Switch tabs on the menu + case Control.FREEPLAY_RIGHT: return [E]; // Switch tabs on the menu case Control.CUTSCENE_ADVANCE: return [G, Z]; case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_CHART: return []; @@ -732,11 +791,10 @@ class Controls extends FlxActionSet case Control.VOLUME_UP: return [PLUS]; case Control.VOLUME_DOWN: return [MINUS]; case Control.VOLUME_MUTE: return [ZERO]; - case Control.FULLSCREEN: return [FlxKey.F]; - } case Duo(false): - switch (control) { + switch (control) + { case Control.UI_UP: return [FlxKey.UP]; case Control.UI_DOWN: return [FlxKey.DOWN]; case Control.UI_LEFT: return [FlxKey.LEFT]; @@ -749,16 +807,18 @@ class Controls extends FlxActionSet case Control.BACK: return [ESCAPE]; case Control.PAUSE: return [ONE]; case Control.RESET: return [R]; - case Control.SCREENSHOT: return [PRINTSCREEN]; + case Control.WINDOW_SCREENSHOT: return []; + case Control.WINDOW_FULLSCREEN: return []; + case Control.FREEPLAY_FAVORITE: return []; + case Control.FREEPLAY_LEFT: return []; + case Control.FREEPLAY_RIGHT: return []; case Control.CUTSCENE_ADVANCE: return [ENTER]; - case Control.DEBUG_MENU: return [GRAVEACCENT]; + case Control.DEBUG_MENU: return []; case Control.DEBUG_CHART: return []; case Control.DEBUG_STAGE: return []; case Control.VOLUME_UP: return [NUMPADPLUS]; case Control.VOLUME_DOWN: return [NUMPADMINUS]; case Control.VOLUME_MUTE: return [NUMPADZERO]; - case Control.FULLSCREEN: return []; - } default: // Fallthrough. @@ -785,8 +845,7 @@ class Controls extends FlxActionSet #end #if android - forEachBound(Control.BACK, function(action, pres) - { + forEachBound(Control.BACK, function(action, pres) { action.add(new FlxActionInputDigitalAndroid(FlxAndroidKey.BACK, JUST_PRESSED)); }); #end @@ -800,8 +859,7 @@ class Controls extends FlxActionSet while (i-- > 0) { var input = action.inputs[i]; - if (input.device == KEYBOARD) - action.remove(input); + if (input.device == KEYBOARD) action.remove(input); } } } @@ -813,11 +871,13 @@ class Controls extends FlxActionSet fromSaveData(padData, Gamepad(id)); } - public function getGamepadIds():Array { + public function getGamepadIds():Array + { return gamepadsAdded; } - public function getGamepads():Array { + public function getGamepads():Array + { return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)]; } @@ -837,8 +897,7 @@ class Controls extends FlxActionSet while (i-- > 0) { var input = action.inputs[i]; - if (isGamepad(input, deviceID)) - action.remove(input); + if (isGamepad(input, deviceID)) action.remove(input); } } @@ -848,52 +907,85 @@ class Controls extends FlxActionSet public function addDefaultGamepad(id):Void { addGamepadLiteral(id, [ - Control.ACCEPT => getDefaultGamepadBinds(Control.ACCEPT), Control.BACK => getDefaultGamepadBinds(Control.BACK), Control.UI_UP => getDefaultGamepadBinds(Control.UI_UP), Control.UI_DOWN => getDefaultGamepadBinds(Control.UI_DOWN), Control.UI_LEFT => getDefaultGamepadBinds(Control.UI_LEFT), Control.UI_RIGHT => getDefaultGamepadBinds(Control.UI_RIGHT), - // don't swap A/B or X/Y for switch on these. A is always the bottom face button Control.NOTE_UP => getDefaultGamepadBinds(Control.NOTE_UP), Control.NOTE_DOWN => getDefaultGamepadBinds(Control.NOTE_DOWN), Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT), Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT), Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE), Control.RESET => getDefaultGamepadBinds(Control.RESET), - // Control.SCREENSHOT => [], - // Control.VOLUME_UP => [RIGHT_SHOULDER], - // Control.VOLUME_DOWN => [LEFT_SHOULDER], - // Control.VOLUME_MUTE => [RIGHT_TRIGGER], + Control.WINDOW_FULLSCREEN => getDefaultGamepadBinds(Control.WINDOW_FULLSCREEN), + Control.WINDOW_SCREENSHOT => getDefaultGamepadBinds(Control.WINDOW_SCREENSHOT), Control.CUTSCENE_ADVANCE => getDefaultGamepadBinds(Control.CUTSCENE_ADVANCE), - // Control.DEBUG_MENU - // Control.DEBUG_CHART + Control.FREEPLAY_FAVORITE => getDefaultGamepadBinds(Control.FREEPLAY_FAVORITE), + Control.FREEPLAY_LEFT => getDefaultGamepadBinds(Control.FREEPLAY_LEFT), + Control.FREEPLAY_RIGHT => getDefaultGamepadBinds(Control.FREEPLAY_RIGHT), + Control.VOLUME_UP => getDefaultGamepadBinds(Control.VOLUME_UP), + Control.VOLUME_DOWN => getDefaultGamepadBinds(Control.VOLUME_DOWN), + Control.VOLUME_MUTE => getDefaultGamepadBinds(Control.VOLUME_MUTE), + Control.DEBUG_MENU => getDefaultGamepadBinds(Control.DEBUG_MENU), + Control.DEBUG_CHART => getDefaultGamepadBinds(Control.DEBUG_CHART), + Control.DEBUG_STAGE => getDefaultGamepadBinds(Control.DEBUG_STAGE), ]); } - function getDefaultGamepadBinds(control:Control):Array { - switch(control) { - case Control.ACCEPT: return [#if switch B #else A #end]; - case Control.BACK: return [#if switch A #else B #end, FlxGamepadInputID.BACK]; - case Control.UI_UP: return [DPAD_UP, LEFT_STICK_DIGITAL_UP]; - case Control.UI_DOWN: return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN]; - case Control.UI_LEFT: return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT]; - case Control.UI_RIGHT: return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT]; - case Control.NOTE_UP: return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP]; - case Control.NOTE_DOWN: return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN]; - case Control.NOTE_LEFT: return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT]; - case Control.NOTE_RIGHT: return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT]; - case Control.PAUSE: return [START]; - case Control.RESET: return [RIGHT_SHOULDER]; - case Control.SCREENSHOT: return []; - case Control.VOLUME_UP: return []; - case Control.VOLUME_DOWN: return []; - case Control.VOLUME_MUTE: return []; - case Control.CUTSCENE_ADVANCE: return [A]; - case Control.DEBUG_MENU: return []; - case Control.DEBUG_CHART: return []; - case Control.FULLSCREEN: return []; + function getDefaultGamepadBinds(control:Control):Array + { + switch (control) + { + case Control.ACCEPT: + return [#if switch B #else A #end]; + case Control.BACK: + return [#if switch A #else B #end]; + case Control.UI_UP: + return [DPAD_UP, LEFT_STICK_DIGITAL_UP]; + case Control.UI_DOWN: + return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN]; + case Control.UI_LEFT: + return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT]; + case Control.UI_RIGHT: + return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT]; + case Control.NOTE_UP: + return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP]; + case Control.NOTE_DOWN: + return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN]; + case Control.NOTE_LEFT: + return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT]; + case Control.NOTE_RIGHT: + return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT]; + case Control.PAUSE: + return [START]; + case Control.RESET: + return [FlxGamepadInputID.BACK]; // Back (i.e. Select) + case Control.WINDOW_FULLSCREEN: + []; + case Control.WINDOW_SCREENSHOT: + []; + case Control.CUTSCENE_ADVANCE: + return [A]; + case Control.FREEPLAY_FAVORITE: + [FlxGamepadInputID.BACK]; // Back (i.e. Select) + case Control.FREEPLAY_LEFT: + [LEFT_SHOULDER]; + case Control.FREEPLAY_RIGHT: + [RIGHT_SHOULDER]; + case Control.VOLUME_UP: + []; + case Control.VOLUME_DOWN: + []; + case Control.VOLUME_MUTE: + []; + case Control.DEBUG_MENU: + []; + case Control.DEBUG_CHART: + []; + case Control.DEBUG_STAGE: + []; default: // Fallthrough. } @@ -911,8 +1003,7 @@ class Controls extends FlxActionSet public function touchShit(control:Control, id) { - forEachBound(control, function(action, state) - { + forEachBound(control, function(action, state) { // action }); } @@ -928,7 +1019,8 @@ class Controls extends FlxActionSet inline static function addButtons(action:FlxActionDigital, buttons:Array, state, id) { - for (button in buttons) { + for (button in buttons) + { if (button == FlxGamepadInputID.NONE) continue; // Ignore unbound keys. action.addGamepad(button, state, id); } @@ -940,29 +1032,25 @@ class Controls extends FlxActionSet while (i-- > 0) { var input = action.inputs[i]; - if (isGamepad(input, gamepadID) && buttons.indexOf(cast input.inputID) != -1) - action.remove(input); + if (isGamepad(input, gamepadID) && buttons.indexOf(cast input.inputID) != -1) action.remove(input); } } public function getInputsFor(control:Control, device:Device, ?list:Array):Array { - if (list == null) - list = []; + if (list == null) list = []; - switch(device) + switch (device) { case Keys: for (input in getActionFromControl(control).inputs) { - if (input.device == KEYBOARD) - list.push(input.inputID); + if (input.device == KEYBOARD) list.push(input.inputID); } case Gamepad(id): for (input in getActionFromControl(control).inputs) { - if (isGamepad(input, id)) - list.push(input.inputID); + if (isGamepad(input, id)) list.push(input.inputID); } } return list; @@ -970,7 +1058,7 @@ class Controls extends FlxActionSet public function removeDevice(device:Device) { - switch(device) + switch (device) { case Keys: setKeyboardScheme(None); @@ -984,26 +1072,32 @@ class Controls extends FlxActionSet * An EMPTY array means the control is uninitialized and needs to be reset to default. * An array with a single FlxKey.NONE means the control was intentionally unbound by the user. */ - public function fromSaveData(data:Dynamic, device:Device) + public function fromSaveData(data:Dynamic, device:Device):Void { for (control in Control.createAll()) { var inputs:Array = Reflect.field(data, control.getName()); + inputs = inputs?.distinct(); if (inputs != null) { - if (inputs.length == 0) { + if (inputs.length == 0) + { trace('Control ${control} is missing bindings, resetting to default.'); - switch(device) + switch (device) { case Keys: bindKeys(control, getDefaultKeybinds(Solo, control)); case Gamepad(id): bindButtons(control, id, getDefaultGamepadBinds(control)); } - } else if (inputs == [FlxKey.NONE]) { + } + else if (inputs == [FlxKey.NONE]) + { trace('Control ${control} is unbound, leaving it be.'); - } else { - switch(device) + } + else + { + switch (device) { case Keys: bindKeys(control, inputs.copy()); @@ -1011,9 +1105,11 @@ class Controls extends FlxActionSet bindButtons(control, id, inputs.copy()); } } - } else { + } + else + { trace('Control ${control} is missing bindings, resetting to default.'); - switch(device) + switch (device) { case Keys: bindKeys(control, getDefaultKeybinds(Solo, control)); @@ -1038,7 +1134,14 @@ class Controls extends FlxActionSet var inputs = getInputsFor(control, device); isEmpty = isEmpty && inputs.length == 0; - if (inputs.length == 0) inputs = [FlxKey.NONE]; + if (inputs.length == 0) + { + inputs = [FlxKey.NONE]; + } + else + { + inputs = inputs.distinct(); + } Reflect.setField(data, control.getName(), inputs); } @@ -1048,7 +1151,7 @@ class Controls extends FlxActionSet static function isDevice(input:FlxActionInput, device:Device) { - return switch(device) + return switch (device) { case Keys: input.device == KEYBOARD; case Gamepad(id): isGamepad(input, id); @@ -1080,7 +1183,8 @@ typedef Swipes = * - Combining `pressed` and `released` inputs into one action. * - Filtering by input method (`KEYBOARD`, `MOUSE`, `GAMEPAD`, etc). */ -class FunkinAction extends FlxActionDigital { +class FunkinAction extends FlxActionDigital +{ public var namePressed(default, null):Null; public var nameReleased(default, null):Null; @@ -1097,83 +1201,102 @@ class FunkinAction extends FlxActionDigital { /** * Input checks default to whether the input was just pressed, on any input device. */ - public override function check():Bool { + public override function check():Bool + { return checkFiltered(JUST_PRESSED); } /** * Check whether the input is currently being held. */ - public function checkPressed():Bool { + public function checkPressed():Bool + { return checkFiltered(PRESSED); } /** * Check whether the input is currently being held, and was not held last frame. */ - public function checkJustPressed():Bool { + public function checkJustPressed():Bool + { return checkFiltered(JUST_PRESSED); } /** * Check whether the input is not currently being held. */ - public function checkReleased():Bool { + public function checkReleased():Bool + { return checkFiltered(RELEASED); } /** * Check whether the input is not currently being held, and was held last frame. */ - public function checkJustReleased():Bool { + public function checkJustReleased():Bool + { return checkFiltered(JUST_RELEASED); } /** * Check whether the input is currently being held by a gamepad device. */ - public function checkPressedGamepad():Bool { + public function checkPressedGamepad():Bool + { return checkFiltered(PRESSED, GAMEPAD); } /** * Check whether the input is currently being held by a gamepad device, and was not held last frame. */ - public function checkJustPressedGamepad():Bool { + public function checkJustPressedGamepad():Bool + { return checkFiltered(JUST_PRESSED, GAMEPAD); } /** * Check whether the input is not currently being held by a gamepad device. */ - public function checkReleasedGamepad():Bool { + public function checkReleasedGamepad():Bool + { return checkFiltered(RELEASED, GAMEPAD); } /** * Check whether the input is not currently being held by a gamepad device, and was held last frame. */ - public function checkJustReleasedGamepad():Bool { + public function checkJustReleasedGamepad():Bool + { return checkFiltered(JUST_RELEASED, GAMEPAD); } - public function checkMultiFiltered(?filterTriggers:Array, ?filterDevices:Array):Bool { - if (filterTriggers == null) { + public function checkMultiFiltered(?filterTriggers:Array, ?filterDevices:Array):Bool + { + if (filterTriggers == null) + { filterTriggers = [PRESSED, JUST_PRESSED]; } - if (filterDevices == null) { + if (filterDevices == null) + { filterDevices = []; } // Perform checkFiltered for each combination. - for (i in filterTriggers) { - if (filterDevices.length == 0) { - if (checkFiltered(i)) { + for (i in filterTriggers) + { + if (filterDevices.length == 0) + { + if (checkFiltered(i)) + { return true; } - } else { - for (j in filterDevices) { - if (checkFiltered(i, j)) { + } + else + { + for (j in filterDevices) + { + if (checkFiltered(i, j)) + { return true; } } @@ -1188,52 +1311,56 @@ class FunkinAction extends FlxActionDigital { * @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`). * @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`). */ - public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool { + public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool + { // The normal // Make sure we only update the inputs once per frame. var key = '${filterTrigger}:${filterDevice}'; var cacheEntry = cache.get(key); - if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) { + if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) + { return cacheEntry.value; } // Use a for loop instead so we can remove inputs while iterating. // We don't return early because we need to call check() on ALL inputs. var result = false; - var len = inputs != null ? inputs.length : 0; - for (i in 0...len) - { - var j = len - i - 1; - var input = inputs[j]; + var len = inputs != null ? inputs.length : 0; + for (i in 0...len) + { + var j = len - i - 1; + var input = inputs[j]; // Filter out dead inputs. - if (input.destroyed) - { - inputs.splice(j, 1); - continue; - } + if (input.destroyed) + { + inputs.splice(j, 1); + continue; + } // Update the input. input.update(); // Check whether the input is the right trigger. - if (filterTrigger != null && input.trigger != filterTrigger) { + if (filterTrigger != null && input.trigger != filterTrigger) + { continue; } // Check whether the input is the right device. - if (filterDevice != null && input.device != filterDevice) { + if (filterDevice != null && input.device != filterDevice) + { continue; } // Check whether the input has triggered. - if (input.check(this)) - { - result = true; - } - } + if (input.check(this)) + { + result = true; + } + } // We need to cache this result. cache.set(key, {timestamp: FlxG.game.ticks, value: result}); @@ -1330,12 +1457,12 @@ class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital { var degAngle = FlxAngle.asDegrees(swp.touchAngle); - switch(trigger) + switch (trigger) { case JUST_PRESSED: if (swp.touchLength >= activateLength) { - switch(inputID) + switch (inputID) { case FlxDirectionFlags.UP: if (degAngle >= 45 && degAngle <= 90 + 45) return properTouch(swp); @@ -1379,7 +1506,7 @@ class FlxActionInputDigitalAndroid extends FlxActionInputDigital override public function check(Action:FlxAction):Bool { - returnswitch(trigger) + return switch (trigger) { #if android case PRESSED: FlxG.android.checkStatus(inputID, PRESSED) || FlxG.android.checkStatus(inputID, PRESSED); @@ -1412,14 +1539,18 @@ enum Control UI_RIGHT; UI_DOWN; RESET; - SCREENSHOT; ACCEPT; BACK; PAUSE; - FULLSCREEN; // CUTSCENE CUTSCENE_ADVANCE; - // SCREENSHOT + // FREEPLAY + FREEPLAY_FAVORITE; + FREEPLAY_LEFT; + FREEPLAY_RIGHT; + // WINDOW + WINDOW_SCREENSHOT; + WINDOW_FULLSCREEN; // VOLUME VOLUME_UP; VOLUME_DOWN; @@ -1462,11 +1593,15 @@ enum abstract Action(String) to String from String var BACK = "back"; var PAUSE = "pause"; var RESET = "reset"; - var FULLSCREEN = "fullscreen"; - // SCREENSHOT - var SCREENSHOT = "screenshot"; + // WINDOW + var WINDOW_FULLSCREEN = "window_fullscreen"; + var WINDOW_SCREENSHOT = "window_screenshot"; // CUTSCENE var CUTSCENE_ADVANCE = "cutscene_advance"; + // FREEPLAY + var FREEPLAY_FAVORITE = "freeplay_favorite"; + var FREEPLAY_LEFT = "freeplay_left"; + var FREEPLAY_RIGHT = "freeplay_right"; // VOLUME var VOLUME_UP = "volume_up"; var VOLUME_DOWN = "volume_down"; diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 8d625290d..dd55de23b 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -140,16 +140,36 @@ class HitNoteScriptEvent extends NoteScriptEvent */ public var score:Int; - public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, comboCount:Int = 0):Void + /** + * If the hit causes a combo break. + */ + public var isComboBreak:Bool = false; + + /** + * The time difference when the player hit the note + */ + public var hitDiff:Float = 0; + + /** + * If the hit causes a notesplash + */ + public var doesNotesplash:Bool = false; + + public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, isComboBreak:Bool, comboCount:Int = 0, hitDiff:Float = 0, + doesNotesplash:Bool = false):Void { super(NOTE_HIT, note, healthChange, comboCount, true); this.score = score; this.judgement = judgement; + this.isComboBreak = isComboBreak; + this.doesNotesplash = doesNotesplash; + this.hitDiff = hitDiff; } public override function toString():String { - return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ')'; + return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ', isComboBreak=' + + isComboBreak + ', hitDiff=' + hitDiff + ', doesNotesplash=' + doesNotesplash + ')'; } } diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index e7b128385..c84d5b154 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -71,7 +71,7 @@ class GameOverSubState extends MusicBeatSubState var gameOverMusic:Null = null; /** - * Whether the player has confirmed and prepared to restart the level. + * Whether the player has confirmed and prepared to restart the level or to go back to the freeplay menu. * This means the animation and transition have already started. */ var isEnding:Bool = false; @@ -83,6 +83,8 @@ class GameOverSubState extends MusicBeatSubState var isChartingMode:Bool = false; + var mustNotExit:Bool = false; + var transparent:Bool; static final CAMERA_ZOOM_DURATION:Float = 0.5; @@ -160,6 +162,8 @@ class GameOverSubState extends MusicBeatSubState @:nullSafety(Off) function setCameraTarget():Void { + if (PlayState.instance.isMinimalMode || boyfriend == null) return; + // Assign a camera follow point to the boyfriend's position. cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; @@ -233,15 +237,16 @@ class GameOverSubState extends MusicBeatSubState } // KEYBOARD ONLY: Restart the level when pressing the assigned key. - if (controls.ACCEPT && blueballed) + if (controls.ACCEPT && blueballed && !mustNotExit) { blueballed = false; confirmDeath(); } // KEYBOARD ONLY: Return to the menu when pressing the assigned key. - if (controls.BACK) + if (controls.BACK && !mustNotExit && !isEnding) { + isEnding = true; blueballed = false; PlayState.instance.deathCounter = 0; // PlayState.seenCutscene = false; // old thing... @@ -252,6 +257,7 @@ class GameOverSubState extends MusicBeatSubState this.close(); if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position! PlayState.instance.close(); // This only works because PlayState is a substate! + return; } else if (PlayStatePlaylist.isStoryMode) { diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index fb9d9b4e2..d0c759b16 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -101,6 +101,10 @@ class PauseSubState extends MusicBeatSubState */ static final MUSIC_FINAL_VOLUME:Float = 0.75; + static final CHARTER_FADE_DELAY:Float = 15.0; + + static final CHARTER_FADE_DURATION:Float = 0.75; + /** * Defines which pause music to use. */ @@ -163,6 +167,12 @@ class PauseSubState extends MusicBeatSubState */ var metadataDeaths:FlxText; + /** + * A text object which displays the current song's artist. + * Fades to the charter after a period before fading back. + */ + var metadataArtist:FlxText; + /** * The actual text objects for the menu entries. */ @@ -203,6 +213,8 @@ class PauseSubState extends MusicBeatSubState regenerateMenu(); transitionIn(); + + startCharterTimer(); } /** @@ -222,6 +234,8 @@ class PauseSubState extends MusicBeatSubState public override function destroy():Void { super.destroy(); + charterFadeTween.cancel(); + charterFadeTween = null; pauseMusic.stop(); } @@ -270,16 +284,25 @@ class PauseSubState extends MusicBeatSubState metadata.scrollFactor.set(0, 0); add(metadata); - var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name - Artist'); + var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name'); metadataSong.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); if (PlayState.instance?.currentChart != null) { - metadataSong.text = '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}'; + metadataSong.text = '${PlayState.instance.currentChart.songName}'; } metadataSong.scrollFactor.set(0, 0); metadata.add(metadataSong); - var metadataDifficulty:FlxText = new FlxText(20, 15 + 32, FlxG.width - 40, 'Difficulty: '); + metadataArtist = new FlxText(20, metadataSong.y + 32, FlxG.width - 40, 'Artist: ${Constants.DEFAULT_ARTIST}'); + metadataArtist.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); + if (PlayState.instance?.currentChart != null) + { + metadataArtist.text = 'Artist: ${PlayState.instance.currentChart.songArtist}'; + } + metadataArtist.scrollFactor.set(0, 0); + metadata.add(metadataArtist); + + var metadataDifficulty:FlxText = new FlxText(20, metadataArtist.y + 32, FlxG.width - 40, 'Difficulty: '); metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); if (PlayState.instance?.currentDifficulty != null) { @@ -288,12 +311,12 @@ class PauseSubState extends MusicBeatSubState metadataDifficulty.scrollFactor.set(0, 0); metadata.add(metadataDifficulty); - metadataDeaths = new FlxText(20, 15 + 64, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls'); + metadataDeaths = new FlxText(20, metadataDifficulty.y + 32, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls'); metadataDeaths.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); metadataDeaths.scrollFactor.set(0, 0); metadata.add(metadataDeaths); - metadataPractice = new FlxText(20, 15 + 96, FlxG.width - 40, 'PRACTICE MODE'); + metadataPractice = new FlxText(20, metadataDeaths.y + 32, FlxG.width - 40, 'PRACTICE MODE'); metadataPractice.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false; metadataPractice.scrollFactor.set(0, 0); @@ -302,6 +325,62 @@ class PauseSubState extends MusicBeatSubState updateMetadataText(); } + var charterFadeTween:Null = null; + + function startCharterTimer():Void + { + charterFadeTween = FlxTween.tween(metadataArtist, {alpha: 0.0}, CHARTER_FADE_DURATION, + { + startDelay: CHARTER_FADE_DELAY, + ease: FlxEase.quartOut, + onComplete: (_) -> { + if (PlayState.instance?.currentChart != null) + { + metadataArtist.text = 'Charter: ${PlayState.instance.currentChart.charter ?? 'Unknown'}'; + } + else + { + metadataArtist.text = 'Charter: ${Constants.DEFAULT_CHARTER}'; + } + + FlxTween.tween(metadataArtist, {alpha: 1.0}, CHARTER_FADE_DURATION, + { + ease: FlxEase.quartOut, + onComplete: (_) -> { + startArtistTimer(); + } + }); + } + }); + } + + function startArtistTimer():Void + { + charterFadeTween = FlxTween.tween(metadataArtist, {alpha: 0.0}, CHARTER_FADE_DURATION, + { + startDelay: CHARTER_FADE_DELAY, + ease: FlxEase.quartOut, + onComplete: (_) -> { + if (PlayState.instance?.currentChart != null) + { + metadataArtist.text = 'Artist: ${PlayState.instance.currentChart.songArtist}'; + } + else + { + metadataArtist.text = 'Artist: ${Constants.DEFAULT_ARTIST}'; + } + + FlxTween.tween(metadataArtist, {alpha: 1.0}, CHARTER_FADE_DURATION, + { + ease: FlxEase.quartOut, + onComplete: (_) -> { + startCharterTimer(); + } + }); + } + }); + } + /** * Perform additional animations to transition the pause menu in when it is first displayed. */ @@ -370,13 +449,14 @@ class PauseSubState extends MusicBeatSubState */ function changeSelection(change:Int = 0):Void { - FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); - + var prevEntry:Int = currentEntry; currentEntry += change; if (currentEntry < 0) currentEntry = currentMenuEntries.length - 1; if (currentEntry >= currentMenuEntries.length) currentEntry = 0; + if (currentEntry != prevEntry) FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); + for (entryIndex in 0...currentMenuEntries.length) { var isCurrent:Bool = entryIndex == currentEntry; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 0ba4e17ec..f55cef388 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -175,6 +175,12 @@ class PlayState extends MusicBeatSubState */ public var currentVariation:String = Constants.DEFAULT_VARIATION; + /** + * The currently selected instrumental ID. + * @default `''` + */ + public var currentInstrumental:String = ''; + /** * The currently active Stage. This is the object containing all the props. */ @@ -236,6 +242,11 @@ class PlayState extends MusicBeatSubState */ public var cameraZoomTween:FlxTween; + /** + * An FlxTween that changes the additive speed to the desired amount. + */ + public var scrollSpeedTweens:Array = []; + /** * The camera follow point from the last stage. * Used to persist the position of the `cameraFollowPosition` between levels. @@ -598,6 +609,7 @@ class PlayState extends MusicBeatSubState currentSong = params.targetSong; if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty; if (params.targetVariation != null) currentVariation = params.targetVariation; + if (params.targetInstrumental != null) currentInstrumental = params.targetInstrumental; isPracticeMode = params.practiceMode ?? false; isBotPlayMode = params.botPlayMode ?? false; isMinimalMode = params.minimalMode ?? false; @@ -772,19 +784,19 @@ class PlayState extends MusicBeatSubState var message:String = 'There was a critical error. Click OK to return to the main menu.'; if (currentSong == null) { - message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.'; + message = 'There was a critical error loading this song\'s chart. Click OK to return to the main menu.'; } else if (currentDifficulty == null) { - message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.'; + message = 'There was a critical error selecting a difficulty for this song. Click OK to return to the main menu.'; } else if (currentChart == null) { - message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.'; + message = 'There was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.'; } else if (currentChart.notes == null) { - message = 'The was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.'; + message = 'There was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.'; } // Display a popup. This blocks the application until the user clicks OK. @@ -822,10 +834,14 @@ class PlayState extends MusicBeatSubState { if (!assertChartExists()) return; + prevScrollTargets = []; + dispatchEvent(new ScriptEvent(SONG_RETRY)); resetCamera(); + var fromDeathState = isPlayerDying; + persistentUpdate = true; persistentDraw = true; @@ -863,8 +879,11 @@ class PlayState extends MusicBeatSubState if (currentStage != null) currentStage.resetStage(); - playerStrumline.vwooshNotes(); - opponentStrumline.vwooshNotes(); + if (!fromDeathState) + { + playerStrumline.vwooshNotes(); + opponentStrumline.vwooshNotes(); + } playerStrumline.clean(); opponentStrumline.clean(); @@ -1075,6 +1094,25 @@ class PlayState extends MusicBeatSubState function moveToGameOver():Void { + // Reset and update a bunch of values in advance for the transition back from the game over substate. + playerStrumline.clean(); + opponentStrumline.clean(); + + songScore = 0; + updateScoreText(); + + health = Constants.HEALTH_STARTING; + healthLerp = health; + + healthBar.value = healthLerp; + + if (!isMinimalMode) + { + iconP1.updatePosition(); + iconP2.updatePosition(); + } + + // Transition to the game over substate. var gameOverSubState = new GameOverSubState( { isChartingMode: isChartingMode, @@ -1180,6 +1218,18 @@ class PlayState extends MusicBeatSubState cameraTweensPausedBySubState.add(cameraZoomTween); } + // Pause camera follow + FlxG.camera.followLerp = 0; + + for (tween in scrollSpeedTweens) + { + if (tween != null && tween.active) + { + tween.active = false; + cameraTweensPausedBySubState.add(tween); + } + } + // Pause the countdown. Countdown.pauseCountdown(); } @@ -1215,6 +1265,9 @@ class PlayState extends MusicBeatSubState } cameraTweensPausedBySubState.clear(); + // Resume camera follow + FlxG.camera.followLerp = Constants.DEFAULT_CAMERA_FOLLOW_RATE; + if (currentConversation != null) { currentConversation.resumeMusic(); @@ -1706,12 +1759,7 @@ class PlayState extends MusicBeatSubState */ function initStrumlines():Void { - var noteStyleId:String = switch (currentStageId) - { - case 'school': 'pixel'; - case 'schoolEvil': 'pixel'; - default: Constants.DEFAULT_NOTE_STYLE; - } + var noteStyleId:String = currentChart.noteStyle; var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); @@ -1933,7 +1981,7 @@ class PlayState extends MusicBeatSubState if (!overrideMusic && !isGamePaused && currentChart != null) { - currentChart.playInst(1.0, false); + currentChart.playInst(1.0, currentInstrumental, false); } if (FlxG.sound.music == null) @@ -2080,7 +2128,8 @@ class PlayState extends MusicBeatSubState // Call an event to allow canceling the note hit. // NOTE: This is what handles the character animations! - var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0); + + var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', false, 0); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2176,7 +2225,7 @@ class PlayState extends MusicBeatSubState // Call an event to allow canceling the note hit. // NOTE: This is what handles the character animations! - var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0); + var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', false, 0); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2234,11 +2283,20 @@ class PlayState extends MusicBeatSubState if (holdNote == null || !holdNote.alive) continue; // While the hold note is being hit, and there is length on the hold note... - if (!isBotPlayMode && holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0) + if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0) { // Grant the player health. - health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed; - songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed); + if (!isBotPlayMode) + { + health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed; + songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed); + } + + // Make sure the player keeps singing while the note is held by the bot. + if (isBotPlayMode && currentStage != null && currentStage.getBoyfriend() != null && currentStage.getBoyfriend().isSinging()) + { + currentStage.getBoyfriend().holdTimer = 0; + } } if (holdNote.missedNote && !holdNote.handledMiss) @@ -2298,8 +2356,6 @@ class PlayState extends MusicBeatSubState var notesInRange:Array = playerStrumline.getNotesMayHit(); var holdNotesInRange:Array = playerStrumline.getHoldNotesHitOrMissed(); - // If there are notes in range, pressing a key will cause a ghost miss. - var notesByDirection:Array> = [[], [], [], []]; for (note in notesInRange) @@ -2321,17 +2377,27 @@ class PlayState extends MusicBeatSubState // Play the strumline animation. playerStrumline.playPress(input.noteDirection); + trace('PENALTY Score: ${songScore}'); } - else if (Constants.GHOST_TAPPING && (holdNotesInRange.length + notesInRange.length > 0) && notesInDirection.length == 0) + else if (Constants.GHOST_TAPPING && (!playerStrumline.mayGhostTap()) && notesInDirection.length == 0) { - // Pressed a wrong key with no notes nearby AND with notes in a different direction available. + // 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) + else if (notesInDirection.length == 0) + { + // Press a key with no penalty. + + // Play the strumline animation. + playerStrumline.playPress(input.noteDirection); + trace('NO PENALTY Score: ${songScore}'); + } + else { // Choose the first note, deprioritizing low priority notes. var targetNote:Null = notesInDirection.find((note) -> !note.lowPriority); @@ -2341,17 +2407,13 @@ class PlayState extends MusicBeatSubState // Judge and hit the note. trace('Hit note! ${targetNote.noteData}'); goodNoteHit(targetNote, input); + trace('Score: ${songScore}'); notesInDirection.remove(targetNote); // Play the strumline animation. playerStrumline.playConfirm(input.noteDirection); } - else - { - // Play the strumline animation. - playerStrumline.playPress(input.noteDirection); - } } while (inputReleaseQueue.length > 0) @@ -2381,27 +2443,41 @@ class PlayState extends MusicBeatSubState var daRating = Scoring.judgeNote(noteDiff, PBOT1); var healthChange = 0.0; + var isComboBreak = false; switch (daRating) { case 'sick': healthChange = Constants.HEALTH_SICK_BONUS; + isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; case 'good': healthChange = Constants.HEALTH_GOOD_BONUS; + isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; case 'bad': healthChange = Constants.HEALTH_BAD_BONUS; + isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; case 'shit': + isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; healthChange = Constants.HEALTH_SHIT_BONUS; } // Send the note hit event. - var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, Highscore.tallies.combo + 1); + var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, isComboBreak, Highscore.tallies.combo + 1, noteDiff, + daRating == 'sick'); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) return; + Highscore.tallies.totalNotesHit++; + // Display the hit on the strums + playerStrumline.hitNote(note, !isComboBreak); + if (event.doesNotesplash) playerStrumline.playNoteSplash(note.noteData.getDirection()); + if (note.isHoldNote && note.holdNoteSprite != null) playerStrumline.playNoteHoldCover(note.holdNoteSprite); + vocals.playerVolume = 1; + // Display the combo meter and add the calculation to the score. - popUpScore(note, event.score, event.judgement, event.healthChange); + applyScore(event.score, event.judgement, event.healthChange, event.isComboBreak); + popUpScore(event.judgement); } /** @@ -2412,9 +2488,6 @@ class PlayState extends MusicBeatSubState { // If we are here, we already CALLED the onNoteMiss script hook! - health += healthChange; - songScore -= 10; - if (!isPracticeMode) { // messy copy paste rn lol @@ -2454,14 +2527,9 @@ class PlayState extends MusicBeatSubState } vocals.playerVolume = 0; - Highscore.tallies.missed++; + if (Highscore.tallies.combo != 0) if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); - if (Highscore.tallies.combo != 0) - { - // Break the combo. - if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); - Highscore.tallies.combo = 0; - } + applyScore(-10, 'miss', healthChange, true); if (playSound) { @@ -2549,20 +2617,12 @@ class PlayState extends MusicBeatSubState // Redirect to the chart editor playing the current song. if (controls.DEBUG_CHART) { - if (isChartingMode) - { - if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position! - this.close(); // This only works because PlayState is a substate! - } - else - { - disableKeys = true; - persistentUpdate = false; - FlxG.switchState(() -> new ChartEditorState( - { - targetSongId: currentSong.id, - })); - } + disableKeys = true; + persistentUpdate = false; + FlxG.switchState(() -> new ChartEditorState( + { + targetSongId: currentSong.id, + })); } #end @@ -2593,46 +2653,24 @@ class PlayState extends MusicBeatSubState } /** - * Handles health, score, and rating popups when a note is hit. + * Handles applying health, score, and ratings. */ - function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void + function applyScore(score:Int, daRating:String, healthChange:Float, isComboBreak:Bool) { - if (daRating == 'miss') - { - // If daRating is 'miss', that means we made a mistake and should not continue. - FlxG.log.warn('popUpScore judged a note as a miss!'); - // TODO: Remove this. - // comboPopUps.displayRating('miss'); - return; - } - - vocals.playerVolume = 1; - - var isComboBreak = false; switch (daRating) { case 'sick': Highscore.tallies.sick += 1; - Highscore.tallies.totalNotesHit++; - isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; case 'good': Highscore.tallies.good += 1; - Highscore.tallies.totalNotesHit++; - isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; case 'bad': Highscore.tallies.bad += 1; - Highscore.tallies.totalNotesHit++; - isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; case 'shit': Highscore.tallies.shit += 1; - Highscore.tallies.totalNotesHit++; - isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; - default: - FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!'); + case 'miss': + Highscore.tallies.missed += 1; } - health += healthChange; - if (isComboBreak) { // Break the combo, but don't increment tallies.misses. @@ -2644,15 +2682,23 @@ class PlayState extends MusicBeatSubState Highscore.tallies.combo++; if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; } - - playerStrumline.hitNote(daNote, !isComboBreak); - - if (daRating == 'sick') - { - playerStrumline.playNoteSplash(daNote.noteData.getDirection()); - } - songScore += score; + } + + /** + * Handles rating popups when a note is hit. + */ + function popUpScore(daRating:String, ?combo:Int):Void + { + if (daRating == 'miss') + { + // If daRating is 'miss', that means we made a mistake and should not continue. + FlxG.log.warn('popUpScore judged a note as a miss!'); + // TODO: Remove this. + // comboPopUps.displayRating('miss'); + return; + } + if (combo == null) combo = Highscore.tallies.combo; if (!isPracticeMode) { @@ -2692,12 +2738,7 @@ class PlayState extends MusicBeatSubState } } comboPopUps.displayRating(daRating); - if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo); - - if (daNote.isHoldNote && daNote.holdNoteSprite != null) - { - playerStrumline.playNoteHoldCover(daNote.holdNoteSprite); - } + if (combo >= 10 || combo == 0) comboPopUps.displayCombo(combo); vocals.playerVolume = 1; } @@ -2784,7 +2825,13 @@ class PlayState extends MusicBeatSubState deathCounter = 0; + // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. + // `easy`, `erect`, `normal-pico`, etc. + var suffixedDifficulty = (currentVariation != Constants.DEFAULT_VARIATION + && currentVariation != 'erect') ? '$currentDifficulty-${currentVariation}' : currentDifficulty; + var isNewHighscore = false; + var prevScoreData:Null = Save.instance.getSongScore(currentSong.id, suffixedDifficulty); if (currentSong != null && currentSong.validScore) { @@ -2804,19 +2851,26 @@ class PlayState extends MusicBeatSubState totalNotesHit: Highscore.tallies.totalNotesHit, totalNotes: Highscore.tallies.totalNotes, }, - accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, }; // adds current song data into the tallies for the level (story levels) Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel); - if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) + if (!isPracticeMode && !isBotPlayMode) { - Save.instance.setSongScore(currentSong.id, currentDifficulty, data); - #if newgrounds - NGio.postScore(score, currentSong.id); - #end - isNewHighscore = true; + isNewHighscore = Save.instance.isSongHighScore(currentSong.id, suffixedDifficulty, data); + + // If no high score is present, save both score and rank. + // If score or rank are better, save the highest one. + // If neither are higher, nothing will change. + Save.instance.applySongRank(currentSong.id, suffixedDifficulty, data); + + if (isNewHighscore) + { + #if newgrounds + NGio.postScore(score, currentSong.id); + #end + } } } @@ -2841,7 +2895,7 @@ class PlayState extends MusicBeatSubState score: PlayStatePlaylist.campaignScore, tallies: { - // TODO: Sum up the values for the whole level! + // TODO: Sum up the values for the whole week! sick: 0, good: 0, bad: 0, @@ -2852,7 +2906,6 @@ class PlayState extends MusicBeatSubState totalNotesHit: 0, totalNotes: 0, }, - accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, }; if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data)) @@ -2938,11 +2991,11 @@ class PlayState extends MusicBeatSubState { if (rightGoddamnNow) { - moveToResultsScreen(isNewHighscore); + moveToResultsScreen(isNewHighscore, prevScoreData); } else { - zoomIntoResultsScreen(isNewHighscore); + zoomIntoResultsScreen(isNewHighscore, prevScoreData); } } } @@ -3016,15 +3069,16 @@ class PlayState extends MusicBeatSubState /** * Play the camera zoom animation and then move to the results screen once it's done. */ - function zoomIntoResultsScreen(isNewHighscore:Bool):Void + function zoomIntoResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void { trace('WENT TO RESULTS SCREEN!'); // Stop camera zooming on beat. cameraZoomRate = 0; - // Cancel camera tweening if it's active. + // Cancel camera and scroll tweening if it's active. cancelAllCameraTweens(); + cancelScrollSpeedTweens(); // If the opponent is GF, zoom in on the opponent. // Else, if there is no GF, zoom in on BF. @@ -3051,12 +3105,12 @@ class PlayState extends MusicBeatSubState FlxG.camera.targetOffset.x += 20; // Replace zoom animation with a fade out for now. - camGame.fade(FlxColor.BLACK, 0.6); + FlxG.camera.fade(FlxColor.BLACK, 0.6); FlxTween.tween(camHUD, {alpha: 0}, 0.6, { onComplete: function(_) { - moveToResultsScreen(isNewHighscore); + moveToResultsScreen(isNewHighscore, prevScoreData); } }); @@ -3089,7 +3143,7 @@ class PlayState extends MusicBeatSubState /** * Move to the results screen right goddamn now. */ - function moveToResultsScreen(isNewHighscore:Bool):Void + function moveToResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void { persistentUpdate = false; vocals.stop(); @@ -3100,7 +3154,10 @@ class PlayState extends MusicBeatSubState var res:ResultState = new ResultState( { storyMode: PlayStatePlaylist.isStoryMode, + songId: currentChart.song.id, + difficultyId: currentDifficulty, title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), + prevScoreData: prevScoreData, scoreData: { score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore, @@ -3116,11 +3173,10 @@ class PlayState extends MusicBeatSubState totalNotesHit: talliesToUse.totalNotesHit, totalNotes: talliesToUse.totalNotes, }, - accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, }, isNewHighscore: isNewHighscore }); - res.camera = camHUD; + this.persistentDraw = false; openSubState(res); } @@ -3144,7 +3200,7 @@ class PlayState extends MusicBeatSubState cancelAllCameraTweens(); } - FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04); + FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE); FlxG.camera.targetOffset.set(); if (resetZoom) @@ -3244,6 +3300,60 @@ class PlayState extends MusicBeatSubState cancelCameraZoomTween(); } + var prevScrollTargets:Array = []; // used to snap scroll speed when things go unruely + + /** + * 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. + cancelScrollSpeedTweens(); + + // Snap to previous event value to prevent the tween breaking when another event cancels the previous tween. + for (i in prevScrollTargets) + { + var value:Float = i[0]; + var strum:Strumline = Reflect.getProperty(this, i[1]); + strum.scrollSpeed = value; + } + + // for next event, clean array. + prevScrollTargets = []; + + for (i in strumlines) + { + var value:Float = speed; + var strum:Strumline = Reflect.getProperty(this, i); + + if (duration == 0) + { + strum.scrollSpeed = value; + } + else + { + scrollSpeedTweens.push(FlxTween.tween(strum, + { + 'scrollSpeed': value + }, duration, {ease: ease})); + } + // make sure charts dont break if the charter is dumb and stupid + prevScrollTargets.push([value, i]); + } + } + + public function cancelScrollSpeedTweens() + { + for (tween in scrollSpeedTweens) + { + if (tween != null) + { + tween.cancel(); + } + } + scrollSpeedTweens = []; + } + #if (debug || FORCE_DEBUG_VERSION) /** * Jumps forward or backward a number of sections in the song. diff --git a/source/funkin/play/ResultScore.hx b/source/funkin/play/ResultScore.hx index d5d5a6567..23e6c8d32 100644 --- a/source/funkin/play/ResultScore.hx +++ b/source/funkin/play/ResultScore.hx @@ -2,11 +2,16 @@ package funkin.play; import flixel.FlxSprite; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.tweens.FlxTween; +import flixel.util.FlxTimer; +import flixel.tweens.FlxEase; class ResultScore extends FlxTypedSpriteGroup { public var scoreShit(default, set):Int = 0; + public var scoreStart:Int = 0; + function set_scoreShit(val):Int { if (group == null || group.members == null) return val; @@ -16,7 +21,8 @@ class ResultScore extends FlxTypedSpriteGroup while (dumbNumb > 0) { - group.members[loopNum].digit = dumbNumb % 10; + scoreStart += 1; + group.members[loopNum].finalDigit = dumbNumb % 10; // var funnyNum = group.members[loopNum]; // prevNum = group.members[loopNum + 1]; @@ -44,9 +50,15 @@ class ResultScore extends FlxTypedSpriteGroup public function animateNumbers():Void { - for (i in group.members) + for (i in group.members.length-scoreStart...group.members.length) { - i.playAnim(); + // if(i.finalDigit == 10) continue; + + new FlxTimer().start((i-1)/24, _ -> { + group.members[i].finalDelay = scoreStart - (i-1); + group.members[i].playAnim(); + group.members[i].shuffle(); + }); } } @@ -71,12 +83,26 @@ class ResultScore extends FlxTypedSpriteGroup class ScoreNum extends FlxSprite { public var digit(default, set):Int = 10; + public var finalDigit(default, set):Int = 10; + public var glow:Bool = true; + + function set_finalDigit(val):Int + { + animation.play('GONE', true, false, 0); + + return finalDigit = val; + } function set_digit(val):Int { if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val]) { - animation.play(numToString[val], true, false, 0); + if(glow){ + animation.play(numToString[val], true, false, 0); + glow = false; + }else{ + animation.play(numToString[val], true, false, 4); + } updateHitbox(); switch (val) @@ -107,6 +133,10 @@ class ScoreNum extends FlxSprite animation.play(numToString[digit], true, false, 0); } + public var shuffleTimer:FlxTimer; + public var finalTween:FlxTween; + public var finalDelay:Float = 0; + public var baseY:Float = 0; public var baseX:Float = 0; @@ -114,6 +144,47 @@ class ScoreNum extends FlxSprite "ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "DISABLED" ]; + function finishShuffleTween():Void{ + + var tweenFunction = function(x) { + var digitRounded = Math.floor(x); + //if(digitRounded == finalDigit) glow = true; + digit = digitRounded; + }; + + finalTween = FlxTween.num(0.0, finalDigit, 23/24, { + ease: FlxEase.quadOut, + onComplete: function (input) { + new FlxTimer().start((finalDelay)/24, _ -> { + animation.play(animation.curAnim.name, true, false, 0); + }); + // fuck + } + }, tweenFunction); + } + + + function shuffleProgress(shuffleTimer:FlxTimer):Void + { + var tempDigit:Int = digit; + tempDigit += 1; + if(tempDigit > 9) tempDigit = 0; + if(tempDigit < 0) tempDigit = 0; + digit = tempDigit; + + if (shuffleTimer.loops > 0 && shuffleTimer.loopsLeft == 0) + { + //digit = finalDigit; + finishShuffleTween(); + } + } + + public function shuffle():Void{ + var duration:Float = 41/24; + var interval:Float = 1/24; + shuffleTimer = new FlxTimer().start(interval, shuffleProgress, Std.int(duration / interval)); + } + public function new(x:Float, y:Float) { super(x, y); @@ -130,6 +201,7 @@ class ScoreNum extends FlxSprite } animation.addByPrefix('DISABLED', 'DISABLED', 24, false); + animation.addByPrefix('GONE', 'GONE', 24, false); this.digit = 10; diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 56dd1e80f..48fb3b04e 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -5,6 +5,7 @@ import funkin.ui.story.StoryMenuState; import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.FlxSprite; import funkin.graphics.FunkinSprite; +import flixel.effects.FlxFlicker; import flixel.graphics.frames.FlxBitmapFont; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxPoint; @@ -12,163 +13,275 @@ import funkin.ui.MusicBeatSubState; import flixel.math.FlxRect; import flixel.text.FlxBitmapText; import funkin.ui.freeplay.FreeplayScore; +import flixel.text.FlxText; +import flixel.util.FlxColor; import flixel.tweens.FlxEase; +import funkin.graphics.FunkinCamera; import funkin.ui.freeplay.FreeplayState; import flixel.tweens.FlxTween; +import flixel.addons.display.FlxBackdrop; import funkin.audio.FunkinSound; import flixel.util.FlxGradient; import flixel.util.FlxTimer; import funkin.save.Save; +import funkin.play.scoring.Scoring; import funkin.save.Save.SaveScoreData; import funkin.graphics.shaders.LeftMaskShader; import funkin.play.components.TallyCounter; +import funkin.play.components.ClearPercentCounter; /** * The state for the results screen after a song or week is finished. */ +@:nullSafety class ResultState extends MusicBeatSubState { final params:ResultsStateParams; - var resultsVariation:ResultVariations; - var songName:FlxBitmapText; - var difficulty:FlxSprite; + final rank:ScoringRank; + final songName:FlxBitmapText; + final difficulty:FlxSprite; + final clearPercentSmall:ClearPercentCounter; - var maskShaderSongName:LeftMaskShader = new LeftMaskShader(); - var maskShaderDifficulty:LeftMaskShader = new LeftMaskShader(); + final maskShaderSongName:LeftMaskShader = new LeftMaskShader(); + final maskShaderDifficulty:LeftMaskShader = new LeftMaskShader(); + + final resultsAnim:FunkinSprite; + final ratingsPopin:FunkinSprite; + final scorePopin:FunkinSprite; + + final bgFlash:FlxSprite; + + final highscoreNew:FlxSprite; + final score:ResultScore; + + var bfPerfect:Null = null; + var heartsPerfect:Null = null; + var bfExcellent:Null = null; + var bfGreat:Null = null; + var gfGreat:Null = null; + var bfGood:Null = null; + var gfGood:Null = null; + var bfShit:Null = null; + + var rankBg:FunkinSprite; + final cameraBG:FunkinCamera; + final cameraScroll:FunkinCamera; + final cameraEverything:FunkinCamera; public function new(params:ResultsStateParams) { super(); this.params = params; - } - override function create():Void - { - /* - if (params.scoreData.sick == params.scoreData.totalNotesHit - && params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT; - else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50) - resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! - else - resultsVariation = NORMAL; - */ - resultsVariation = NORMAL; + rank = Scoring.calculateRank(params.scoreData) ?? SHIT; - FunkinSound.playMusic('results$resultsVariation', - { - startingVolume: 1.0, - overrideExisting: true, - restartTrack: true, - loop: resultsVariation != SHIT - }); + cameraBG = new FunkinCamera('resultsBG', 0, 0, FlxG.width, FlxG.height); + cameraScroll = new FunkinCamera('resultsScroll', 0, 0, FlxG.width, FlxG.height); + cameraEverything = new FunkinCamera('resultsEverything', 0, 0, FlxG.width, FlxG.height); - // Reset the camera zoom on the results screen. - FlxG.camera.zoom = 1.0; - - // TEMP-ish, just used to sorta "cache" the 3000x3000 image! - var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem")); - add(cacheBullShit); - - var dumb:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin")); - add(dumb); - - var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90); - bg.scrollFactor.set(); - add(bg); - - var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90); - bgFlash.scrollFactor.set(); - bgFlash.visible = false; - add(bgFlash); - - // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared")); - // bfGfExcellent.visible = false; - // add(bfGfExcellent); - // - // var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared")); - // bfPerfect.visible = false; - // add(bfPerfect); - // - // var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared")); - // bfSHIT.visible = false; - // add(bfSHIT); - // - // bfGfExcellent.anim.onComplete = () -> { - // bfGfExcellent.anim.curFrame = 28; - // bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! - // }; - // - // bfPerfect.anim.onComplete = () -> { - // bfPerfect.anim.curFrame = 136; - // bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! - // }; - // - // bfSHIT.anim.onComplete = () -> { - // bfSHIT.anim.curFrame = 150; - // bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce! - // }; - - var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD'); - gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); - gf.visible = false; - gf.animation.finishCallback = _ -> { - gf.animation.play('clap', true, false, 9); - }; - add(gf); - - var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD'); - boyfriend.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false); - boyfriend.visible = false; - boyfriend.animation.finishCallback = function(_) { - boyfriend.animation.play('fall', true, false, 14); - }; - - add(boyfriend); - - var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem'); - soundSystem.animation.addByPrefix("idle", "sound system", 24, false); - soundSystem.visible = false; - new FlxTimer().start(0.4, _ -> { - soundSystem.animation.play("idle"); - soundSystem.visible = true; - }); - add(soundSystem); - - difficulty = new FlxSprite(555); - - var diffSpr:String = switch (PlayState.instance.currentDifficulty) - { - case 'easy': - 'difEasy'; - case 'normal': - 'difNormal'; - case 'hard': - 'difHard'; - case 'erect': - 'difErect'; - case 'nightmare': - 'difNightmare'; - case _: - 'difNormal'; - } - - difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr)); - add(difficulty); + // We build a lot of this stuff in the constructor, then place it in create(). + // This prevents having to do `null` checks everywhere. var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890"; songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62))); songName.text = params.title; songName.letterSpacing = -15; songName.angle = -4.4; + songName.zIndex = 1000; + + difficulty = new FlxSprite(555); + difficulty.zIndex = 1000; + + clearPercentSmall = new ClearPercentCounter(FlxG.width / 2 + 300, FlxG.height / 2 - 100, 100, true); + clearPercentSmall.zIndex = 1000; + clearPercentSmall.visible = false; + + bgFlash = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFF1A6, 0xFFFFF1BE], 90); + + resultsAnim = FunkinSprite.createSparrow(-200, -10, "resultScreen/results"); + + ratingsPopin = FunkinSprite.createSparrow(-135, 135, "resultScreen/ratingsPopin"); + + scorePopin = FunkinSprite.createSparrow(-180, 515, "resultScreen/scorePopin"); + + highscoreNew = new FlxSprite(44, 557); + + score = new ResultScore(35, 305, 10, params.scoreData.score); + + rankBg = new FunkinSprite(0, 0); + } + + override function create():Void + { + if (FlxG.sound.music != null) FlxG.sound.music.stop(); + + // We need multiple cameras so we can put one at an angle. + cameraScroll.angle = -3.8; + + cameraBG.bgColor = FlxColor.MAGENTA; + cameraScroll.bgColor = FlxColor.TRANSPARENT; + cameraEverything.bgColor = FlxColor.TRANSPARENT; + + FlxG.cameras.add(cameraBG, false); + FlxG.cameras.add(cameraScroll, false); + FlxG.cameras.add(cameraEverything, false); + + FlxG.cameras.setDefaultDrawTarget(cameraEverything, true); + this.camera = cameraEverything; + + // Reset the camera zoom on the results screen. + FlxG.camera.zoom = 1.0; + + var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90); + bg.scrollFactor.set(); + bg.zIndex = 10; + bg.cameras = [cameraBG]; + add(bg); + + bgFlash.scrollFactor.set(); + bgFlash.visible = false; + bgFlash.zIndex = 20; + // bgFlash.cameras = [cameraBG]; + add(bgFlash); + + // The sound system which falls into place behind the score text. Plays every time! + var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem'); + soundSystem.animation.addByPrefix("idle", "sound system", 24, false); + soundSystem.visible = false; + new FlxTimer().start(8 / 24, _ -> { + soundSystem.animation.play("idle"); + soundSystem.visible = true; + }); + soundSystem.zIndex = 1100; + add(soundSystem); + + switch (rank) + { + case PERFECT | PERFECT_GOLD: + heartsPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT/hearts", "shared")); + heartsPerfect.visible = false; + heartsPerfect.zIndex = 501; + add(heartsPerfect); + + heartsPerfect.anim.onComplete = () -> { + if (heartsPerfect != null) + { + // bfPerfect.anim.curFrame = 137; + heartsPerfect.anim.curFrame = 43; + heartsPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + } + }; + + bfPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared")); + bfPerfect.visible = false; + bfPerfect.zIndex = 500; + add(bfPerfect); + + bfPerfect.anim.onComplete = () -> { + if (bfPerfect != null) + { + // bfPerfect.anim.curFrame = 137; + bfPerfect.anim.curFrame = 137; + bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + } + }; + + case EXCELLENT: + bfExcellent = new FlxAtlasSprite(1329, 429, Paths.animateAtlas("resultScreen/results-bf/resultsEXCELLENT", "shared")); + bfExcellent.visible = false; + bfExcellent.zIndex = 500; + add(bfExcellent); + + bfExcellent.anim.onComplete = () -> { + if (bfExcellent != null) + { + bfExcellent.anim.curFrame = 28; + bfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! + } + }; + + case GREAT: + gfGreat = new FlxAtlasSprite(802, 331, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/gf", "shared")); + gfGreat.visible = false; + gfGreat.zIndex = 499; + add(gfGreat); + + gfGreat.scale.set(0.93, 0.93); + + gfGreat.anim.onComplete = () -> { + if (gfGreat != null) + { + gfGreat.anim.curFrame = 9; + gfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce! + } + }; + + bfGreat = new FlxAtlasSprite(929, 363, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/bf", "shared")); + bfGreat.visible = false; + bfGreat.zIndex = 500; + add(bfGreat); + + bfGreat.scale.set(0.93, 0.93); + + bfGreat.anim.onComplete = () -> { + if (bfGreat != null) + { + bfGreat.anim.curFrame = 15; + bfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce! + } + }; + + case GOOD: + gfGood = FunkinSprite.createSparrow(625, 325, 'resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD'); + gfGood.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); + gfGood.visible = false; + gfGood.zIndex = 500; + gfGood.animation.finishCallback = _ -> { + if (gfGood != null) + { + gfGood.animation.play('clap', true, false, 9); + } + }; + add(gfGood); + + bfGood = FunkinSprite.createSparrow(640, -200, 'resultScreen/results-bf/resultsGOOD/resultBoyfriendGOOD'); + bfGood.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false); + bfGood.visible = false; + bfGood.zIndex = 501; + bfGood.animation.finishCallback = function(_) { + if (bfGood != null) + { + bfGood.animation.play('fall', true, false, 14); + } + }; + add(bfGood); + + case SHIT: + bfShit = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/results-bf/resultsSHIT", "shared")); + bfShit.visible = false; + bfShit.zIndex = 500; + add(bfShit); + bfShit.onAnimationFinish.add((animName) -> { + if (bfShit != null) + { + bfShit.playAnimation('Loop Start'); + } + }); + } + + var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}'; + difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr)); + add(difficulty); + add(songName); var angleRad = songName.angle * Math.PI / 180; speedOfTween.x = -1.0 * Math.cos(angleRad); speedOfTween.y = -1.0 * Math.sin(angleRad); - timerThenSongName(); + timerThenSongName(1.0, false); songName.shader = maskShaderSongName; difficulty.shader = maskShaderDifficulty; @@ -178,35 +291,77 @@ class ResultState extends MusicBeatSubState var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack")); blackTopBar.y = -blackTopBar.height; - FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5}); + FlxTween.tween(blackTopBar, {y: 0}, 7 / 24, {ease: FlxEase.quartOut, startDelay: 3 / 24}); + blackTopBar.zIndex = 1010; add(blackTopBar); - var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results"); resultsAnim.animation.addByPrefix("result", "results instance 1", 24, false); - resultsAnim.animation.play("result"); + resultsAnim.visible = false; + resultsAnim.zIndex = 1200; add(resultsAnim); + new FlxTimer().start(6 / 24, _ -> { + resultsAnim.visible = true; + resultsAnim.animation.play("result"); + }); - var ratingsPopin:FunkinSprite = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin"); ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false); ratingsPopin.visible = false; + ratingsPopin.zIndex = 1200; add(ratingsPopin); + new FlxTimer().start(21 / 24, _ -> { + ratingsPopin.visible = true; + ratingsPopin.animation.play("idle"); + }); - var scorePopin:FunkinSprite = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin"); scorePopin.animation.addByPrefix("score", "tally score", 24, false); scorePopin.visible = false; + scorePopin.zIndex = 1200; add(scorePopin); + new FlxTimer().start(36 / 24, _ -> { + scorePopin.visible = true; + scorePopin.animation.play("score"); + scorePopin.animation.finishCallback = anim -> {}; + }); + + new FlxTimer().start(37 / 24, _ -> { + score.visible = true; + score.animateNumbers(); + startRankTallySequence(); + }); + + new FlxTimer().start(rank.getBFDelay(), _ -> { + afterRankTallySequence(); + }); + + new FlxTimer().start(rank.getFlashDelay(), _ -> { + displayRankText(); + }); - var highscoreNew:FlxSprite = new FlxSprite(310, 570); highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew"); - highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24); + highscoreNew.animation.addByPrefix("new", "highscoreAnim0", 24, false); highscoreNew.visible = false; - highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8)); + // highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8)); highscoreNew.updateHitbox(); + highscoreNew.zIndex = 1200; add(highscoreNew); + new FlxTimer().start(rank.getHighscoreDelay(), _ -> { + if (params.isNewHighscore ?? false) + { + highscoreNew.visible = true; + highscoreNew.animation.play("new"); + highscoreNew.animation.finishCallback = _ -> highscoreNew.animation.play("new", true, false, 16); + } + else + { + highscoreNew.visible = false; + } + }); + var hStuf:Int = 50; var ratingGrp:FlxTypedGroup = new FlxTypedGroup(); + ratingGrp.zIndex = 1200; add(ratingGrp); /** @@ -220,7 +375,10 @@ class ResultState extends MusicBeatSubState ratingGrp.add(maxCombo); hStuf += 2; - var extraYOffset:Float = 5; + var extraYOffset:Float = 7; + + hStuf += 2; + var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E); ratingGrp.add(tallySick); @@ -236,83 +394,289 @@ class ResultState extends MusicBeatSubState var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6); ratingGrp.add(tallyMissed); - var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score); score.visible = false; + score.zIndex = 1200; add(score); for (ind => rating in ratingGrp.members) { rating.visible = false; - new FlxTimer().start((0.3 * ind) + 0.55, _ -> { + new FlxTimer().start((0.3 * ind) + 1.20, _ -> { rating.visible = true; FlxTween.tween(rating, {curNumber: rating.neededNumber}, 0.5, {ease: FlxEase.quartOut}); }); } - new FlxTimer().start(0.5, _ -> { - ratingsPopin.animation.play("idle"); - ratingsPopin.visible = true; + // if (params.isNewHighscore ?? false) + // { + // highscoreNew.visible = true; + // highscoreNew.animation.play("new"); + // //FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); + // } + // else + // { + // highscoreNew.visible = false; + // } + + new FlxTimer().start(rank.getMusicDelay(), _ -> { + if (rank.hasMusicIntro()) + { + // Play the intro music. + var introMusic:String = Paths.music(rank.getMusicPath() + '/' + rank.getMusicPath() + '-intro'); + FunkinSound.load(introMusic, 1.0, false, true, true, () -> { + FunkinSound.playMusic(rank.getMusicPath(), + { + startingVolume: 1.0, + overrideExisting: true, + restartTrack: true, + loop: rank.shouldMusicLoop() + }); + }); + } + else + { + FunkinSound.playMusic(rank.getMusicPath(), + { + startingVolume: 1.0, + overrideExisting: true, + restartTrack: true, + loop: rank.shouldMusicLoop() + }); + } + }); + + rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xFF000000); + rankBg.zIndex = 99999; + add(rankBg); + + rankBg.alpha = 0; + + refresh(); + + super.create(); + } + + var rankTallyTimer:Null = null; + var clearPercentTarget:Int = 100; + var clearPercentLerp:Int = 0; + + function startRankTallySequence():Void + { + bgFlash.visible = true; + FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24); + var clearPercentFloat = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100; + clearPercentTarget = Math.floor(clearPercentFloat); + // Prevent off-by-one errors. + + clearPercentLerp = Std.int(Math.max(0, clearPercentTarget - 36)); + + trace('Clear percent target: ' + clearPercentFloat + ', round: ' + clearPercentTarget); + + var clearPercentCounter:ClearPercentCounter = new ClearPercentCounter(FlxG.width / 2 + 190, FlxG.height / 2 - 70, clearPercentLerp); + FlxTween.tween(clearPercentCounter, {curNumber: clearPercentTarget}, 58 / 24, + { + ease: FlxEase.quartOut, + onUpdate: _ -> { + // Only play the tick sound if the number increased. + if (clearPercentLerp != clearPercentCounter.curNumber) + { + clearPercentLerp = clearPercentCounter.curNumber; + FunkinSound.playOnce(Paths.sound('scrollMenu')); + } + }, + onComplete: _ -> { + // Play confirm sound. + FunkinSound.playOnce(Paths.sound('confirmMenu')); + + // Just to be sure that the lerp didn't mess things up. + clearPercentCounter.curNumber = clearPercentTarget; + + clearPercentCounter.flash(true); + new FlxTimer().start(0.4, _ -> { + clearPercentCounter.flash(false); + }); + + // displayRankText(); + + // previously 2.0 seconds + new FlxTimer().start(0.25, _ -> { + FlxTween.tween(clearPercentCounter, {alpha: 0}, 0.5, + { + startDelay: 0.5, + ease: FlxEase.quartOut, + onComplete: _ -> { + remove(clearPercentCounter); + } + }); + + // afterRankTallySequence(); + }); + } + }); + clearPercentCounter.zIndex = 450; + add(clearPercentCounter); + + if (ratingsPopin == null) + { + trace("Could not build ratingsPopin!"); + } + else + { + // ratingsPopin.animation.play("idle"); + // ratingsPopin.visible = true; ratingsPopin.animation.finishCallback = anim -> { - scorePopin.animation.play("score"); - scorePopin.animation.finishCallback = anim -> { - score.visible = true; - score.animateNumbers(); - }; - scorePopin.visible = true; + // scorePopin.animation.play("score"); - if (params.isNewHighscore) + // scorePopin.visible = true; + + if (params.isNewHighscore ?? false) { highscoreNew.visible = true; highscoreNew.animation.play("new"); - FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); } else { highscoreNew.visible = false; } }; + } - switch (resultsVariation) - { - // case SHIT: - // bfSHIT.visible = true; - // bfSHIT.playAnimation(""); - - case NORMAL: - boyfriend.animation.play('fall'); - boyfriend.visible = true; - - new FlxTimer().start((1 / 24) * 12, _ -> { - bgFlash.visible = true; - FlxTween.tween(bgFlash, {alpha: 0}, 0.4); - new FlxTimer().start((1 / 24) * 2, _ -> - { - // bgFlash.alpha = 0.5; - - // bgFlash.visible = false; - }); - }); - - new FlxTimer().start((1 / 24) * 22, _ -> { - // plays about 22 frames (at 24fps timing) after bf spawns in - gf.animation.play('clap', true); - gf.visible = true; - }); - // case PERFECT: - // bfPerfect.visible = true; - // bfPerfect.playAnimation(""); - - // bfGfExcellent.visible = true; - // bfGfExcellent.playAnimation(""); - default: - } - }); - - super.create(); + refresh(); } - function timerThenSongName():Void + function displayRankText():Void + { + bgFlash.visible = true; + bgFlash.alpha = 1; + FlxTween.tween(bgFlash, {alpha: 0}, 14 / 24); + + var rankTextVert:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getVerTextAsset()), Y, 0, 30); + rankTextVert.x = FlxG.width - 44; + rankTextVert.y = 100; + rankTextVert.zIndex = 990; + add(rankTextVert); + + FlxFlicker.flicker(rankTextVert, 2 / 24 * 3, 2 / 24, true); + + // Scrolling. + new FlxTimer().start(30 / 24, _ -> { + rankTextVert.velocity.y = -80; + }); + + for (i in 0...12) + { + var rankTextBack:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getHorTextAsset()), X, 10, 0); + rankTextBack.x = FlxG.width / 2 - 320; + rankTextBack.y = 50 + (135 * i / 2) + 10; + // rankTextBack.angle = -3.8; + rankTextBack.zIndex = 100; + rankTextBack.cameras = [cameraScroll]; + add(rankTextBack); + + // Scrolling. + rankTextBack.velocity.x = (i % 2 == 0) ? -7.0 : 7.0; + } + + refresh(); + } + + function afterRankTallySequence():Void + { + showSmallClearPercent(); + + switch (rank) + { + case PERFECT | PERFECT_GOLD: + if (bfPerfect == null) + { + trace("Could not build PERFECT animation!"); + } + else + { + bfPerfect.visible = true; + bfPerfect.playAnimation(''); + } + new FlxTimer().start(106 / 24, _ -> { + if (heartsPerfect == null) + { + trace("Could not build heartsPerfect animation!"); + } + else + { + heartsPerfect.visible = true; + heartsPerfect.playAnimation(''); + } + }); + case EXCELLENT: + if (bfExcellent == null) + { + trace("Could not build EXCELLENT animation!"); + } + else + { + bfExcellent.visible = true; + bfExcellent.playAnimation(''); + } + case GREAT: + if (bfGreat == null) + { + trace("Could not build GREAT animation!"); + } + else + { + bfGreat.visible = true; + bfGreat.playAnimation(''); + } + + new FlxTimer().start(6 / 24, _ -> { + if (gfGreat == null) + { + trace("Could not build GREAT animation for gf!"); + } + else + { + gfGreat.visible = true; + gfGreat.playAnimation(''); + } + }); + case SHIT: + if (bfShit == null) + { + trace("Could not build SHIT animation!"); + } + else + { + bfShit.visible = true; + bfShit.playAnimation('Intro'); + } + case GOOD: + if (bfGood == null) + { + trace("Could not build GOOD animation!"); + } + else + { + bfGood.animation.play('fall'); + bfGood.visible = true; + new FlxTimer().start((1 / 24) * 22, _ -> { + // plays about 22 frames (at 24fps timing) after bf spawns in + if (gfGood != null) + { + gfGood.animation.play('clap', true); + gfGood.visible = true; + } + else + { + trace("Could not build GOOD animation!"); + } + }); + } + default: + } + } + + function timerThenSongName(timerLength:Float = 3.0, autoScroll:Bool = true):Void { movingSongStuff = false; @@ -323,17 +687,45 @@ class ResultState extends MusicBeatSubState difficulty.y = -difficulty.height; FlxTween.tween(difficulty, {y: diffYTween}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.8}); + if (clearPercentSmall != null) + { + clearPercentSmall.x = (difficulty.x + difficulty.width) + 60; + clearPercentSmall.y = -clearPercentSmall.height; + FlxTween.tween(clearPercentSmall, {y: 122 - 5}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.85}); + } + songName.y = -songName.height; var fuckedupnumber = (10) * (songName.text.length / 15); - FlxTween.tween(songName, {y: diffYTween - 35 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9}); - songName.x = (difficulty.x + difficulty.width) + 20; + FlxTween.tween(songName, {y: diffYTween - 25 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9}); + songName.x = clearPercentSmall.x + 94; - new FlxTimer().start(3, _ -> { + new FlxTimer().start(timerLength, _ -> { var tempSpeed = FlxPoint.get(speedOfTween.x, speedOfTween.y); speedOfTween.set(0, 0); FlxTween.tween(speedOfTween, {x: tempSpeed.x, y: tempSpeed.y}, 0.7, {ease: FlxEase.quadIn}); + movingSongStuff = (autoScroll); + }); + } + + function showSmallClearPercent():Void + { + if (clearPercentSmall != null) + { + add(clearPercentSmall); + clearPercentSmall.visible = true; + clearPercentSmall.flash(true); + new FlxTimer().start(0.4, _ -> { + clearPercentSmall.flash(false); + }); + + clearPercentSmall.curNumber = clearPercentTarget; + clearPercentSmall.zIndex = 1000; + refresh(); + } + + new FlxTimer().start(2.5, _ -> { movingSongStuff = true; }); } @@ -345,11 +737,9 @@ class ResultState extends MusicBeatSubState { super.draw(); - if (songName != null) - { - songName.clipRect = FlxRect.get(Math.max(0, 540 - songName.x), 0, FlxG.width, songName.height); - // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!! - } + songName.clipRect = FlxRect.get(Math.max(0, 520 - songName.x), 0, FlxG.width, songName.height); + + // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!! // if (songName != null && songName.frame != null) // maskShaderSongName.frameUV = songName.frame.uv; @@ -357,6 +747,79 @@ class ResultState extends MusicBeatSubState override function update(elapsed:Float):Void { + // if(FlxG.keys.justPressed.R){ + // FlxG.switchState(() -> new funkin.play.ResultState( + // { + // storyMode: false, + // title: "Cum Song Erect by Kawai Sprite", + // songId: "cum", + // difficultyId: "nightmare", + // isNewHighscore: true, + // scoreData: + // { + // score: 1_234_567, + // tallies: + // { + // sick: 200, + // good: 0, + // bad: 0, + // shit: 0, + // missed: 0, + // combo: 0, + // maxCombo: 69, + // totalNotesHit: 200, + // totalNotes: 200 // 0, + // } + // }, + // })); + // } + + // if(heartsPerfect != null){ + // if (FlxG.keys.justPressed.I) + // { + // heartsPerfect.y -= 1; + // trace(heartsPerfect.x, heartsPerfect.y); + // } + // if (FlxG.keys.justPressed.J) + // { + // heartsPerfect.x -= 1; + // trace(heartsPerfect.x, heartsPerfect.y); + // } + // if (FlxG.keys.justPressed.L) + // { + // heartsPerfect.x += 1; + // trace(heartsPerfect.x, heartsPerfect.y); + // } + // if (FlxG.keys.justPressed.K) + // { + // heartsPerfect.y += 1; + // trace(heartsPerfect.x, heartsPerfect.y); + // } + // } + + // if(bfGreat != null){ + // if (FlxG.keys.justPressed.W) + // { + // bfGreat.y -= 1; + // trace(bfGreat.x, bfGreat.y); + // } + // if (FlxG.keys.justPressed.A) + // { + // bfGreat.x -= 1; + // trace(bfGreat.x, bfGreat.y); + // } + // if (FlxG.keys.justPressed.D) + // { + // bfGreat.x += 1; + // trace(bfGreat.x, bfGreat.y); + // } + // if (FlxG.keys.justPressed.S) + // { + // bfGreat.y += 1; + // trace(bfGreat.x, bfGreat.y); + // } + // } + // maskShaderSongName.swagSprX = songName.x; maskShaderDifficulty.swagSprX = difficulty.x; @@ -364,8 +827,10 @@ class ResultState extends MusicBeatSubState { songName.x += speedOfTween.x; difficulty.x += speedOfTween.x; + clearPercentSmall.x += speedOfTween.x; songName.y += speedOfTween.y; difficulty.y += speedOfTween.y; + clearPercentSmall.y += speedOfTween.y; if (songName.x + songName.width < 100) { @@ -382,20 +847,64 @@ class ResultState extends MusicBeatSubState if (controls.PAUSE) { - FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8); - FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1, - { - onComplete: _ -> { - FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4); - } - }); + if (FlxG.sound.music != null) + { + FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8); + FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1, + { + onComplete: _ -> { + FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4); + } + }); + } if (params.storyMode) { openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker))); } else { - openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker))); + var rigged:Bool = true; + if (rank > Scoring.calculateRank(params?.prevScoreData)) // if (rigged) + { + trace('THE RANK IS Higher.....'); + + FlxTween.tween(rankBg, {alpha: 1}, 0.5, + { + ease: FlxEase.expoOut, + onComplete: function(_) { + FlxG.switchState(FreeplayState.build( + { + { + fromResults: + { + oldRank: Scoring.calculateRank(params?.prevScoreData), + newRank: rank, + songId: params.songId, + difficultyId: params.difficultyId, + playRankAnim: true + } + } + })); + } + }); + } + else + { + trace('rank is lower...... and/or equal'); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build( + { + { + fromResults: + { + oldRank: null, + playRankAnim: false, + newRank: rank, + songId: params.songId, + difficultyId: params.difficultyId + } + } + }, sticker))); + } } } @@ -403,14 +912,6 @@ class ResultState extends MusicBeatSubState } } -enum abstract ResultVariations(String) -{ - var PERFECT; - var EXCELLENT; - var NORMAL; - var SHIT; -} - typedef ResultsStateParams = { /** @@ -423,13 +924,26 @@ typedef ResultsStateParams = */ var title:String; + var songId:String; + /** * Whether the displayed score is a new highscore */ - var isNewHighscore:Bool; + var ?isNewHighscore:Bool; + + /** + * The difficulty ID of the song/week we just played. + * @default Normal + */ + var ?difficultyId:String; /** * The score, accuracy, and judgements. */ var scoreData:SaveScoreData; + + /** + * The previous score data, used for rank comparision. + */ + var ?prevScoreData:SaveScoreData; }; diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 2796f8123..4ef86c6a9 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -420,7 +420,8 @@ class BaseCharacter extends Bopper { if (isSinging()) return; - if (['hey', 'cheer'].contains(getCurrentAnimation()) && !isAnimationFinished()) return; + var currentAnimation:String = getCurrentAnimation(); + if ((currentAnimation == 'hey' || currentAnimation == 'cheer') && !isAnimationFinished()) return; } // Prevent dancing while another animation is playing. @@ -441,19 +442,15 @@ class BaseCharacter extends Bopper switch (player) { case 1: - return [ - PlayerSettings.player1.controls.NOTE_LEFT_P, - PlayerSettings.player1.controls.NOTE_DOWN_P, - PlayerSettings.player1.controls.NOTE_UP_P, - PlayerSettings.player1.controls.NOTE_RIGHT_P, - ].contains(true); + return PlayerSettings.player1.controls.NOTE_LEFT_P + || PlayerSettings.player1.controls.NOTE_DOWN_P + || PlayerSettings.player1.controls.NOTE_UP_P + || PlayerSettings.player1.controls.NOTE_RIGHT_P; case 2: - return [ - PlayerSettings.player2.controls.NOTE_LEFT_P, - PlayerSettings.player2.controls.NOTE_DOWN_P, - PlayerSettings.player2.controls.NOTE_UP_P, - PlayerSettings.player2.controls.NOTE_RIGHT_P, - ].contains(true); + return PlayerSettings.player2.controls.NOTE_LEFT_P + || PlayerSettings.player2.controls.NOTE_DOWN_P + || PlayerSettings.player2.controls.NOTE_UP_P + || PlayerSettings.player2.controls.NOTE_RIGHT_P; } return false; } @@ -469,19 +466,15 @@ class BaseCharacter extends Bopper switch (player) { case 1: - return [ - PlayerSettings.player1.controls.NOTE_LEFT, - PlayerSettings.player1.controls.NOTE_DOWN, - PlayerSettings.player1.controls.NOTE_UP, - PlayerSettings.player1.controls.NOTE_RIGHT, - ].contains(true); + return PlayerSettings.player1.controls.NOTE_LEFT + || PlayerSettings.player1.controls.NOTE_DOWN + || PlayerSettings.player1.controls.NOTE_UP + || PlayerSettings.player1.controls.NOTE_RIGHT; case 2: - return [ - PlayerSettings.player2.controls.NOTE_LEFT, - PlayerSettings.player2.controls.NOTE_DOWN, - PlayerSettings.player2.controls.NOTE_UP, - PlayerSettings.player2.controls.NOTE_RIGHT, - ].contains(true); + return PlayerSettings.player2.controls.NOTE_LEFT + || PlayerSettings.player2.controls.NOTE_DOWN + || PlayerSettings.player2.controls.NOTE_UP + || PlayerSettings.player2.controls.NOTE_RIGHT; } return false; } diff --git a/source/funkin/play/components/ClearPercentCounter.hx b/source/funkin/play/components/ClearPercentCounter.hx new file mode 100644 index 000000000..e3d3795d9 --- /dev/null +++ b/source/funkin/play/components/ClearPercentCounter.hx @@ -0,0 +1,141 @@ +package funkin.play.components; + +import funkin.graphics.FunkinSprite; +import funkin.graphics.shaders.PureColor; +import flixel.FlxSprite; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.math.FlxMath; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.text.FlxText.FlxTextAlign; +import funkin.util.MathUtil; +import flixel.util.FlxColor; + +/** + * Numerical counters used to display the clear percent. + */ +class ClearPercentCounter extends FlxTypedSpriteGroup +{ + public var curNumber(default, set):Int = 0; + + var numberChanged:Bool = false; + + function set_curNumber(val:Int):Int + { + numberChanged = true; + return curNumber = val; + } + + var small:Bool = false; + var flashShader:PureColor; + + public function new(x:Float, y:Float, startingNumber:Int = 0, small:Bool = false) + { + super(x, y); + + flashShader = new PureColor(FlxColor.WHITE); + flashShader.colorSet = false; + + curNumber = startingNumber; + + this.small = small; + + var clearPercentText:FunkinSprite = FunkinSprite.create(0, 0, 'resultScreen/clearPercent/clearPercentText${small ? 'Small' : ''}'); + clearPercentText.x = small ? 40 : 0; + add(clearPercentText); + + drawNumbers(); + } + + /** + * Make the counter flash turn white or stop being all white. + * @param enabled Whether the counter should be white. + */ + public function flash(enabled:Bool):Void + { + flashShader.colorSet = enabled; + } + + var tmr:Float = 0; + + override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (numberChanged) drawNumbers(); + } + + function drawNumbers():Void + { + var seperatedScore:Array = []; + var tempCombo:Int = Math.round(curNumber); + + while (tempCombo != 0) + { + seperatedScore.push(tempCombo % 10); + tempCombo = Math.floor(tempCombo / 10); + } + + if (seperatedScore.length == 0) seperatedScore.push(0); + + seperatedScore.reverse(); + + for (ind => num in seperatedScore) + { + var digitIndex:Int = ind + 1; + // If there's only one digit, move it to the right + // If there's three digits, move them all to the left + var digitOffset = (seperatedScore.length == 1) ? 1 : (seperatedScore.length == 3) ? -1 : 0; + var digitSize = small ? 32 : 72; + var digitHeightOffset = small ? -4 : 0; + + var xPos = (digitIndex - 1 + digitOffset) * (digitSize * this.scale.x); + xPos += small ? -24 : 0; + var yPos = (digitIndex - 1 + digitOffset) * (digitHeightOffset * this.scale.y); + yPos += small ? 0 : 72; + + if (digitIndex >= members.length) + { + // Three digits = LLR because the 1 and 0 won't be the same anyway. + var variant:Bool = (seperatedScore.length == 3) ? (digitIndex >= 2) : (digitIndex >= 1); + // var variant:Bool = (seperatedScore.length % 2 != 0) ? (digitIndex % 2 == 0) : (digitIndex % 2 == 1); + var numb:ClearPercentNumber = new ClearPercentNumber(xPos, yPos, num, variant, this.small); + numb.scale.set(this.scale.x, this.scale.y); + numb.shader = flashShader; + numb.visible = true; + add(numb); + } + else + { + members[digitIndex].animation.play(Std.string(num)); + // Reset the position of the number + members[digitIndex].x = xPos + this.x; + members[digitIndex].y = yPos + this.y; + members[digitIndex].visible = true; + } + } + for (ind in (seperatedScore.length + 1)...(members.length)) + { + members[ind].visible = false; + } + } +} + +class ClearPercentNumber extends FlxSprite +{ + public function new(x:Float, y:Float, digit:Int, variant:Bool, small:Bool) + { + super(x, y); + + frames = Paths.getSparrowAtlas('resultScreen/clearPercent/clearPercentNumber${small ? 'Small' : variant ? 'Right' : 'Left'}'); + + for (i in 0...10) + { + animation.addByPrefix('$i', 'number $i 0', 24, false); + } + + animation.play('$digit'); + updateHitbox(); + } +} diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx index 957daa43c..2442b0dc5 100644 --- a/source/funkin/play/components/HealthIcon.hx +++ b/source/funkin/play/components/HealthIcon.hx @@ -24,7 +24,7 @@ import funkin.util.MathUtil; * - i.e. `PlayState.instance.iconP1.playAnimation("losing")` * - Scripts can also utilize all functionality that a normal FlxSprite would have access to, such as adding supplimental animations. * - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);` - * @author MasterEric + * @author EliteMasterEric */ @:nullSafety class HealthIcon extends FunkinSprite @@ -53,8 +53,9 @@ class HealthIcon extends FunkinSprite /** * Apply the "bop" animation once every X steps. + * Defaults to once per beat. */ - public var bopEvery:Int = 4; + public var bopEvery:Int = Constants.STEPS_PER_BEAT; /** * The amount, in degrees, to rotate the icon by when boping. @@ -373,6 +374,10 @@ class HealthIcon extends FunkinSprite // Don't flip BF's icon here! That's done later. this.animation.add(Idle, [0], 0, false, false); this.animation.add(Losing, [1], 0, false, false); + if (animation.numFrames >= 3) + { + this.animation.add(Winning, [2], 0, false, false); + } } function correctCharacterId(charId:Null):String diff --git a/source/funkin/play/event/ScrollSpeedEvent.hx b/source/funkin/play/event/ScrollSpeedEvent.hx new file mode 100644 index 000000000..c752d2f6d --- /dev/null +++ b/source/funkin/play/event/ScrollSpeedEvent.hx @@ -0,0 +1,176 @@ +package funkin.play.event; + +import flixel.tweens.FlxTween; +import flixel.FlxCamera; +import flixel.tweens.FlxEase; +// Data from the chart +import funkin.data.song.SongData; +import funkin.data.song.SongData.SongEventData; +// Data from the event schema +import funkin.play.event.SongEvent; +import funkin.data.event.SongEventSchema; +import funkin.data.event.SongEventSchema.SongEventFieldType; + +/** + * This class represents a handler for scroll speed events. + * + * Example: Scroll speed change of both strums from 1x to 1.3x: + * ``` + * { + * 'e': 'ScrollSpeed', + * "v": { + * "scroll": "1.3", + * "duration": "4", + * "ease": "linear", + * "strumline": "both", + * "absolute": false + * } + * } + * ``` + */ +class ScrollSpeedEvent extends SongEvent +{ + public function new() + { + super('ScrollSpeed'); + } + + static final DEFAULT_SCROLL:Float = 1; + static final DEFAULT_DURATION:Float = 4.0; + static final DEFAULT_EASE:String = 'linear'; + static final DEFAULT_ABSOLUTE:Bool = false; + static final DEFAULT_STRUMLINE:String = 'both'; // my special little trick + + public override function handleEvent(data:SongEventData):Void + { + // Does nothing if there is no PlayState. + if (PlayState.instance == null) return; + + var scroll:Float = data.getFloat('scroll') ?? DEFAULT_SCROLL; + + var duration:Float = data.getFloat('duration') ?? DEFAULT_DURATION; + + var ease:String = data.getString('ease') ?? DEFAULT_EASE; + + var strumline:String = data.getString('strumline') ?? DEFAULT_STRUMLINE; + + var absolute:Bool = data.getBool('absolute') ?? DEFAULT_ABSOLUTE; + + var strumlineNames:Array = []; + + if (!absolute) + { + // If absolute is set to false, do the awesome multiplicative thing + scroll = scroll * (PlayState.instance?.currentChart?.scrollSpeed ?? 1.0); + } + + switch (strumline) + { + case 'both': + strumlineNames = ['playerStrumline', 'opponentStrumline']; + default: + strumlineNames = [strumline + 'Strumline']; + } + // If it's a string, check the value. + switch (ease) + { + case 'INSTANT': + PlayState.instance.tweenScrollSpeed(scroll, 0, null, strumlineNames); + default: + var durSeconds = Conductor.instance.stepLengthMs * duration / 1000; + var easeFunction:NullFloat> = Reflect.field(FlxEase, ease); + if (easeFunction == null) + { + trace('Invalid ease function: $ease'); + return; + } + + PlayState.instance.tweenScrollSpeed(scroll, durSeconds, easeFunction, strumlineNames); + } + } + + public override function getTitle():String + { + return 'Scroll Speed'; + } + + /** + * ``` + * { + * 'scroll': FLOAT, // Target scroll level. + * 'duration': FLOAT, // Duration in steps. + * 'ease': ENUM, // Easing function. + * 'strumline': ENUM, // Which strumline to change + * 'absolute': BOOL, // True to set the scroll speed to the target level, false to set the scroll speed to (target level x base scroll speed) + * } + * @return SongEventSchema + */ + public override function getEventSchema():SongEventSchema + { + return new SongEventSchema([ + { + name: 'scroll', + title: 'Target Value', + defaultValue: 1.0, + step: 0.1, + type: SongEventFieldType.FLOAT, + units: 'x' + }, + { + name: 'duration', + title: 'Duration', + defaultValue: 4.0, + step: 0.5, + type: SongEventFieldType.FLOAT, + units: 'steps' + }, + { + name: 'ease', + title: 'Easing Type', + defaultValue: 'linear', + type: SongEventFieldType.ENUM, + keys: [ + 'Linear' => 'linear', + 'Instant (Ignores Duration)' => 'INSTANT', + 'Sine In' => 'sineIn', + 'Sine Out' => 'sineOut', + 'Sine In/Out' => 'sineInOut', + 'Quad In' => 'quadIn', + 'Quad Out' => 'quadOut', + 'Quad In/Out' => 'quadInOut', + 'Cube In' => 'cubeIn', + 'Cube Out' => 'cubeOut', + 'Cube In/Out' => 'cubeInOut', + 'Quart In' => 'quartIn', + 'Quart Out' => 'quartOut', + 'Quart In/Out' => 'quartInOut', + 'Quint In' => 'quintIn', + 'Quint Out' => 'quintOut', + 'Quint In/Out' => 'quintInOut', + 'Expo In' => 'expoIn', + 'Expo Out' => 'expoOut', + 'Expo In/Out' => 'expoInOut', + 'Smooth Step In' => 'smoothStepIn', + 'Smooth Step Out' => 'smoothStepOut', + 'Smooth Step In/Out' => 'smoothStepInOut', + 'Elastic In' => 'elasticIn', + 'Elastic Out' => 'elasticOut', + 'Elastic In/Out' => 'elasticInOut' + ] + }, + { + name: 'strumline', + title: 'Target Strumline', + defaultValue: 'both', + type: SongEventFieldType.ENUM, + keys: ['Both' => 'both', 'Player' => 'player', 'Opponent' => 'opponent'] + }, + { + name: 'absolute', + title: 'Absolute', + defaultValue: false, + type: SongEventFieldType.BOOL, + } + ]); + } +} diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 95e0668be..0e4b6645f 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -52,6 +52,14 @@ class Strumline extends FlxSpriteGroup */ public var conductorInUse(get, set):Conductor; + // Used in-game to control the scroll speed within a song + public var scrollSpeed:Float = 1.0; + + public function resetScrollSpeed():Void + { + scrollSpeed = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; + } + var _conductorInUse:Null; function get_conductorInUse():Conductor @@ -134,6 +142,7 @@ class Strumline extends FlxSpriteGroup this.refresh(); this.onNoteIncoming = new FlxTypedSignalVoid>(); + resetScrollSpeed(); for (i in 0...KEY_COUNT) { @@ -171,6 +180,20 @@ class Strumline extends FlxSpriteGroup updateNotes(); } + /** + * 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. + + // 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; + } + /** * Return notes that are within `Constants.HIT_WINDOW` ms of the strumline. * @return An array of `NoteSprite` objects. @@ -283,7 +306,6 @@ class Strumline extends FlxSpriteGroup // var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0; // ^^^ commented this out... do NOT make it move faster as it moves offscreen! var vwoosh:Float = 1.0; - var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; return Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1); @@ -406,7 +428,7 @@ class Strumline extends FlxSpriteGroup if (Preferences.downscroll) { - holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2; + holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2; } else { @@ -435,7 +457,7 @@ class Strumline extends FlxSpriteGroup if (Preferences.downscroll) { - holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2; + holdNote.y = this.y - INITIAL_OFFSET - holdNote.height + STRUMLINE_SIZE / 2; } else { @@ -450,7 +472,7 @@ class Strumline extends FlxSpriteGroup if (Preferences.downscroll) { - holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2; + holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2; } else { @@ -539,6 +561,7 @@ class Strumline extends FlxSpriteGroup { playStatic(dir); } + resetScrollSpeed(); } public function applyNoteData(data:Array):Void @@ -705,6 +728,7 @@ class Strumline extends FlxSpriteGroup if (holdNoteSprite != null) { + holdNoteSprite.parentStrumline = this; holdNoteSprite.noteData = note; holdNoteSprite.strumTime = note.time; holdNoteSprite.noteDirection = note.getDirection(); diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index 056a6a5a9..b358d7f03 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -32,6 +32,7 @@ class SustainTrail extends FlxSprite public var sustainLength(default, set):Float = 0; // millis public var fullSustainLength:Float = 0; public var noteData:Null; + public var parentStrumline:Strumline; public var cover:NoteHoldCover = null; @@ -119,7 +120,7 @@ class SustainTrail extends FlxSprite // CALCULATE SIZE graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2 - graphicHeight = sustainHeight(sustainLength, getScrollSpeed()); + graphicHeight = sustainHeight(sustainLength, parentStrumline?.scrollSpeed ?? 1.0); // instead of scrollSpeed, PlayState.SONG.speed flipY = Preferences.downscroll; @@ -135,9 +136,21 @@ class SustainTrail extends FlxSprite this.active = true; // This NEEDS to be true for the note to be drawn! } - function getScrollSpeed():Float + function getBaseScrollSpeed() { - return PlayState?.instance?.currentChart?.scrollSpeed ?? 1.0; + return (PlayState.instance?.currentChart?.scrollSpeed ?? 1.0); + } + + var previousScrollSpeed:Float = 1; + + override function update(elapsed) + { + super.update(elapsed); + if (previousScrollSpeed != (parentStrumline?.scrollSpeed ?? 1.0)) + { + triggerRedraw(); + } + previousScrollSpeed = parentStrumline?.scrollSpeed ?? 1.0; } /** @@ -155,12 +168,16 @@ class SustainTrail extends FlxSprite if (s < 0.0) s = 0.0; if (sustainLength == s) return s; - - graphicHeight = sustainHeight(s, getScrollSpeed()); this.sustainLength = s; + triggerRedraw(); + return this.sustainLength; + } + + function triggerRedraw() + { + graphicHeight = sustainHeight(sustainLength, parentStrumline?.scrollSpeed ?? 1.0); updateClipping(); updateHitbox(); - return this.sustainLength; } public override function updateHitbox():Void @@ -178,7 +195,7 @@ class SustainTrail extends FlxSprite */ public function updateClipping(songTime:Float = 0):Void { - var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight); + var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight); if (clipHeight <= 0.1) { visible = false; diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index 744091b44..dc2c40647 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -1,5 +1,7 @@ package funkin.play.scoring; +import funkin.save.Save.SaveScoreData; + /** * Which system to use when scoring and judging notes. */ @@ -344,4 +346,323 @@ class Scoring return 'miss'; } } + + public static function calculateRank(scoreData:Null):Null + { + if (scoreData?.tallies.totalNotes == 0 || scoreData == null) return null; + + // we can return null here, meaning that the player hasn't actually played and finished the song (thus has no data) + if (scoreData.tallies.totalNotes == 0) return null; + + // Perfect (Platinum) is a Sick Full Clear + var isPerfectGold = scoreData.tallies.sick == scoreData.tallies.totalNotes; + if (isPerfectGold) + { + return ScoringRank.PERFECT_GOLD; + } + + // Else, use the standard grades + + // Grade % (only good and sick), 1.00 is a full combo + var grade = (scoreData.tallies.sick + scoreData.tallies.good) / scoreData.tallies.totalNotes; + // Clear % (including bad and shit). 1.00 is a full clear but not a full combo + var clear = (scoreData.tallies.totalNotesHit) / scoreData.tallies.totalNotes; + + if (grade == Constants.RANK_PERFECT_THRESHOLD) + { + return ScoringRank.PERFECT; + } + else if (grade >= Constants.RANK_EXCELLENT_THRESHOLD) + { + return ScoringRank.EXCELLENT; + } + else if (grade >= Constants.RANK_GREAT_THRESHOLD) + { + return ScoringRank.GREAT; + } + else if (grade >= Constants.RANK_GOOD_THRESHOLD) + { + return ScoringRank.GOOD; + } + else + { + return ScoringRank.SHIT; + } + } +} + +enum abstract ScoringRank(String) +{ + var PERFECT_GOLD; + var PERFECT; + var EXCELLENT; + var GREAT; + var GOOD; + var SHIT; + + /** + * Converts ScoringRank to an integer value for comparison. + * Better ranks should be tied to a higher value. + */ + static function getValue(rank:Null):Int + { + if (rank == null) return -1; + switch (rank) + { + case PERFECT_GOLD: + return 5; + case PERFECT: + return 4; + case EXCELLENT: + return 3; + case GREAT: + return 2; + case GOOD: + return 1; + case SHIT: + return 0; + default: + return -1; + } + } + + // Yes, we really need a different function for each comparison operator. + @:op(A > B) static function compareGT(a:Null, b:Null):Bool + { + if (a != null && b == null) return true; + if (a == null || b == null) return false; + + var temp1:Int = getValue(a); + var temp2:Int = getValue(b); + + return temp1 > temp2; + } + + @:op(A >= B) static function compareGTEQ(a:Null, b:Null):Bool + { + if (a != null && b == null) return true; + if (a == null || b == null) return false; + + var temp1:Int = getValue(a); + var temp2:Int = getValue(b); + + return temp1 >= temp2; + } + + @:op(A < B) static function compareLT(a:Null, b:Null):Bool + { + if (a != null && b == null) return true; + if (a == null || b == null) return false; + + var temp1:Int = getValue(a); + var temp2:Int = getValue(b); + + return temp1 < temp2; + } + + @:op(A <= B) static function compareLTEQ(a:Null, b:Null):Bool + { + if (a != null && b == null) return true; + if (a == null || b == null) return false; + + var temp1:Int = getValue(a); + var temp2:Int = getValue(b); + + return temp1 <= temp2; + } + + // @:op(A == B) isn't necessary! + + /** + * Delay in seconds + */ + public function getMusicDelay():Float + { + switch (abstract) + { + case PERFECT_GOLD | PERFECT: + // return 2.5; + return 95 / 24; + case EXCELLENT: + return 0; + case GREAT: + return 5 / 24; + case GOOD: + return 3 / 24; + case SHIT: + return 2 / 24; + default: + return 3.5; + } + } + + public function getBFDelay():Float + { + switch (abstract) + { + case PERFECT_GOLD | PERFECT: + // return 2.5; + return 95 / 24; + case EXCELLENT: + return 97 / 24; + case GREAT: + return 95 / 24; + case GOOD: + return 95 / 24; + case SHIT: + return 95 / 24; + default: + return 3.5; + } + } + + public function getFlashDelay():Float + { + switch (abstract) + { + case PERFECT_GOLD | PERFECT: + // return 2.5; + return 129 / 24; + case EXCELLENT: + return 122 / 24; + case GREAT: + return 109 / 24; + case GOOD: + return 107 / 24; + case SHIT: + return 186 / 24; + default: + return 3.5; + } + } + + public function getHighscoreDelay():Float + { + switch (abstract) + { + case PERFECT_GOLD | PERFECT: + // return 2.5; + return 140 / 24; + case EXCELLENT: + return 140 / 24; + case GREAT: + return 129 / 24; + case GOOD: + return 127 / 24; + case SHIT: + return 207 / 24; + default: + return 3.5; + } + } + + public function getMusicPath():String + { + switch (abstract) + { + case PERFECT_GOLD: + return 'resultsPERFECT'; + case PERFECT: + return 'resultsPERFECT'; + case EXCELLENT: + return 'resultsEXCELLENT'; + case GREAT: + return 'resultsNORMAL'; + case GOOD: + return 'resultsNORMAL'; + case SHIT: + return 'resultsSHIT'; + default: + return 'resultsNORMAL'; + } + } + + public function hasMusicIntro():Bool + { + switch (abstract) + { + case EXCELLENT: + return true; + case SHIT: + return true; + default: + return false; + } + } + + public function getFreeplayRankIconAsset():Null + { + switch (abstract) + { + case PERFECT_GOLD: + return 'PERFECTSICK'; + case PERFECT: + return 'PERFECT'; + case EXCELLENT: + return 'EXCELLENT'; + case GREAT: + return 'GREAT'; + case GOOD: + return 'GOOD'; + case SHIT: + return 'LOSS'; + default: + return null; + } + } + + public function shouldMusicLoop():Bool + { + switch (abstract) + { + case PERFECT_GOLD | PERFECT | EXCELLENT | GREAT | GOOD: + return true; + case SHIT: + return false; + default: + return false; + } + } + + public function getHorTextAsset() + { + switch (abstract) + { + case PERFECT_GOLD: + return 'resultScreen/rankText/rankScrollPERFECT'; + case PERFECT: + return 'resultScreen/rankText/rankScrollPERFECT'; + case EXCELLENT: + return 'resultScreen/rankText/rankScrollEXCELLENT'; + case GREAT: + return 'resultScreen/rankText/rankScrollGREAT'; + case GOOD: + return 'resultScreen/rankText/rankScrollGOOD'; + case SHIT: + return 'resultScreen/rankText/rankScrollLOSS'; + default: + return 'resultScreen/rankText/rankScrollGOOD'; + } + } + + public function getVerTextAsset() + { + switch (abstract) + { + case PERFECT_GOLD: + return 'resultScreen/rankText/rankTextPERFECT'; + case PERFECT: + return 'resultScreen/rankText/rankTextPERFECT'; + case EXCELLENT: + return 'resultScreen/rankText/rankTextEXCELLENT'; + case GREAT: + return 'resultScreen/rankText/rankTextGREAT'; + case GOOD: + return 'resultScreen/rankText/rankTextGOOD'; + case SHIT: + return 'resultScreen/rankText/rankTextLOSS'; + default: + return 'resultScreen/rankText/rankTextGOOD'; + } + } } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index e71ae3213..dde5ee7b8 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -91,6 +91,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry 0) return _metadata.get(Constants.DEFAULT_VARIATION)?.charter ?? 'Unknown'; + return Constants.DEFAULT_CHARTER; + } + /** * @param id The ID of the song to load. * @param ignoreErrors If false, an exception will be thrown if the song data could not be loaded. @@ -270,6 +288,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry):Null { - if (diffId == null) diffId = listDifficulties(variation)[0]; + if (diffId == null) diffId = listDifficulties(variation, variations)[0]; if (variation == null) variation = Constants.DEFAULT_VARIATION; if (variations == null) variations = [variation]; @@ -399,6 +419,27 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + { + if (charId == null) charId = Constants.DEFAULT_CHARACTER; + + if (variations.contains(charId)) + { + return [charId]; + } + else + { + // TODO: How to exclude character variations while keeping other custom variations? + return variations; + } + } + /** * List all the difficulties in this song. * @@ -418,12 +459,16 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = difficulties.keys().array().map(function(diffId:String):Null { - var difficulty:Null = difficulties.get(diffId); - if (difficulty == null) return null; - if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null; - return difficulty.difficulty; - }).nonNull().unique(); + var diffFiltered:Array = difficulties.keys() + .array() + .map(function(diffId:String):Null { + var difficulty:Null = difficulties.get(diffId); + if (difficulty == null) return null; + if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null; + return difficulty.difficulty; + }) + .filterNull() + .distinct(); diffFiltered = diffFiltered.filter(function(diffId:String):Bool { if (showHidden) return true; @@ -565,6 +610,7 @@ class SongDifficulty public var songName:String = Constants.DEFAULT_SONGNAME; public var songArtist:String = Constants.DEFAULT_ARTIST; + public var charter:String = Constants.DEFAULT_CHARTER; public var timeFormat:SongTimeFormat = Constants.DEFAULT_TIMEFORMAT; public var divisions:Null = null; public var looped:Bool = false; @@ -636,9 +682,9 @@ class SongDifficulty FlxG.sound.cache(getInstPath(instrumental)); } - public function playInst(volume:Float = 1.0, looped:Bool = false):Void + public function playInst(volume:Float = 1.0, instId:String = '', looped:Bool = false):Void { - var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; + var suffix:String = (instId != '') ? '-$instId' : ''; FlxG.sound.music = FunkinSound.load(Paths.inst(this.song.id, suffix), volume, looped, false, true); diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index eb9eb1810..4f8ab4434 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -124,7 +124,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements getGirlfriend().resetCharacter(true); // Reapply the camera offsets. var stageCharData:StageDataCharacter = _data.characters.gf; - var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale; + var finalScale:Float = getGirlfriend().getBaseScale() * stageCharData.scale; getGirlfriend().setScale(finalScale); getGirlfriend().cameraFocusPoint.x += stageCharData.cameraOffsets[0]; getGirlfriend().cameraFocusPoint.y += stageCharData.cameraOffsets[1]; @@ -134,7 +134,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements getDad().resetCharacter(true); // Reapply the camera offsets. var stageCharData:StageDataCharacter = _data.characters.dad; - var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale; + var finalScale:Float = getDad().getBaseScale() * stageCharData.scale; getDad().setScale(finalScale); getDad().cameraFocusPoint.x += stageCharData.cameraOffsets[0]; getDad().cameraFocusPoint.y += stageCharData.cameraOffsets[1]; @@ -852,6 +852,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements } } + public override function toString():String + { + return 'Stage($id)'; + } + static function _fetchData(id:String):Null { return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id)); diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index acbe59edd..2900ce2be 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -1,21 +1,23 @@ package funkin.save; import flixel.util.FlxSave; -import funkin.save.migrator.SaveDataMigrator; -import thx.semver.Version; +import funkin.util.FileUtil; import funkin.input.Controls.Device; +import funkin.play.scoring.Scoring; +import funkin.play.scoring.Scoring.ScoringRank; import funkin.save.migrator.RawSaveData_v1_0_0; import funkin.save.migrator.SaveDataMigrator; +import funkin.save.migrator.SaveDataMigrator; import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle; import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; -import thx.semver.Version; import funkin.util.SerializerUtil; +import thx.semver.Version; +import thx.semver.Version; @:nullSafety class Save { - // Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null. - public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.3"; + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.5"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. @@ -53,7 +55,11 @@ class Save public function new(?data:RawSaveData) { if (data == null) this.data = Save.getDefault(); - else this.data = data; + else + this.data = data; + + // Make sure the verison number is up to date before we flush. + updateVersionToLatest(); } public static function getDefault():RawSaveData @@ -77,6 +83,9 @@ class Save levels: [], songs: [], }, + + favoriteSongs: [], + options: { // Reasonable defaults. @@ -489,8 +498,13 @@ class Save return song.get(difficultyId); } + public function getSongRank(songId:String, difficultyId:String = 'normal'):Null + { + return Scoring.calculateRank(getSongScore(songId, difficultyId)); + } + /** - * Apply the score the user achieved for a given song on a given difficulty. + * Directly set the score the user achieved for a given song on a given difficulty. */ public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void { @@ -505,6 +519,44 @@ class Save flush(); } + /** + * Only replace the ranking data for the song, because the old score is still better. + */ + public function applySongRank(songId:String, difficultyId:String, newScoreData:SaveScoreData):Void + { + var newRank = Scoring.calculateRank(newScoreData); + if (newScoreData == null || newRank == null) return; + + var song = data.scores.songs.get(songId); + if (song == null) + { + song = []; + data.scores.songs.set(songId, song); + } + + var previousScoreData = song.get(difficultyId); + + var previousRank = Scoring.calculateRank(previousScoreData); + + if (previousScoreData == null || previousRank == null) + { + // Directly set the highscore. + setSongScore(songId, difficultyId, newScoreData); + return; + } + + // Set the high score and the high rank separately. + var newScore:SaveScoreData = + { + score: (previousScoreData.score > newScoreData.score) ? previousScoreData.score : newScoreData.score, + tallies: (previousRank > newRank) ? previousScoreData.tallies : newScoreData.tallies + }; + + song.set(difficultyId, newScore); + + flush(); + } + /** * Is the provided score data better than the current high score for the given song? * @param songId The song ID to check. @@ -530,6 +582,39 @@ class Save return score.score > currentScore.score; } + /** + * Is the provided score data better than the current rank for the given song? + * @param songId The song ID to check. + * @param difficultyId The difficulty to check. + * @param score The score to check the rank for. + * @return Whether the score's rank is better than the current rank. + */ + public function isSongHighRank(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool + { + var newScoreRank = Scoring.calculateRank(score); + if (newScoreRank == null) + { + // The provided score is invalid. + return false; + } + + var song = data.scores.songs.get(songId); + if (song == null) + { + song = []; + data.scores.songs.set(songId, song); + } + var currentScore = song.get(difficultyId); + var currentScoreRank = Scoring.calculateRank(currentScore); + if (currentScoreRank == null) + { + // There is no primary highscore for this song. + return true; + } + + return newScoreRank > currentScoreRank; + } + /** * Has the provided song been beaten on one of the listed difficulties? * @param songId The song ID to check. @@ -554,6 +639,35 @@ class Save return false; } + public function isSongFavorited(id:String):Bool + { + if (data.favoriteSongs == null) + { + data.favoriteSongs = []; + flush(); + }; + + return data.favoriteSongs.contains(id); + } + + public function favoriteSong(id:String):Void + { + if (!isSongFavorited(id)) + { + data.favoriteSongs.push(id); + flush(); + } + } + + public function unfavoriteSong(id:String):Void + { + if (isSongFavorited(id)) + { + data.favoriteSongs.remove(id); + flush(); + } + } + public function getControls(playerId:Int, inputType:Device):Null { switch (inputType) @@ -674,7 +788,6 @@ class Save { trace('[SAVE] Found legacy save data, converting...'); var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData); - @:privateAccess FlxG.save.mergeData(gameSave.data, true); } else @@ -686,13 +799,94 @@ class Save } else { - trace('[SAVE] Loaded save data.'); - @:privateAccess + trace('[SAVE] Found existing save data.'); var gameSave = SaveDataMigrator.migrate(FlxG.save.data); FlxG.save.mergeData(gameSave.data, true); } } + public static function archiveBadSaveData(data:Dynamic):Int + { + // We want to save this somewhere so we can try to recover it for the user in the future! + + final RECOVERY_SLOT_START = 1000; + + return writeToAvailableSlot(RECOVERY_SLOT_START, data); + } + + public static function debug_queryBadSaveData():Void + { + final RECOVERY_SLOT_START = 1000; + final RECOVERY_SLOT_END = 1100; + var firstBadSaveData = querySlotRange(RECOVERY_SLOT_START, RECOVERY_SLOT_END); + if (firstBadSaveData > 0) + { + trace('[SAVE] Found bad save data in slot ${firstBadSaveData}!'); + trace('We should look into recovery...'); + + trace(haxe.Json.stringify(fetchFromSlotRaw(firstBadSaveData))); + } + } + + static function fetchFromSlotRaw(slot:Int):Null + { + var targetSaveData = new FlxSave(); + targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); + if (targetSaveData.isEmpty()) return null; + return targetSaveData.data; + } + + static function writeToAvailableSlot(slot:Int, data:Dynamic):Int + { + trace('[SAVE] Finding slot to write data to (starting with ${slot})...'); + + var targetSaveData = new FlxSave(); + targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); + while (!targetSaveData.isEmpty()) + { + // Keep trying to bind to slots until we find an empty slot. + trace('[SAVE] Slot ${slot} is taken, continuing...'); + slot++; + targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); + } + + trace('[SAVE] Writing data to slot ${slot}...'); + targetSaveData.mergeData(data, true); + + trace('[SAVE] Data written to slot ${slot}!'); + return slot; + } + + /** + * Return true if the given save slot is not empty. + * @param slot The slot number to check. + * @return Whether the slot is not empty. + */ + static function querySlot(slot:Int):Bool + { + var targetSaveData = new FlxSave(); + targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); + return !targetSaveData.isEmpty(); + } + + /** + * Return true if any of the slots in the given range is not empty. + * @param start The starting slot number to check. + * @param end The ending slot number to check. + * @return The first slot in the range that is not empty, or `-1` if none are. + */ + static function querySlotRange(start:Int, end:Int):Int + { + for (i in start...end) + { + if (querySlot(i)) + { + return i; + } + } + return -1; + } + static function fetchLegacySaveData():Null { trace("[SAVE] Checking for legacy save data..."); @@ -710,10 +904,34 @@ class Save return cast legacySave.data; } } + + /** + * Serialize this Save into a JSON string. + * @param pretty Whether the JSON should be big ol string (false), + * or formatted with tabs (true) + * @return The JSON string. + */ + public function serialize(pretty:Bool = true):String + { + var ignoreNullOptionals = true; + var writer = new json2object.JsonWriter(ignoreNullOptionals); + return writer.write(data, pretty ? ' ' : null); + } + + public function updateVersionToLatest():Void + { + this.data.version = Save.SAVE_DATA_VERSION; + } + + public function debug_dumpSave():Void + { + FileUtil.saveFile(haxe.io.Bytes.ofString(this.serialize()), [FileUtil.FILE_FILTER_JSON], null, null, './save.json', 'Write save data as JSON...'); + } } /** * An anonymous structure containingg all the user's save data. + * Isn't stored with JSON, stored with some sort of Haxe built-in serialization? */ typedef RawSaveData = { @@ -724,8 +942,6 @@ typedef RawSaveData = /** * A semantic versioning string for the save data format. */ - @:jcustomparse(funkin.data.DataParse.semverVersion) - @:jcustomwrite(funkin.data.DataWrite.semverVersion) var version:Version; var api:SaveApiData; @@ -740,6 +956,12 @@ typedef RawSaveData = */ var options:SaveDataOptions; + /** + * The user's favorited songs in the Freeplay menu, + * as a list of song IDs. + */ + var favoriteSongs:Array; + var mods:SaveDataMods; /** @@ -777,6 +999,9 @@ typedef SaveHighScoresData = typedef SaveDataMods = { var enabledMods:Array; + + // TODO: Make this not trip up the serializer when debugging. + @:jignored var modOptions:Map; } @@ -809,11 +1034,6 @@ typedef SaveScoreData = * The count of each judgement hit. */ var tallies:SaveScoreTallyData; - - /** - * The accuracy percentage. - */ - var accuracy:Float; } typedef SaveScoreTallyData = diff --git a/source/funkin/save/changelog.md b/source/funkin/save/changelog.md index 3fa9839d1..e3038373d 100644 --- a/source/funkin/save/changelog.md +++ b/source/funkin/save/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.0.5] - 2024-05-21 +### Fixed +- Resolved an issue where HTML5 wouldn't store the semantic version properly, causing the game to fail to load the save. + +## [2.0.4] - 2024-05-21 +### Added +- `favoriteSongs:Array` to `Save` ## [2.0.3] - 2024-01-09 ### Added diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index 3ed59e726..7a929322a 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -3,7 +3,6 @@ package funkin.save.migrator; import funkin.save.Save; import funkin.save.migrator.RawSaveData_v1_0_0; import thx.semver.Version; -import funkin.util.StructureUtil; import funkin.util.VersionUtil; @:nullSafety @@ -24,16 +23,21 @@ class SaveDataMigrator } else { + // Sometimes the Haxe serializer has issues with the version so we fix it here. + version = VersionUtil.repairVersion(version); if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE)) { - // Simply import the structured data. - var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData)); + // Import the structured data. + var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData); + var save:Save = new Save(saveDataWithDefaults); return save; } else { - trace('[SAVE] Invalid save data version! Returning blank data.'); - trace(inputData); + var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.'; + var slot:Int = Save.archiveBadSaveData(inputData); + var fullMessage:String = 'An error occurred migrating your save data.\n${message}\nInvalid data has been moved to save slot ${slot}.'; + lime.app.Application.current.window.alert(fullMessage, "Save Data Failure"); return new Save(Save.getDefault()); } } @@ -118,7 +122,7 @@ class SaveDataMigrator var scoreDataEasy:SaveScoreData = { score: inputSaveData.songScores.get('${levelId}-easy') ?? 0, - accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0, + // accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0, tallies: { sick: 0, @@ -137,7 +141,7 @@ class SaveDataMigrator var scoreDataNormal:SaveScoreData = { score: inputSaveData.songScores.get('${levelId}') ?? 0, - accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0, + // accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0, tallies: { sick: 0, @@ -156,7 +160,7 @@ class SaveDataMigrator var scoreDataHard:SaveScoreData = { score: inputSaveData.songScores.get('${levelId}-hard') ?? 0, - accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0, + // accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0, tallies: { sick: 0, @@ -178,7 +182,6 @@ class SaveDataMigrator var scoreDataEasy:SaveScoreData = { score: 0, - accuracy: 0, tallies: { sick: 0, @@ -196,14 +199,13 @@ class SaveDataMigrator for (songId in songIds) { scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0)); - scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0); + // scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0); } result.setSongScore(songIds[0], 'easy', scoreDataEasy); var scoreDataNormal:SaveScoreData = { score: 0, - accuracy: 0, tallies: { sick: 0, @@ -221,14 +223,13 @@ class SaveDataMigrator for (songId in songIds) { scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0)); - scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0); + // scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0); } result.setSongScore(songIds[0], 'normal', scoreDataNormal); var scoreDataHard:SaveScoreData = { score: 0, - accuracy: 0, tallies: { sick: 0, @@ -246,7 +247,7 @@ class SaveDataMigrator for (songId in songIds) { scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0)); - scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0); + // scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0); } result.setSongScore(songIds[0], 'hard', scoreDataHard); } diff --git a/source/funkin/ui/MenuList.hx b/source/funkin/ui/MenuList.hx index 63a688778..d7319abd6 100644 --- a/source/funkin/ui/MenuList.hx +++ b/source/funkin/ui/MenuList.hx @@ -94,7 +94,7 @@ class MenuTypedList extends FlxTypedGroup if (newIndex != selectedIndex) { - FunkinSound.playOnce(Paths.sound('scrollMenu')); + FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); selectItem(newIndex); } @@ -142,7 +142,7 @@ class MenuTypedList extends FlxTypedGroup */ function navGrid(latSize:Int, latPrev:Bool, latNext:Bool, latAllowWrap:Bool, prev:Bool, next:Bool, allowWrap:Bool):Int { - // The grid lenth along the variable-length axis + // The grid length along the variable-length axis var size = Math.ceil(length / latSize); // The selected position along the variable-length axis var index = Math.floor(selectedIndex / latSize); diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx index 2240ec50e..844d0f4db 100644 --- a/source/funkin/ui/credits/CreditsDataHandler.hx +++ b/source/funkin/ui/credits/CreditsDataHandler.hx @@ -54,7 +54,7 @@ class CreditsDataHandler body: [ {line: 'ninjamuffin99'}, {line: 'PhantomArcade'}, - {line: 'KawaiSprite'}, + {line: 'Kawai Sprite'}, {line: 'evilsk8r'}, ] } diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx index 6be1fecf7..44769e9b3 100644 --- a/source/funkin/ui/credits/CreditsState.hx +++ b/source/funkin/ui/credits/CreditsState.hx @@ -4,6 +4,7 @@ import flixel.text.FlxText; import flixel.util.FlxColor; import funkin.audio.FunkinSound; import flixel.FlxSprite; +import funkin.ui.mainmenu.MainMenuState; import flixel.group.FlxSpriteGroup; /** @@ -199,7 +200,7 @@ class CreditsState extends MusicBeatState function exit():Void { - FlxG.switchState(funkin.ui.mainmenu.MainMenuState.new); + FlxG.switchState(() -> new MainMenuState()); } public override function destroy():Void diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index 6d6e73e80..f8b1be9d2 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -62,7 +62,6 @@ class DebugMenuSubState extends MusicBeatSubState #if sys createItem("OPEN CRASH LOG FOLDER", openLogFolder); #end - FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y)); FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y + 500)); } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index b75cd8bf1..f72cca77f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -137,7 +137,7 @@ using Lambda; * * Some functionality is split into handler classes to help maintain my sanity. * - * @author MasterEric + * @author EliteMasterEric */ // @:nullSafety @@ -904,7 +904,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function set_notePreviewDirty(value:Bool):Bool { - trace('Note preview dirtied!'); + // trace('Note preview dirtied!'); return notePreviewDirty = value; } @@ -1270,7 +1270,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var result:Null = songMetadata.get(selectedVariation); if (result == null) { - result = new SongMetadata('DadBattle', 'Kawai Sprite', selectedVariation); + result = new SongMetadata('Default Song Name', Constants.DEFAULT_ARTIST, selectedVariation); songMetadata.set(selectedVariation, result); } return result; @@ -4566,8 +4566,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } gridGhostHoldNote.visible = true; - gridGhostHoldNote.noteData = gridGhostNote.noteData; - gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection(); + gridGhostHoldNote.noteData = currentPlaceNoteData; + gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); @@ -6304,7 +6304,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault()); tempNote.noteData = noteData; tempNote.scrollFactor.set(0, 0); - var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', 0); + var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', false, 0); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx index 73cf80fa0..661c44d85 100644 --- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx @@ -35,7 +35,15 @@ class SetItemSelectionCommand implements ChartEditorCommand { var eventSelected = this.events[0]; - state.eventKindToPlace = eventSelected.eventKind; + if (state.eventKindToPlace == eventSelected.eventKind) + { + trace('Target event kind matches selection: ${eventSelected.eventKind}'); + } + else + { + trace('Switching target event kind to match selection: ${state.eventKindToPlace} != ${eventSelected.eventKind}'); + state.eventKindToPlace = eventSelected.eventKind; + } // This code is here to parse event data that's not built as a struct for some reason. // TODO: Clean this up or get rid of it. diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index aeb6dd0e4..ded48abe3 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -36,6 +36,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail zoom *= 0.7; zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE; + flipY = false; + setup(); } @@ -58,11 +60,11 @@ class ChartEditorHoldNoteSprite extends SustainTrail { if (lerp) { - sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25); + sustainLength = FlxMath.lerp(sustainLength, h / (getBaseScrollSpeed() * Constants.PIXELS_PER_MS), 0.25); } else { - sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS); + sustainLength = h / (getBaseScrollSpeed() * Constants.PIXELS_PER_MS); } fullSustainLength = sustainLength; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index 0308cd871..e84f7ec43 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -384,17 +384,34 @@ class ChartEditorImportExportHandler if (variationId == '') { var variationMetadata:Null = state.songMetadata.get(variation); - if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize())); + if (variationMetadata != null) + { + variationMetadata.version = funkin.data.song.SongRegistry.SONG_METADATA_VERSION; + variationMetadata.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY; + zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize())); + } var variationChart:Null = state.songChartData.get(variation); - if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize())); + if (variationChart != null) + { + variationChart.version = funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION; + variationChart.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY; + zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize())); + } } else { var variationMetadata:Null = state.songMetadata.get(variation); - if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json', - variationMetadata.serialize())); + if (variationMetadata != null) + { + zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json', variationMetadata.serialize())); + } var variationChart:Null = state.songChartData.get(variation); - if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize())); + if (variationChart != null) + { + variationChart.version = funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION; + variationChart.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY; + zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize())); + } } } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index b1af0ce4c..e42102a52 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -201,7 +201,8 @@ class ChartEditorThemeHandler // Selection borders horizontally in the middle. for (i in 1...(Conductor.instance.stepsPerMeasure)) { - if ((i % Conductor.instance.beatsPerMeasure) == 0) + // There may be a different number of beats per measure, but there's always 4 steps per beat. + if ((i % Constants.STEPS_PER_BEAT) == 0) { state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (GRID_BEAT_DIVIDER_WIDTH / 2), state.gridBitmap.width, GRID_BEAT_DIVIDER_WIDTH), diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx index f0949846d..8f021840a 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx @@ -58,17 +58,8 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox function initialize():Void { - toolboxEventsEventKind.dataSource = new ArrayDataSource(); - - var songEvents:Array = SongEventRegistry.listEvents(); - - for (event in songEvents) - { - toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id}); - } - toolboxEventsEventKind.onChange = function(event:UIEvent) { - var eventType:String = event.data.value; + var eventType:String = event.data.id; trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType'); @@ -83,7 +74,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox return; } - buildEventDataFormFromSchema(toolboxEventsDataGrid, schema); + buildEventDataFormFromSchema(toolboxEventsDataGrid, schema, chartEditorState.eventKindToPlace); if (!_initializing && chartEditorState.currentEventSelection.length > 0) { @@ -98,14 +89,40 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox chartEditorState.notePreviewDirty = true; } } - toolboxEventsEventKind.value = chartEditorState.eventKindToPlace; + var startingEventValue = ChartEditorDropdowns.populateDropdownWithSongEvents(toolboxEventsEventKind, chartEditorState.eventKindToPlace); + trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Starting event kind: ${startingEventValue}'); + toolboxEventsEventKind.value = startingEventValue; } public override function refresh():Void { super.refresh(); - toolboxEventsEventKind.value = chartEditorState.eventKindToPlace; + var newDropdownElement = ChartEditorDropdowns.findDropdownElement(chartEditorState.eventKindToPlace, toolboxEventsEventKind); + + if (newDropdownElement == null) + { + throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event kind not in dropdown: ${chartEditorState.eventKindToPlace}'; + } + else if (toolboxEventsEventKind.value != newDropdownElement || lastEventKind != toolboxEventsEventKind.value.id) + { + toolboxEventsEventKind.value = newDropdownElement; + + var schema:SongEventSchema = SongEventRegistry.getEventSchema(chartEditorState.eventKindToPlace); + if (schema == null) + { + trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: ${chartEditorState.eventKindToPlace}'); + } + else + { + trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event kind changed: ${toolboxEventsEventKind.value.id} != ${newDropdownElement.id} != ${lastEventKind}, rebuilding form'); + buildEventDataFormFromSchema(toolboxEventsDataGrid, schema, chartEditorState.eventKindToPlace); + } + } + else + { + trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event kind not changed: ${toolboxEventsEventKind.value} == ${newDropdownElement} == ${lastEventKind}'); + } for (pair in chartEditorState.eventDataToPlace.keyValueIterator()) { @@ -116,7 +133,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox if (field == null) { - throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" does not exist in the event data form.'; + throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" does not exist in the event data form for kind ${lastEventKind}.'; } else { @@ -141,9 +158,15 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox } } - function buildEventDataFormFromSchema(target:Box, schema:SongEventSchema):Void + var lastEventKind:String = 'unknown'; + + function buildEventDataFormFromSchema(target:Box, schema:SongEventSchema, eventKind:String):Void { - trace(schema); + trace('Building event data form from schema for event kind: ${eventKind}'); + // trace(schema); + + lastEventKind = eventKind ?? 'unknown'; + // Clear the frame. target.removeAllComponents(); @@ -188,6 +211,9 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox var dropDown:DropDown = new DropDown(); dropDown.id = field.name; dropDown.width = 200.0; + dropDown.dropdownSize = 10; + dropDown.dropdownWidth = 300; + dropDown.searchable = true; dropDown.dataSource = new ArrayDataSource(); if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.'; @@ -197,12 +223,15 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox for (optionName in field.keys.keys()) { var optionValue:Null = field.keys.get(optionName); - trace('$optionName : $optionValue'); + // trace('$optionName : $optionValue'); dropDown.dataSource.add({value: optionValue, text: optionName}); } dropDown.value = field.defaultValue; + // TODO: Add an option to customize sort. + dropDown.dataSource.sort('text', ASCENDING); + input = dropDown; case STRING: input = new TextField(); diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx index f85307c64..c97e8142d 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx @@ -29,6 +29,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox { var inputSongName:TextField; var inputSongArtist:TextField; + var inputSongCharter:TextField; var inputStage:DropDown; var inputNoteStyle:DropDown; var buttonCharacterPlayer:Button; @@ -89,6 +90,20 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox } }; + inputSongCharter.onChange = function(event:UIEvent) { + var valid:Bool = event.target.text != null && event.target.text != ''; + + if (valid) + { + inputSongCharter.removeClass('invalid-value'); + chartEditorState.currentSongMetadata.charter = event.target.text; + } + else + { + chartEditorState.currentSongMetadata.charter = null; + } + }; + inputStage.onChange = function(event:UIEvent) { var valid:Bool = event.data != null && event.data.id != null; @@ -104,6 +119,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox if (event.data?.id == null) return; chartEditorState.currentSongNoteStyle = event.data.id; }; + var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, chartEditorState.currentSongMetadata.playData.noteStyle); + inputNoteStyle.value = startingValueNoteStyle; inputBPM.onChange = function(event:UIEvent) { if (event.value == null || event.value <= 0) return; @@ -176,6 +193,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox inputSongName.value = chartEditorState.currentSongMetadata.songName; inputSongArtist.value = chartEditorState.currentSongMetadata.artist; + inputSongCharter.value = chartEditorState.currentSongMetadata.charter; inputStage.value = chartEditorState.currentSongMetadata.playData.stage; inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle; inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm; diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx index d2a0a053e..55aab0ab0 100644 --- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx +++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx @@ -3,11 +3,13 @@ package funkin.ui.debug.charting.util; import funkin.data.notestyle.NoteStyleRegistry; import funkin.play.notes.notestyle.NoteStyle; import funkin.data.stage.StageData; +import funkin.play.event.SongEvent; import funkin.data.stage.StageRegistry; import funkin.play.character.CharacterData; import haxe.ui.components.DropDown; import funkin.play.stage.Stage; import funkin.play.character.BaseCharacter.CharacterType; +import funkin.data.event.SongEventRegistry; import funkin.play.character.CharacterData.CharacterDataParser; /** @@ -81,6 +83,42 @@ class ChartEditorDropdowns return returnValue; } + public static function populateDropdownWithSongEvents(dropDown:DropDown, startingEventId:String):DropDownEntry + { + dropDown.dataSource.clear(); + + var returnValue:DropDownEntry = {id: "FocusCamera", text: "Focus Camera"}; + + var songEvents:Array = SongEventRegistry.listEvents(); + + for (event in songEvents) + { + var value = {id: event.id, text: event.getTitle()}; + if (startingEventId == event.id) returnValue = value; + dropDown.dataSource.add(value); + } + + dropDown.dataSource.sort('text', ASCENDING); + + return returnValue; + } + + /** + * Given the ID of a dropdown element, find the corresponding entry in the dropdown's dataSource. + */ + public static function findDropdownElement(id:String, dropDown:DropDown):Null + { + // Attempt to find the entry. + for (entryIndex in 0...dropDown.dataSource.size) + { + var entry = dropDown.dataSource.get(entryIndex); + if (entry.id == id) return entry; + } + + // Not found. + return null; + } + /** * Populate a dropdown with a list of note styles. */ diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index 35facf131..49c588722 100644 --- a/source/funkin/ui/freeplay/AlbumRoll.hx +++ b/source/funkin/ui/freeplay/AlbumRoll.hx @@ -38,7 +38,7 @@ class AlbumRoll extends FlxSpriteGroup var newAlbumArt:FlxAtlasSprite; - // var difficultyStars:DifficultyStars; + var difficultyStars:DifficultyStars; var _exitMovers:Null; var albumData:Album; @@ -65,9 +65,9 @@ class AlbumRoll extends FlxSpriteGroup add(newAlbumArt); - // difficultyStars = new DifficultyStars(140, 39); - // difficultyStars.stars.visible = false; - // add(difficultyStars); + difficultyStars = new DifficultyStars(140, 39); + difficultyStars.visible = false; + add(difficultyStars); } function onAlbumFinish(animName:String):Void @@ -86,9 +86,14 @@ class AlbumRoll extends FlxSpriteGroup { if (albumId == null) { - // difficultyStars.stars.visible = false; + this.visible = false; + difficultyStars.stars.visible = false; return; } + else + { + this.visible = true; + } albumData = AlbumRegistry.instance.fetchEntry(albumId); @@ -126,7 +131,7 @@ class AlbumRoll extends FlxSpriteGroup if (exitMovers == null) return; - exitMovers.set([newAlbumArt], + exitMovers.set([newAlbumArt, difficultyStars], { x: FlxG.width, speed: 0.4, @@ -144,10 +149,10 @@ class AlbumRoll extends FlxSpriteGroup newAlbumArt.visible = true; newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false); - // difficultyStars.stars.visible = false; + difficultyStars.visible = false; new FlxTimer().start(0.75, function(_) { // showTitle(); - // showStars(); + showStars(); }); } @@ -156,16 +161,18 @@ class AlbumRoll extends FlxSpriteGroup newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false); } - // public function setDifficultyStars(?difficulty:Int):Void - // { - // if (difficulty == null) return; - // difficultyStars.difficulty = difficulty; - // } - // /** - // * Make the album stars visible. - // */ - // public function showStars():Void - // { - // difficultyStars.stars.visible = false; // true; - // } + public function setDifficultyStars(?difficulty:Int):Void + { + if (difficulty == null) return; + difficultyStars.difficulty = difficulty; + } + + /** + * Make the album stars visible. + */ + public function showStars():Void + { + difficultyStars.visible = true; // true; + difficultyStars.flameCheck(); + } } diff --git a/source/funkin/ui/freeplay/CapsuleText.hx b/source/funkin/ui/freeplay/CapsuleText.hx index 3a520e015..c3bcdb09b 100644 --- a/source/funkin/ui/freeplay/CapsuleText.hx +++ b/source/funkin/ui/freeplay/CapsuleText.hx @@ -4,6 +4,12 @@ import openfl.filters.BitmapFilterQuality; import flixel.text.FlxText; import flixel.group.FlxSpriteGroup; import funkin.graphics.shaders.GaussianBlurShader; +import funkin.graphics.shaders.LeftMaskShader; +import flixel.math.FlxRect; +import flixel.tweens.FlxEase; +import flixel.util.FlxTimer; +import flixel.tweens.FlxTween; +import openfl.display.BlendMode; class CapsuleText extends FlxSpriteGroup { @@ -13,6 +19,15 @@ class CapsuleText extends FlxSpriteGroup public var text(default, set):String; + var maskShaderSongName:LeftMaskShader = new LeftMaskShader(); + + public var clipWidth(default, set):Int = 255; + + public var tooLong:Bool = false; + + // 255, 27 normal + // 220, 27 favourited + public function new(x:Float, y:Float, songTitle:String, size:Float) { super(x, y); @@ -36,6 +51,41 @@ class CapsuleText extends FlxSpriteGroup return text; } + // ???? none + // 255, 27 normal + // 220, 27 favourited + + function set_clipWidth(value:Int):Int + { + resetText(); + checkClipWidth(value); + return clipWidth = value; + } + + /** + * Checks if the text if it's too long, and clips if it is + * @param wid + */ + function checkClipWidth(?wid:Int):Void + { + if (wid == null) wid = clipWidth; + + if (whiteText.width > wid) + { + tooLong = true; + + blurredText.clipRect = new FlxRect(0, 0, wid, blurredText.height); + whiteText.clipRect = new FlxRect(0, 0, wid, whiteText.height); + } + else + { + tooLong = false; + + blurredText.clipRect = null; + whiteText.clipRect = null; + } + } + function set_text(value:String):String { if (value == null) return value; @@ -47,10 +97,107 @@ class CapsuleText extends FlxSpriteGroup blurredText.text = value; whiteText.text = value; + checkClipWidth(); whiteText.textField.filters = [ new openfl.filters.GlowFilter(0x00ccff, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW) ]; + return text = value; } + + var moveTimer:FlxTimer = new FlxTimer(); + var moveTween:FlxTween; + + public function initMove():Void + { + moveTimer.start(0.6, (timer) -> { + moveTextRight(); + }); + } + + function moveTextRight():Void + { + var distToMove:Float = whiteText.width - clipWidth; + moveTween = FlxTween.tween(whiteText.offset, {x: distToMove}, 2, + { + onUpdate: function(_) { + whiteText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, whiteText.height); + blurredText.offset = whiteText.offset; + blurredText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, blurredText.height); + }, + onComplete: function(_) { + moveTimer.start(0.3, (timer) -> { + moveTextLeft(); + }); + }, + ease: FlxEase.sineInOut + }); + } + + function moveTextLeft():Void + { + moveTween = FlxTween.tween(whiteText.offset, {x: 0}, 2, + { + onUpdate: function(_) { + whiteText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, whiteText.height); + blurredText.offset = whiteText.offset; + blurredText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, blurredText.height); + }, + onComplete: function(_) { + moveTimer.start(0.3, (timer) -> { + moveTextRight(); + }); + }, + ease: FlxEase.sineInOut + }); + } + + public function resetText():Void + { + if (moveTween != null) moveTween.cancel(); + if (moveTimer != null) moveTimer.cancel(); + whiteText.offset.x = 0; + whiteText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, whiteText.height); + blurredText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, whiteText.height); + } + + var flickerState:Bool = false; + var flickerTimer:FlxTimer; + + public function flickerText():Void + { + resetText(); + flickerTimer = new FlxTimer().start(1 / 24, flickerProgress, 19); + } + + function flickerProgress(timer:FlxTimer):Void + { + if (flickerState == true) + { + whiteText.blend = BlendMode.ADD; + blurredText.blend = BlendMode.ADD; + blurredText.color = 0xFFFFFFFF; + whiteText.color = 0xFFFFFFFF; + whiteText.textField.filters = [ + new openfl.filters.GlowFilter(0xFFFFFF, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), + // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW) + ]; + } + else + { + blurredText.color = 0xFF00aadd; + whiteText.color = 0xFFDDDDDD; + whiteText.textField.filters = [ + new openfl.filters.GlowFilter(0xDDDDDD, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), + // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW) + ]; + } + flickerState = !flickerState; + } + + override function update(elapsed:Float):Void + { + super.update(elapsed); + } } diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx index 5f1144fab..bbf043dd4 100644 --- a/source/funkin/ui/freeplay/DJBoyfriend.hx +++ b/source/funkin/ui/freeplay/DJBoyfriend.hx @@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite var gotSpooked:Bool = false; - static final SPOOK_PERIOD:Float = 120.0; - static final TV_PERIOD:Float = 180.0; + static final SPOOK_PERIOD:Float = 60.0; + static final TV_PERIOD:Float = 120.0; // Time since dad last SPOOKED you. var timeSinceSpook:Float = 0; @@ -82,6 +82,8 @@ class DJBoyfriend extends FlxAtlasSprite return anims; } + var lowPumpLoopPoint:Int = 4; + public override function update(elapsed:Float):Void { super.update(elapsed); @@ -114,6 +116,14 @@ class DJBoyfriend extends FlxAtlasSprite case Confirm: if (getCurrentAnimation() != 'Boyfriend DJ confirm') playFlashAnimation('Boyfriend DJ confirm', false); timeSinceSpook = 0; + case PumpIntro: + if (getCurrentAnimation() != 'Boyfriend DJ fist pump') playFlashAnimation('Boyfriend DJ fist pump', false); + if (getCurrentAnimation() == 'Boyfriend DJ fist pump' && anim.curFrame >= 4) + { + anim.play("Boyfriend DJ fist pump", true, false, 0); + } + case FistPump: + case Spook: if (getCurrentAnimation() != 'bf dj afk') { @@ -174,6 +184,12 @@ class DJBoyfriend extends FlxAtlasSprite currentState = Idle; case "Boyfriend DJ confirm": + case "Boyfriend DJ fist pump": + currentState = Idle; + + case "Boyfriend DJ loss reaction 1": + currentState = Idle; + case "Boyfriend DJ watchin tv OG": var frame:Int = FlxG.random.bool(33) ? 112 : 166; @@ -250,7 +266,7 @@ class DJBoyfriend extends FlxAtlasSprite // Fade out music to 40% volume over 1 second. // This helps make the TV a bit more audible. - FlxG.sound.music.fadeOut(1.0, 0.4); + FlxG.sound.music.fadeOut(1.0, 0.1); // Play the cartoon at a random time between the start and 5 seconds from the end. cartoonSnd.time = FlxG.random.float(0, Math.max(cartoonSnd.length - (5 * Constants.MS_PER_SEC), 0.0)); @@ -275,6 +291,23 @@ class DJBoyfriend extends FlxAtlasSprite currentState = Confirm; } + public function fistPump():Void + { + currentState = PumpIntro; + } + + public function pumpFist():Void + { + currentState = FistPump; + anim.play("Boyfriend DJ fist pump", true, false, 4); + } + + public function pumpFistBad():Void + { + currentState = FistPump; + anim.play("Boyfriend DJ loss reaction 1", true, false, 4); + } + public inline function addOffset(name:String, x:Float = 0, y:Float = 0) { animOffsets[name] = [x, y]; @@ -331,6 +364,8 @@ enum DJBoyfriendState Intro; Idle; Confirm; + PumpIntro; + FistPump; Spook; TV; } diff --git a/source/funkin/ui/freeplay/DifficultyStars.hx b/source/funkin/ui/freeplay/DifficultyStars.hx new file mode 100644 index 000000000..e7a2b8888 --- /dev/null +++ b/source/funkin/ui/freeplay/DifficultyStars.hx @@ -0,0 +1,111 @@ +package funkin.ui.freeplay; + +import flixel.group.FlxSpriteGroup; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.graphics.shaders.HSVShader; + +class DifficultyStars extends FlxSpriteGroup +{ + /** + * Internal handler var for difficulty... ranges from 0... to 15 + * 0 is 1 star... 15 is 0 stars! + */ + var curDifficulty(default, set):Int = 0; + + /** + * Range between 0 and 15 + */ + public var difficulty(default, set):Int = 1; + + public var stars:FlxAtlasSprite; + + public var flames:FreeplayFlames; + + var hsvShader:HSVShader; + + public function new(x:Float, y:Float) + { + super(x, y); + + hsvShader = new HSVShader(); + + flames = new FreeplayFlames(0, 0); + add(flames); + + stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars")); + stars.anim.play("diff stars"); + add(stars); + + stars.shader = hsvShader; + + for (memb in flames.members) + memb.shader = hsvShader; + } + + override function update(elapsed:Float):Void + { + super.update(elapsed); + + // "loops" the current animation + // for clarity, the animation file looks like + // frame : stars + // 0-99: 1 star + // 100-199: 2 stars + // ...... + // 1300-1499: 15 stars + // 1500 : 0 stars + if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100) + { + stars.anim.play("diff stars", true, false, curDifficulty * 100); + } + } + + function set_difficulty(value:Int):Int + { + difficulty = value; + + if (difficulty <= 0) + { + difficulty = 0; + curDifficulty = 15; + } + else if (difficulty <= 15) + { + difficulty = value; + curDifficulty = difficulty - 1; + } + else + { + difficulty = 15; + curDifficulty = difficulty - 1; + } + + flameCheck(); + + return difficulty; + } + + public function flameCheck():Void + { + if (difficulty > 10) flames.flameCount = difficulty - 10; + else + flames.flameCount = 0; + } + + function set_curDifficulty(value:Int):Int + { + curDifficulty = value; + if (curDifficulty == 15) + { + stars.anim.play("diff stars", true, false, 1500); + stars.anim.pause(); + } + else + { + stars.anim.curFrame = Std.int(curDifficulty * 100); + stars.anim.play("diff stars", true, false, curDifficulty * 100); + } + + return curDifficulty; + } +} diff --git a/source/funkin/ui/freeplay/FreeplayFlames.hx b/source/funkin/ui/freeplay/FreeplayFlames.hx index c20d85898..f6b6f5c3d 100644 --- a/source/funkin/ui/freeplay/FreeplayFlames.hx +++ b/source/funkin/ui/freeplay/FreeplayFlames.hx @@ -50,8 +50,19 @@ class FreeplayFlames extends FlxSpriteGroup } } + var timers:Array = []; + function set_flameCount(value:Int):Int { + // Stop all existing timers. + // This fixes a bug where quickly switching difficulties would show flames. + for (timer in timers) + { + timer.active = false; + timer.destroy(); + timers.remove(timer); + } + this.flameCount = value; var visibleCount:Int = 0; for (i in 0...5) @@ -62,10 +73,18 @@ class FreeplayFlames extends FlxSpriteGroup { if (!flame.visible) { - new FlxTimer().start(flameTimer * visibleCount, function(_) { + var nextTimer:FlxTimer = new FlxTimer().start(flameTimer * visibleCount, function(currentTimer:FlxTimer) { + if (i >= this.flameCount) + { + trace('EARLY EXIT'); + return; + } + timers.remove(currentTimer); flame.animation.play("flame", true); flame.visible = true; }); + timers.push(nextTimer); + visibleCount++; } } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 7b7543845..0caaf4591 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1,15 +1,18 @@ package funkin.ui.freeplay; +import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.addons.transition.FlxTransitionableState; import flixel.addons.ui.FlxInputText; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.group.FlxGroup; +import funkin.graphics.shaders.GaussianBlurShader; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.input.touch.FlxTouch; import flixel.math.FlxAngle; import flixel.math.FlxPoint; +import openfl.display.BlendMode; import flixel.system.debug.watch.Tracker.TrackerProfile; import flixel.text.FlxText; import flixel.tweens.FlxEase; @@ -29,15 +32,21 @@ import funkin.graphics.shaders.StrokeShader; import funkin.input.Controls; import funkin.play.PlayStatePlaylist; import funkin.play.song.Song; +import funkin.ui.story.Level; import funkin.save.Save; import funkin.save.Save.SaveScoreData; import funkin.ui.AtlasText; +import funkin.play.scoring.Scoring; +import funkin.play.scoring.Scoring.ScoringRank; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatSubState; import funkin.ui.transition.LoadingState; import funkin.ui.transition.StickerSubState; import funkin.util.MathUtil; import lime.utils.Assets; +import flixel.tweens.misc.ShakeTween; +import funkin.effects.IntervalShake; +import funkin.ui.freeplay.SongMenuItem.FreeplayRank; /** * Parameters used to initialize the FreeplayState. @@ -45,6 +54,39 @@ import lime.utils.Assets; typedef FreeplayStateParams = { ?character:String, + + ?fromResults:FromResultsParams, +}; + +/** + * A set of parameters for transitioning to the FreeplayState from the ResultsState. + */ +typedef FromResultsParams = +{ + /** + * The previous rank the song hand, if any. Null if it had no score before. + */ + var ?oldRank:ScoringRank; + + /** + * Whether or not to play the rank animation on returning to freeplay. + */ + var playRankAnim:Bool; + + /** + * The new rank the song has. + */ + var newRank:ScoringRank; + + /** + * The song ID to play the animation on. + */ + var songId:String; + + /** + * The difficulty ID to play the animation on. + */ + var difficultyId:String; }; /** @@ -69,6 +111,7 @@ class FreeplayState extends MusicBeatSubState /** * For the audio preview, the duration of the fade-out effect. + * */ public static final FADE_OUT_DURATION:Float = 0.25; @@ -120,8 +163,6 @@ class FreeplayState extends MusicBeatSubState var curCapsule:SongMenuItem; var curPlaying:Bool = false; - var displayedVariations:Array; - var dj:DJBoyfriend; var ostName:FlxText; @@ -135,10 +176,44 @@ class FreeplayState extends MusicBeatSubState public static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; public static var rememberedSongId:Null = 'tutorial'; + var funnyCam:FunkinCamera; + var rankCamera:FunkinCamera; + var rankBg:FunkinSprite; + var rankVignette:FlxSprite; + + var backingTextYeah:FlxAtlasSprite; + var orangeBackShit:FunkinSprite; + var alsoOrangeLOL:FunkinSprite; + var pinkBack:FunkinSprite; + var confirmGlow:FlxSprite; + var confirmGlow2:FlxSprite; + var confirmTextGlow:FlxSprite; + + var moreWays:BGScrollingText; + var funnyScroll:BGScrollingText; + var txtNuts:BGScrollingText; + var funnyScroll2:BGScrollingText; + var moreWays2:BGScrollingText; + var funnyScroll3:BGScrollingText; + + var bgDad:FlxSprite; + var cardGlow:FlxSprite; + + var fromResultsParams:Null = null; + + var prepForNewRank:Bool = false; + public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER; + fromResultsParams = params?.fromResults; + + if (fromResultsParams?.playRankAnim == true) + { + prepForNewRank = true; + } + if (stickers != null) { stickerSubState = stickers; @@ -155,6 +230,12 @@ class FreeplayState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; + // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere + funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); + funnyCam.bgColor = FlxColor.TRANSPARENT; + FlxG.cameras.add(funnyCam, false); + this.cameras = [funnyCam]; + if (stickerSubState != null) { this.persistentUpdate = true; @@ -175,28 +256,45 @@ class FreeplayState extends MusicBeatSubState isDebug = true; #end - FunkinSound.playMusic('freakyMenu', - { - overrideExisting: true, - restartTrack: false - }); + if (prepForNewRank == false) + { + FunkinSound.playMusic('freakyMenu', + { + overrideExisting: true, + restartTrack: false + }); + } // Add a null entry that represents the RANDOM option songs.push(null); - // TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later. - // Default character (BF) shows default and Erect variations. Pico shows only Pico variations. - displayedVariations = (currentCharacter == 'bf') ? [Constants.DEFAULT_VARIATION, 'erect'] : [currentCharacter]; - // programmatically adds the songs via LevelRegistry and SongRegistry for (levelId in LevelRegistry.instance.listSortedLevelIds()) { - for (songId in LevelRegistry.instance.parseEntryData(levelId).songs) + var level:Level = LevelRegistry.instance.fetchEntry(levelId); + + if (level == null) + { + trace('[WARN] Could not find level with id (${levelId})'); + continue; + } + + for (songId in level.getSongs()) { var song:Song = SongRegistry.instance.fetchEntry(songId); - // Only display songs which actually have available charts for the current character. + if (song == null) + { + trace('[WARN] Could not find song with id (${songId})'); + continue; + } + + // Only display songs which actually have available difficulties for the current character. + var displayedVariations = song.getVariationsByCharId(currentCharacter); + trace(songId); + trace(displayedVariations); var availableDifficultiesForSong:Array = song.listDifficulties(displayedVariations, false); + trace(availableDifficultiesForSong); if (availableDifficultiesForSong.length == 0) continue; songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); @@ -216,17 +314,17 @@ class FreeplayState extends MusicBeatSubState trace(FlxG.camera.initialZoom); trace(FlxCamera.defaultZoom); - var pinkBack:FunkinSprite = FunkinSprite.create('freeplay/pinkBack'); + pinkBack = FunkinSprite.create('freeplay/pinkBack'); pinkBack.color = 0xFFFFD4E9; // sets it to pink! pinkBack.x -= pinkBack.width; FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); add(pinkBack); - var orangeBackShit:FunkinSprite = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); + orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); add(orangeBackShit); - var alsoOrangeLOL:FunkinSprite = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); + alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); add(alsoOrangeLOL); exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL], @@ -241,13 +339,30 @@ class FreeplayState extends MusicBeatSubState orangeBackShit.visible = false; alsoOrangeLOL.visible = false; + confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText')); + confirmTextGlow.blend = BlendMode.ADD; + confirmTextGlow.visible = false; + + confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow')); + confirmGlow.blend = BlendMode.ADD; + + confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2')); + + confirmGlow.visible = false; + confirmGlow2.visible = false; + + add(confirmGlow2); + add(confirmGlow); + + add(confirmTextGlow); + var grpTxtScrolls:FlxGroup = new FlxGroup(); add(grpTxtScrolls); grpTxtScrolls.visible = false; FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ['x', 'y', 'speed', 'size'])); - var moreWays:BGScrollingText = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43); + moreWays = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43); moreWays.funnyColor = 0xFFFFF383; moreWays.speed = 6.8; grpTxtScrolls.add(moreWays); @@ -258,7 +373,7 @@ class FreeplayState extends MusicBeatSubState speed: 0.4, }); - var funnyScroll:BGScrollingText = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60); + funnyScroll = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60); funnyScroll.funnyColor = 0xFFFF9963; funnyScroll.speed = -3.8; grpTxtScrolls.add(funnyScroll); @@ -271,7 +386,7 @@ class FreeplayState extends MusicBeatSubState wait: 0 }); - var txtNuts:BGScrollingText = new BGScrollingText(0, 285, 'PROTECT YO NUTS', FlxG.width / 2, true, 43); + txtNuts = new BGScrollingText(0, 285, 'PROTECT YO NUTS', FlxG.width / 2, true, 43); txtNuts.speed = 3.5; grpTxtScrolls.add(txtNuts); exitMovers.set([txtNuts], @@ -280,7 +395,7 @@ class FreeplayState extends MusicBeatSubState speed: 0.4, }); - var funnyScroll2:BGScrollingText = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60); + funnyScroll2 = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60); funnyScroll2.funnyColor = 0xFFFF9963; funnyScroll2.speed = -3.8; grpTxtScrolls.add(funnyScroll2); @@ -291,7 +406,7 @@ class FreeplayState extends MusicBeatSubState speed: 0.5, }); - var moreWays2:BGScrollingText = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43); + moreWays2 = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43); moreWays2.funnyColor = 0xFFFFF383; moreWays2.speed = 6.8; grpTxtScrolls.add(moreWays2); @@ -302,7 +417,7 @@ class FreeplayState extends MusicBeatSubState speed: 0.4 }); - var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60); + funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60); funnyScroll3.funnyColor = 0xFFFEA400; funnyScroll3.speed = -3.8; grpTxtScrolls.add(funnyScroll3); @@ -313,6 +428,24 @@ class FreeplayState extends MusicBeatSubState speed: 0.3 }); + backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"), + { + FrameRate: 24.0, + Reversed: false, + // ?OnComplete:Void -> Void, + ShowPivot: false, + Antialiasing: true, + ScrollFactor: new FlxPoint(1, 1), + }); + + add(backingTextYeah); + + cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow')); + cardGlow.blend = BlendMode.ADD; + cardGlow.visible = false; + + add(cardGlow); + dj = new DJBoyfriend(640, 366); exitMovers.set([dj], { @@ -325,15 +458,20 @@ class FreeplayState extends MusicBeatSubState add(dj); - var bgDad:FlxSprite = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); - bgDad.setGraphicSize(0, FlxG.height); - bgDad.updateHitbox(); + bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); bgDad.shader = new AngleMask(); bgDad.visible = false; var blackOverlayBullshitLOLXD:FlxSprite = new FlxSprite(FlxG.width).makeGraphic(Std.int(bgDad.width), Std.int(bgDad.height), FlxColor.BLACK); add(blackOverlayBullshitLOLXD); // used to mask the text lol! + // this makes the texture sizes consistent, for the angle shader + bgDad.setGraphicSize(0, FlxG.height); + blackOverlayBullshitLOLXD.setGraphicSize(0, FlxG.height); + + bgDad.updateHitbox(); + blackOverlayBullshitLOLXD.updateHitbox(); + exitMovers.set([blackOverlayBullshitLOLXD, bgDad], { x: FlxG.width * 1.5, @@ -342,10 +480,14 @@ class FreeplayState extends MusicBeatSubState }); add(bgDad); - FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.75}, 0.7, {ease: FlxEase.quintOut}); + FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.74}, 0.7, {ease: FlxEase.quintOut}); blackOverlayBullshitLOLXD.shader = bgDad.shader; + rankBg = new FunkinSprite(0, 0); + rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xD3000000); + add(rankBg); + grpSongs = new FlxTypedGroup(); add(grpSongs); @@ -455,6 +597,8 @@ class FreeplayState extends MusicBeatSubState generateSongList({filterType: FAVORITE}, true); case 'ALL': generateSongList(null, true); + case '#': + generateSongList({filterType: REGEXP, filterData: '0-9'}, true); default: generateSongList({filterType: REGEXP, filterData: str}, true); } @@ -463,6 +607,7 @@ class FreeplayState extends MusicBeatSubState // that is, only if there's more than one song in the group! if (grpCapsules.members.length > 0) { + FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); curSelected = 1; changeSelection(); } @@ -488,10 +633,6 @@ class FreeplayState extends MusicBeatSubState albumRoll.playIntro(); - new FlxTimer().start(0.75, function(_) { - // albumRoll.showTitle(); - }); - FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); diffSelLeft.visible = true; @@ -527,18 +668,48 @@ class FreeplayState extends MusicBeatSubState orangeBackShit.visible = true; alsoOrangeLOL.visible = true; grpTxtScrolls.visible = true; + + // render optimisation + if (_parentState != null) _parentState.persistentDraw = false; + + cardGlow.visible = true; + FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut}); + + if (prepForNewRank) + { + rankAnimStart(fromResultsParams); + } }); generateSongList(null, false); // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere - var funnyCam:FunkinCamera = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); + funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); funnyCam.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(funnyCam, false); + rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette')); + rankVignette.scale.set(2, 2); + rankVignette.updateHitbox(); + rankVignette.blend = BlendMode.ADD; + // rankVignette.cameras = [rankCamera]; + add(rankVignette); + rankVignette.alpha = 0; + forEach(function(bs) { bs.cameras = [funnyCam]; }); + + rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height); + rankCamera.bgColor = FlxColor.TRANSPARENT; + FlxG.cameras.add(rankCamera, false); + rankBg.cameras = [rankCamera]; + rankBg.alpha = 0; + + if (prepForNewRank) + { + rankCamera.fade(0xFF000000, 0, false, null, true); + } } var currentFilter:SongFilter = null; @@ -585,6 +756,7 @@ class FreeplayState extends MusicBeatSubState for (cap in grpCapsules.members) { + cap.songText.resetText(); cap.kill(); } @@ -602,9 +774,12 @@ class FreeplayState extends MusicBeatSubState }; randomCapsule.y = randomCapsule.intendedY(0) + 10; randomCapsule.targetPos.x = randomCapsule.x; - randomCapsule.alpha = 0.5; + randomCapsule.alpha = 0; randomCapsule.songText.visible = false; randomCapsule.favIcon.visible = false; + randomCapsule.favIconBlurred.visible = false; + randomCapsule.ranking.visible = false; + randomCapsule.blurredRanking.visible = false; randomCapsule.initJumpIn(0, force); randomCapsule.hsvShader = hsvShader; grpCapsules.add(randomCapsule); @@ -625,8 +800,11 @@ class FreeplayState extends MusicBeatSubState funnyMenu.capsule.alpha = 0.5; funnyMenu.songText.visible = false; funnyMenu.favIcon.visible = tempSongs[i].isFav; + funnyMenu.favIconBlurred.visible = tempSongs[i].isFav; funnyMenu.hsvShader = hsvShader; + funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45); + funnyMenu.checkClip(); funnyMenu.forcePosition(); grpCapsules.add(funnyMenu); @@ -648,6 +826,13 @@ class FreeplayState extends MusicBeatSubState */ public function sortSongs(songsToFilter:Array, songFilter:SongFilter):Array { + var filterAlphabetically = function(a:FreeplaySongData, b:FreeplaySongData):Int { + if (a?.songName.toLowerCase() < b?.songName.toLowerCase()) return -1; + else if (a?.songName.toLowerCase() > b?.songName.toLowerCase()) return 1; + else + return 0; + }; + switch (songFilter.filterType) { case REGEXP: @@ -662,6 +847,8 @@ class FreeplayState extends MusicBeatSubState return filterRegexp.match(str.songName); }); + songsToFilter.sort(filterAlphabetically); + case STARTSWITH: // extra note: this is essentially a "search" @@ -676,12 +863,307 @@ class FreeplayState extends MusicBeatSubState if (str == null) return true; // Random return str.isFav; }); + + songsToFilter.sort(filterAlphabetically); + default: // return all on default } + return songsToFilter; } + var sparks:FlxSprite; + var sparksADD:FlxSprite; + + function rankAnimStart(fromResults:Null):Void + { + busy = true; + grpCapsules.members[curSelected].sparkle.alpha = 0; + // grpCapsules.members[curSelected].forcePosition(); + + if (fromResults != null) + { + rememberedSongId = fromResults.songId; + rememberedDifficulty = fromResults.difficultyId; + changeSelection(); + changeDiff(); + } + + dj.fistPump(); + // 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; + rankBg.alpha = 1; + + if (fromResults?.oldRank != null) + { + grpCapsules.members[curSelected].fakeRanking.rank = fromResults.oldRank; + grpCapsules.members[curSelected].fakeBlurredRanking.rank = fromResults.oldRank; + + sparks = new FlxSprite(0, 0); + sparks.frames = Paths.getSparrowAtlas('freeplay/sparks'); + sparks.animation.addByPrefix('sparks', 'sparks', 24, false); + sparks.visible = false; + sparks.blend = BlendMode.ADD; + sparks.setPosition(517, 134); + sparks.scale.set(0.5, 0.5); + add(sparks); + sparks.cameras = [rankCamera]; + + sparksADD = new FlxSprite(0, 0); + sparksADD.visible = false; + sparksADD.frames = Paths.getSparrowAtlas('freeplay/sparksadd'); + sparksADD.animation.addByPrefix('sparks add', 'sparks add', 24, false); + sparksADD.setPosition(498, 116); + sparksADD.blend = BlendMode.ADD; + sparksADD.scale.set(0.5, 0.5); + add(sparksADD); + sparksADD.cameras = [rankCamera]; + + switch (fromResults.oldRank) + { + case SHIT: + sparksADD.color = 0xFF6044FF; + case GOOD: + sparksADD.color = 0xFFEF8764; + case GREAT: + sparksADD.color = 0xFFEAF6FF; + case EXCELLENT: + sparksADD.color = 0xFFFDCB42; + case PERFECT: + sparksADD.color = 0xFFFF58B4; + case PERFECT_GOLD: + sparksADD.color = 0xFFFFB619; + } + // sparksADD.color = sparks.color; + } + + grpCapsules.members[curSelected].doLerp = false; + + // originalPos.x = grpCapsules.members[curSelected].x; + // originalPos.y = grpCapsules.members[curSelected].y; + + originalPos.x = 320.488; + originalPos.y = 235.6; + trace(originalPos); + + grpCapsules.members[curSelected].ranking.visible = false; + grpCapsules.members[curSelected].blurredRanking.visible = false; + + rankCamera.zoom = 1.85; + FlxTween.tween(rankCamera, {"zoom": 1.8}, 0.6, {ease: FlxEase.sineIn}); + + funnyCam.zoom = 1.15; + FlxTween.tween(funnyCam, {"zoom": 1.1}, 0.6, {ease: FlxEase.sineIn}); + + grpCapsules.members[curSelected].cameras = [rankCamera]; + // grpCapsules.members[curSelected].targetPos.set((FlxG.width / 2) - (grpCapsules.members[curSelected].width / 2), + // (FlxG.height / 2) - (grpCapsules.members[curSelected].height / 2)); + + grpCapsules.members[curSelected].setPosition((FlxG.width / 2) - (grpCapsules.members[curSelected].width / 2), + (FlxG.height / 2) - (grpCapsules.members[curSelected].height / 2)); + + new FlxTimer().start(0.5, _ -> { + rankDisplayNew(fromResults); + }); + } + + function rankDisplayNew(fromResults:Null):Void + { + grpCapsules.members[curSelected].ranking.visible = true; + grpCapsules.members[curSelected].blurredRanking.visible = true; + grpCapsules.members[curSelected].ranking.scale.set(20, 20); + grpCapsules.members[curSelected].blurredRanking.scale.set(20, 20); + + if (fromResults?.newRank != null) + { + grpCapsules.members[curSelected].ranking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true); + } + + FlxTween.tween(grpCapsules.members[curSelected].ranking, {"scale.x": 1, "scale.y": 1}, 0.1); + + if (fromResults?.newRank != null) + { + grpCapsules.members[curSelected].blurredRanking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true); + } + FlxTween.tween(grpCapsules.members[curSelected].blurredRanking, {"scale.x": 1, "scale.y": 1}, 0.1); + + new FlxTimer().start(0.1, _ -> { + if (fromResults?.oldRank != null) + { + grpCapsules.members[curSelected].fakeRanking.visible = false; + grpCapsules.members[curSelected].fakeBlurredRanking.visible = false; + + sparks.visible = true; + sparksADD.visible = true; + sparks.animation.play('sparks', true); + sparksADD.animation.play('sparks add', true); + + sparks.animation.finishCallback = anim -> { + sparks.visible = false; + sparksADD.visible = false; + }; + } + + switch (fromResultsParams?.newRank) + { + case SHIT: + FunkinSound.playOnce(Paths.sound('ranks/rankinbad')); + case PERFECT: + FunkinSound.playOnce(Paths.sound('ranks/rankinperfect')); + case PERFECT_GOLD: + FunkinSound.playOnce(Paths.sound('ranks/rankinperfect')); + default: + FunkinSound.playOnce(Paths.sound('ranks/rankinnormal')); + } + rankCamera.zoom = 1.3; + + FlxTween.tween(rankCamera, {"zoom": 1.5}, 0.3, {ease: FlxEase.backInOut}); + + grpCapsules.members[curSelected].x -= 10; + grpCapsules.members[curSelected].y -= 20; + + FlxTween.tween(funnyCam, {"zoom": 1.05}, 0.3, {ease: FlxEase.elasticOut}); + + grpCapsules.members[curSelected].capsule.angle = -3; + FlxTween.tween(grpCapsules.members[curSelected].capsule, {angle: 0}, 0.5, {ease: FlxEase.backOut}); + + IntervalShake.shake(grpCapsules.members[curSelected].capsule, 0.3, 1 / 30, 0.1, 0, FlxEase.quadOut); + }); + + new FlxTimer().start(0.4, _ -> { + FlxTween.tween(funnyCam, {"zoom": 1}, 0.8, {ease: FlxEase.sineIn}); + FlxTween.tween(rankCamera, {"zoom": 1.2}, 0.8, {ease: FlxEase.backIn}); + FlxTween.tween(grpCapsules.members[curSelected], {x: originalPos.x - 7, y: originalPos.y - 80}, 0.8 + 0.5, {ease: FlxEase.quartIn}); + }); + + new FlxTimer().start(0.6, _ -> { + rankAnimSlam(fromResults); + }); + } + + function rankAnimSlam(fromResultsParams:Null) + { + // FlxTween.tween(rankCamera, {"zoom": 1.9}, 0.5, {ease: FlxEase.backOut}); + FlxTween.tween(rankBg, {alpha: 0}, 0.5, {ease: FlxEase.expoIn}); + + // FlxTween.tween(grpCapsules.members[curSelected], {angle: 5}, 0.5, {ease: FlxEase.backIn}); + + switch (fromResultsParams?.newRank) + { + case SHIT: + FunkinSound.playOnce(Paths.sound('ranks/loss')); + case GOOD: + FunkinSound.playOnce(Paths.sound('ranks/good')); + case GREAT: + FunkinSound.playOnce(Paths.sound('ranks/great')); + case EXCELLENT: + FunkinSound.playOnce(Paths.sound('ranks/excellent')); + case PERFECT: + FunkinSound.playOnce(Paths.sound('ranks/perfect')); + case PERFECT_GOLD: + FunkinSound.playOnce(Paths.sound('ranks/perfect')); + default: + FunkinSound.playOnce(Paths.sound('ranks/loss')); + } + + FlxTween.tween(grpCapsules.members[curSelected], {"targetPos.x": originalPos.x, "targetPos.y": originalPos.y}, 0.5, {ease: FlxEase.expoOut}); + new FlxTimer().start(0.5, _ -> { + funnyCam.shake(0.0045, 0.35); + + if (fromResultsParams?.newRank == SHIT) + { + dj.pumpFistBad(); + } + else + { + dj.pumpFist(); + } + + rankCamera.zoom = 0.8; + funnyCam.zoom = 0.8; + FlxTween.tween(rankCamera, {"zoom": 1}, 1, {ease: FlxEase.elasticOut}); + FlxTween.tween(funnyCam, {"zoom": 1}, 0.8, {ease: FlxEase.elasticOut}); + + for (index => capsule in grpCapsules.members) + { + var distFromSelected:Float = Math.abs(index - curSelected) - 1; + + if (distFromSelected < 5) + { + if (index == curSelected) + { + FlxTween.cancelTweensOf(capsule); + // capsule.targetPos.x += 50; + capsule.fadeAnim(); + + rankVignette.color = capsule.getTrailColor(); + rankVignette.alpha = 1; + FlxTween.tween(rankVignette, {alpha: 0}, 0.6, {ease: FlxEase.expoOut}); + + capsule.doLerp = false; + capsule.setPosition(originalPos.x, originalPos.y); + IntervalShake.shake(capsule, 0.6, 1 / 24, 0.12, 0, FlxEase.quadOut, function(_) { + capsule.doLerp = true; + capsule.cameras = [funnyCam]; + + // NOW we can interact with the menu + busy = false; + grpCapsules.members[curSelected].sparkle.alpha = 0.7; + playCurSongPreview(capsule); + }, null); + + // FlxTween.tween(capsule, {"targetPos.x": capsule.targetPos.x - 50}, 0.6, + // { + // ease: FlxEase.backInOut, + // onComplete: function(_) { + // capsule.cameras = [funnyCam]; + // } + // }); + FlxTween.tween(capsule, {angle: 0}, 0.5, {ease: FlxEase.backOut}); + } + if (index > curSelected) + { + // capsule.color = FlxColor.RED; + new FlxTimer().start(distFromSelected / 20, _ -> { + capsule.doLerp = false; + + capsule.capsule.angle = FlxG.random.float(-10 + (distFromSelected * 2), 10 - (distFromSelected * 2)); + FlxTween.tween(capsule.capsule, {angle: 0}, 0.5, {ease: FlxEase.backOut}); + + IntervalShake.shake(capsule, 0.6, 1 / 24, 0.12 / (distFromSelected + 1), 0, FlxEase.quadOut, function(_) { + capsule.doLerp = true; + }); + }); + } + + if (index < curSelected) + { + // capsule.color = FlxColor.BLUE; + new FlxTimer().start(distFromSelected / 20, _ -> { + capsule.doLerp = false; + + capsule.capsule.angle = FlxG.random.float(-10 + (distFromSelected * 2), 10 - (distFromSelected * 2)); + FlxTween.tween(capsule.capsule, {angle: 0}, 0.5, {ease: FlxEase.backOut}); + + IntervalShake.shake(capsule, 0.6, 1 / 24, 0.12 / (distFromSelected + 1), 0, FlxEase.quadOut, function(_) { + capsule.doLerp = true; + }); + }); + } + } + + index += 1; + } + }); + + new FlxTimer().start(2, _ -> { + // dj.fistPump(); + prepForNewRank = false; + }); + } + var touchY:Float = 0; var touchX:Float = 0; var dxTouch:Float = 0; @@ -696,45 +1178,95 @@ class FreeplayState extends MusicBeatSubState var spamTimer:Float = 0; var spamming:Bool = false; - var busy:Bool = false; // Set to true once the user has pressed enter to select a song. + /** + * If true, disable interaction with the interface. + */ + var busy:Bool = false; + + var originalPos:FlxPoint = new FlxPoint(); override function update(elapsed:Float):Void { super.update(elapsed); - if (FlxG.keys.justPressed.F) + #if debug + if (FlxG.keys.justPressed.T) + { + rankAnimStart(fromResultsParams); + } + + // if (FlxG.keys.justPressed.H) + // { + // rankDisplayNew(fromResultsParams); + // } + + // if (FlxG.keys.justPressed.G) + // { + // rankAnimSlam(fromResultsParams); + // } + #end + + if (controls.FREEPLAY_FAVORITE && !busy) { var targetSong = grpCapsules.members[curSelected]?.songData; if (targetSong != null) { var realShit:Int = curSelected; - targetSong.isFav = !targetSong.isFav; - if (targetSong.isFav) + var isFav = targetSong.toggleFavorite(); + if (isFav) { - FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, + grpCapsules.members[realShit].favIcon.visible = true; + grpCapsules.members[realShit].favIconBlurred.visible = true; + grpCapsules.members[realShit].favIcon.animation.play('fav'); + grpCapsules.members[realShit].favIconBlurred.animation.play('fav'); + FunkinSound.playOnce(Paths.sound('fav'), 1); + grpCapsules.members[realShit].checkClip(); + grpCapsules.members[realShit].selected = grpCapsules.members[realShit].selected; // set selected again, so it can run it's getter function to initialize movement + busy = true; + + grpCapsules.members[realShit].doLerp = false; + FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y - 5}, 0.1, {ease: FlxEase.expoOut}); + + FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y + 5}, 0.1, { - ease: FlxEase.elasticOut, - onComplete: _ -> { - grpCapsules.members[realShit].favIcon.visible = true; - grpCapsules.members[realShit].favIcon.animation.play('fav'); + ease: FlxEase.expoIn, + startDelay: 0.1, + onComplete: function(_) { + grpCapsules.members[realShit].doLerp = true; + busy = false; } }); } else { - grpCapsules.members[realShit].favIcon.animation.play('fav', false, true); - new FlxTimer().start((1 / 24) * 14, _ -> { + grpCapsules.members[realShit].favIcon.animation.play('fav', true, true, 9); + grpCapsules.members[realShit].favIconBlurred.animation.play('fav', true, true, 9); + FunkinSound.playOnce(Paths.sound('unfav'), 1); + new FlxTimer().start(0.2, _ -> { grpCapsules.members[realShit].favIcon.visible = false; + grpCapsules.members[realShit].favIconBlurred.visible = false; + grpCapsules.members[realShit].checkClip(); }); - new FlxTimer().start((1 / 24) * 24, _ -> { - FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut}); - }); + + busy = true; + grpCapsules.members[realShit].doLerp = false; + FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y + 5}, 0.1, {ease: FlxEase.expoOut}); + + FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y - 5}, 0.1, + { + ease: FlxEase.expoIn, + startDelay: 0.1, + onComplete: function(_) { + grpCapsules.members[realShit].doLerp = true; + busy = false; + } + }); } } } - lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2); - lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9); + lerpScore = MathUtil.smoothLerp(lerpScore, intendedScore, elapsed, 0.5); + lerpCompletion = MathUtil.smoothLerp(lerpCompletion, intendedCompletion, elapsed, 0.5); if (Math.isNaN(lerpScore)) { @@ -889,11 +1421,24 @@ class FreeplayState extends MusicBeatSubState spamTimer = 0; } + #if !html5 if (FlxG.mouse.wheel != 0) { dj.resetAFKTimer(); - changeSelection(-Math.round(FlxG.mouse.wheel / 4)); + changeSelection(-Math.round(FlxG.mouse.wheel)); } + #else + if (FlxG.mouse.wheel < 0) + { + dj.resetAFKTimer(); + changeSelection(-Math.round(FlxG.mouse.wheel / 8)); + } + else if (FlxG.mouse.wheel > 0) + { + dj.resetAFKTimer(); + changeSelection(-Math.round(FlxG.mouse.wheel / 8)); + } + #end if (controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) { @@ -910,6 +1455,7 @@ class FreeplayState extends MusicBeatSubState if (controls.BACK) { + busy = true; FlxTween.globalManager.clear(); FlxTimer.globalManager.clear(); dj.onIntroDone.removeAll(); @@ -918,6 +1464,24 @@ class FreeplayState extends MusicBeatSubState var longestTimer:Float = 0; + // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut}); + FlxTween.color(pinkBack, 0.25, 0xFFFFD863, 0xFFFFD0D5, {ease: FlxEase.quadOut}); + + cardGlow.visible = true; + cardGlow.alpha = 1; + cardGlow.scale.set(1, 1); + FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.25, {ease: FlxEase.sineOut}); + + orangeBackShit.visible = false; + alsoOrangeLOL.visible = false; + + moreWays.visible = false; + funnyScroll.visible = false; + txtNuts.visible = false; + funnyScroll2.visible = false; + moreWays2.visible = false; + funnyScroll3.visible = false; + for (grpSpr in exitMovers.keys()) { var moveData:MoveData = exitMovers.get(grpSpr); @@ -962,6 +1526,7 @@ class FreeplayState extends MusicBeatSubState overrideExisting: true, restartTrack: false }); + FlxG.sound.music.fadeIn(4.0, 0.0, 1.0); close(); } else @@ -985,6 +1550,8 @@ class FreeplayState extends MusicBeatSubState { clearDaCache(daSong.songName); } + // remove and destroy freeplay camera + FlxG.cameras.remove(funnyCam); } function changeDiff(change:Int = 0, force:Bool = false):Void @@ -1005,9 +1572,21 @@ class FreeplayState extends MusicBeatSubState var daSong:Null = grpCapsules.members[curSelected].songData; if (daSong != null) { - var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty); + // TODO: Make this actually be the variation you're focused on. We don't need to fetch the song metadata just to calculate it. + var targetSong:Song = SongRegistry.instance.fetchEntry(grpCapsules.members[curSelected].songData.songId); + if (targetSong == null) + { + FlxG.log.warn('WARN: could not find song with id (${grpCapsules.members[curSelected].songData.songId})'); + return; + } + var targetVariation:String = targetSong.getFirstValidVariation(currentDifficulty); + + // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. + var suffixedDifficulty = (targetVariation != Constants.DEFAULT_VARIATION + && targetVariation != 'erect') ? '$currentDifficulty-${targetVariation}' : currentDifficulty; + var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, suffixedDifficulty); intendedScore = songScore?.score ?? 0; - intendedCompletion = songScore?.accuracy ?? 0.0; + intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); rememberedDifficulty = currentDifficulty; } else @@ -1057,6 +1636,7 @@ class FreeplayState extends MusicBeatSubState { songCapsule.songData.currentDifficulty = currentDifficulty; songCapsule.init(null, null, songCapsule.songData); + songCapsule.checkClip(); } else { @@ -1072,6 +1652,9 @@ class FreeplayState extends MusicBeatSubState albumRoll.albumId = newAlbumId; albumRoll.skipIntro(); } + + // Set difficulty star count. + albumRoll.setDifficultyStars(daSong?.difficultyRating); } // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String) @@ -1136,22 +1719,71 @@ class FreeplayState extends MusicBeatSubState FlxG.log.warn('WARN: could not find song with id (${cap.songData.songId})'); return; } - var targetDifficulty:String = currentDifficulty; - var targetVariation:String = targetSong.getFirstValidVariation(targetDifficulty); - + var targetDifficultyId:String = currentDifficulty; + var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId); PlayStatePlaylist.campaignId = cap.songData.levelId; + var targetDifficulty:SongDifficulty = targetSong.getDifficulty(targetDifficultyId, targetVariation); + if (targetDifficulty == null) + { + FlxG.log.warn('WARN: could not find difficulty with id (${targetDifficultyId})'); + return; + } + + // TODO: Change this with alternate instrumentals + var targetInstId:String = targetDifficulty.characters.instrumental; + // Visual and audio effects. FunkinSound.playOnce(Paths.sound('confirmMenu')); dj.confirm(); + grpCapsules.members[curSelected].forcePosition(); + grpCapsules.members[curSelected].songText.flickerText(); + + // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut}); + FlxTween.color(pinkBack, 0.33, 0xFFFFD0D5, 0xFF171831, {ease: FlxEase.quadOut}); + orangeBackShit.visible = false; + alsoOrangeLOL.visible = false; + + confirmGlow.visible = true; + confirmGlow2.visible = true; + + backingTextYeah.anim.play("BF back card confirm raw", false, false, 0); + confirmGlow2.alpha = 0; + confirmGlow.alpha = 0; + + FlxTween.tween(confirmGlow2, {alpha: 0.5}, 0.33, + { + ease: FlxEase.quadOut, + onComplete: function(_) { + confirmGlow2.alpha = 0.6; + confirmGlow.alpha = 1; + confirmTextGlow.visible = true; + confirmTextGlow.alpha = 1; + FlxTween.tween(confirmTextGlow, {alpha: 0.4}, 0.5); + FlxTween.tween(confirmGlow, {alpha: 0}, 0.5); + } + }); + + // confirmGlow + + moreWays.visible = false; + funnyScroll.visible = false; + txtNuts.visible = false; + funnyScroll2.visible = false; + moreWays2.visible = false; + funnyScroll3.visible = false; + new FlxTimer().start(1, function(tmr:FlxTimer) { + FunkinSound.emptyPartialQueue(); + Paths.setCurrentLevel(cap.songData.levelId); LoadingState.loadPlayState( { targetSong: targetSong, - targetDifficulty: targetDifficulty, + targetDifficulty: targetDifficultyId, targetVariation: targetVariation, + targetInstrumental: targetInstId, practiceMode: false, minimalMode: false, @@ -1188,12 +1820,12 @@ class FreeplayState extends MusicBeatSubState function changeSelection(change:Int = 0):Void { - FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); - var prevSelected:Int = curSelected; curSelected += change; + if (!prepForNewRank && curSelected != prevSelected) FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); + if (curSelected < 0) curSelected = grpCapsules.countLiving() - 1; if (curSelected >= grpCapsules.countLiving()) curSelected = 0; @@ -1202,7 +1834,7 @@ class FreeplayState extends MusicBeatSubState { var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty); intendedScore = songScore?.score ?? 0; - intendedCompletion = songScore?.accuracy ?? 0.0; + intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); diffIdsCurrent = daSongCapsule.songData.songDifficulties; rememberedSongId = daSongCapsule.songData.songId; changeDiff(); @@ -1229,49 +1861,58 @@ class FreeplayState extends MusicBeatSubState if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure } - if (grpCapsules.countLiving() > 0) + if (grpCapsules.countLiving() > 0 && !prepForNewRank) { - if (curSelected == 0) - { - FunkinSound.playMusic('freeplayRandom', - { - startingVolume: 0.0, - overrideExisting: true, - restartTrack: true - }); - FlxG.sound.music.fadeIn(2, 0, 0.8); - } - else - { - // TODO: Stream the instrumental of the selected song? - var didReplace:Bool = FunkinSound.playMusic('freakyMenu', - { - startingVolume: 0.0, - overrideExisting: true, - restartTrack: false - }); - if (didReplace) - { - FunkinSound.playMusic('freakyMenu', - { - startingVolume: 0.0, - overrideExisting: true, - restartTrack: false - }); - FlxG.sound.music.fadeIn(2, 0, 0.8); - } - } + playCurSongPreview(daSongCapsule); grpCapsules.members[curSelected].selected = true; } } + public function playCurSongPreview(daSongCapsule:SongMenuItem):Void + { + if (curSelected == 0) + { + FunkinSound.playMusic('freeplayRandom', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: false + }); + FlxG.sound.music.fadeIn(2, 0, 0.8); + } + else + { + var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : ""; + FunkinSound.playMusic(daSongCapsule.songData.songId, + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: false, + pathsFunction: INST, + suffix: potentiallyErect, + partialParams: + { + loadPartial: true, + start: 0.05, + end: 0.25 + }, + onLoad: function() { + FlxG.sound.music.fadeIn(2, 0, 0.4); + } + }); + } + } + /** * Build an instance of `FreeplayState` that is above the `MainMenuState`. * @return The MainMenuState with the FreeplayState as a substate. */ public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState { - var result = new MainMenuState(); + var result:MainMenuState; + if (params?.fromResults.playRankAnim) result = new MainMenuState(true); + else + result = new MainMenuState(false); result.openSubState(new FreeplayState(params, stickers)); result.persistentUpdate = false; @@ -1374,6 +2015,8 @@ class FreeplaySongData */ public var isFav:Bool = false; + public var isNew:Bool = false; + var song:Song; public var levelId(default, null):String = ''; @@ -1383,16 +2026,18 @@ class FreeplaySongData public var songName(default, null):String = ''; public var songCharacter(default, null):String = ''; - public var songRating(default, null):Int = 0; + public var songStartingBpm(default, null):Float = 0; + public var difficultyRating(default, null):Int = 0; public var albumId(default, null):Null = null; public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; - public var displayedVariations(default, null):Array = [Constants.DEFAULT_VARIATION]; + + public var scoringRank:Null = null; + + var displayedVariations:Array = [Constants.DEFAULT_VARIATION]; function set_currentDifficulty(value:String):String { - if (currentDifficulty == value) return value; - currentDifficulty = value; updateValues(displayedVariations); return value; @@ -1403,21 +2048,43 @@ class FreeplaySongData this.levelId = levelId; this.songId = songId; this.song = song; + + this.isFav = Save.instance.isSongFavorited(songId); + if (displayedVariations != null) this.displayedVariations = displayedVariations; updateValues(displayedVariations); } + /** + * Toggle whether or not the song is favorited, then flush to save data. + * @return Whether or not the song is now favorited. + */ + public function toggleFavorite():Bool + { + isFav = !isFav; + if (isFav) + { + Save.instance.favoriteSong(this.songId); + } + else + { + Save.instance.unfavoriteSong(this.songId); + } + return isFav; + } + function updateValues(variations:Array):Void { - this.songDifficulties = song.listDifficulties(variations, false, false); + this.songDifficulties = song.listDifficulties(null, variations, false, false); if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY; - var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations); + var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations); if (songDifficulty == null) return; + this.songStartingBpm = songDifficulty.getStartingBPM(); this.songName = songDifficulty.songName; this.songCharacter = songDifficulty.characters.opponent; - this.songRating = songDifficulty.difficultyRating; + this.difficultyRating = songDifficulty.difficultyRating; if (songDifficulty.album == null) { FlxG.log.warn('No album for: ${songDifficulty.songName}'); @@ -1427,6 +2094,10 @@ class FreeplaySongData { this.albumId = songDifficulty.album; } + + this.scoringRank = Save.instance.getSongRank(songId, currentDifficulty); + + this.isNew = song.isSongNew(currentDifficulty); } } diff --git a/source/funkin/ui/freeplay/LetterSort.hx b/source/funkin/ui/freeplay/LetterSort.hx index e813c9198..049e9194a 100644 --- a/source/funkin/ui/freeplay/LetterSort.hx +++ b/source/funkin/ui/freeplay/LetterSort.hx @@ -8,6 +8,7 @@ import flixel.tweens.FlxTween; import flixel.tweens.FlxEase; import flixel.util.FlxColor; import flixel.util.FlxTimer; +import funkin.input.Controls; import funkin.graphics.adobeanimate.FlxAtlasSprite; class LetterSort extends FlxTypedSpriteGroup @@ -69,14 +70,19 @@ class LetterSort extends FlxTypedSpriteGroup changeSelection(0); } + var controls(get, never):Controls; + + inline function get_controls():Controls + return PlayerSettings.player1.controls; + override function update(elapsed:Float):Void { super.update(elapsed); if (inputEnabled) { - if (FlxG.keys.justPressed.E) changeSelection(1); - if (FlxG.keys.justPressed.Q) changeSelection(-1); + if (controls.FREEPLAY_LEFT) changeSelection(-1); + if (controls.FREEPLAY_RIGHT) changeSelection(1); } } diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index f6d85e56e..7708b3bcf 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -14,6 +14,16 @@ import flixel.text.FlxText; import flixel.util.FlxTimer; import funkin.util.MathUtil; import funkin.graphics.shaders.Grayscale; +import funkin.graphics.shaders.GaussianBlurShader; +import openfl.display.BlendMode; +import funkin.graphics.FunkinSprite; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.addons.effects.FlxTrail; +import funkin.play.scoring.Scoring.ScoringRank; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; +import flixel.util.FlxColor; class SongMenuItem extends FlxSpriteGroup { @@ -30,10 +40,16 @@ class SongMenuItem extends FlxSpriteGroup public var selected(default, set):Bool; public var songText:CapsuleText; + public var favIconBlurred:FlxSprite; public var favIcon:FlxSprite; - public var ranking:FlxSprite; - var ranks:Array = ["fail", "average", "great", "excellent", "perfect"]; + public var ranking:FreeplayRank; + public var blurredRanking:FreeplayRank; + + public var fakeRanking:FreeplayRank; + public var fakeBlurredRanking:FreeplayRank; + + var ranks:Array = ["fail", "average", "great", "excellent", "perfect", "perfectsick"]; public var targetPos:FlxPoint = new FlxPoint(); public var doLerp:Bool = false; @@ -47,6 +63,24 @@ class SongMenuItem extends FlxSpriteGroup public var hsvShader(default, set):HSVShader; // var diffRatingSprite:FlxSprite; + public var bpmText:FlxSprite; + public var difficultyText:FlxSprite; + public var weekType:FlxSprite; + + public var newText:FlxSprite; + + // public var weekType:FlxSprite; + public var bigNumbers:Array = []; + + public var smallNumbers:Array = []; + + public var weekNumbers:Array = []; + + var impactThing:FunkinSprite; + + public var sparkle:FlxSprite; + + var sparkleTimer:FlxTimer; public function new(x:Float, y:Float) { @@ -59,12 +93,84 @@ class SongMenuItem extends FlxSpriteGroup // capsule.animation add(capsule); + bpmText = new FlxSprite(144, 87).loadGraphic(Paths.image('freeplay/freeplayCapsule/bpmtext')); + bpmText.setGraphicSize(Std.int(bpmText.width * 0.9)); + add(bpmText); + + difficultyText = new FlxSprite(414, 87).loadGraphic(Paths.image('freeplay/freeplayCapsule/difficultytext')); + difficultyText.setGraphicSize(Std.int(difficultyText.width * 0.9)); + add(difficultyText); + + weekType = new FlxSprite(291, 87); + weekType.frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/weektypes'); + + weekType.animation.addByPrefix('WEEK', 'WEEK text instance 1', 24, false); + weekType.animation.addByPrefix('WEEKEND', 'WEEKEND text instance 1', 24, false); + + weekType.setGraphicSize(Std.int(weekType.width * 0.9)); + add(weekType); + + newText = new FlxSprite(454, 9); + newText.frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/new'); + newText.animation.addByPrefix('newAnim', 'NEW notif', 24, true); + newText.animation.play('newAnim', true); + newText.setGraphicSize(Std.int(newText.width * 0.9)); + + // newText.visible = false; + + add(newText); + + // var debugNumber2:CapsuleNumber = new CapsuleNumber(0, 0, true, 2); + // add(debugNumber2); + + for (i in 0...2) + { + var bigNumber:CapsuleNumber = new CapsuleNumber(466 + (i * 30), 32, true, 0); + add(bigNumber); + + bigNumbers.push(bigNumber); + } + + for (i in 0...3) + { + var smallNumber:CapsuleNumber = new CapsuleNumber(185 + (i * 11), 88.5, false, 0); + add(smallNumber); + + smallNumbers.push(smallNumber); + } + // doesn't get added, simply is here to help with visibility of things for the pop in! grpHide = new FlxGroup(); - var rank:String = FlxG.random.getObject(ranks); + fakeRanking = new FreeplayRank(420, 41); + add(fakeRanking); + + fakeBlurredRanking = new FreeplayRank(fakeRanking.x, fakeRanking.y); + fakeBlurredRanking.shader = new GaussianBlurShader(1); + add(fakeBlurredRanking); + + fakeRanking.visible = false; + fakeBlurredRanking.visible = false; + + ranking = new FreeplayRank(420, 41); + add(ranking); + + blurredRanking = new FreeplayRank(ranking.x, ranking.y); + blurredRanking.shader = new GaussianBlurShader(1); + add(blurredRanking); + + sparkle = new FlxSprite(ranking.x, ranking.y); + sparkle.frames = Paths.getSparrowAtlas('freeplay/sparkle'); + sparkle.animation.addByPrefix('sparkle', 'sparkle', 24, false); + sparkle.animation.play('sparkle', true); + sparkle.scale.set(0.8, 0.8); + sparkle.blend = BlendMode.ADD; + + sparkle.visible = false; + sparkle.alpha = 0.7; + + add(sparkle); - ranking = new FlxSprite(capsule.width * 0.84, 30); // ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank)); // ranking.scale.x = ranking.scale.y = realScaled; // ranking.alpha = 0.75; @@ -73,11 +179,11 @@ class SongMenuItem extends FlxSpriteGroup // add(ranking); // grpHide.add(ranking); - switch (rank) - { - case 'perfect': - ranking.x -= 10; - } + // switch (rank) + // { + // case 'perfect': + // ranking.x -= 10; + // } grayscaleShader = new Grayscale(1); @@ -93,7 +199,7 @@ class SongMenuItem extends FlxSpriteGroup grpHide.add(songText); // TODO: Use value from metadata instead of random. - updateDifficultyRating(FlxG.random.int(0, 15)); + updateDifficultyRating(FlxG.random.int(0, 20)); pixelIcon = new FlxSprite(160, 35); @@ -103,25 +209,263 @@ class SongMenuItem extends FlxSpriteGroup add(pixelIcon); grpHide.add(pixelIcon); - favIcon = new FlxSprite(400, 40); + favIconBlurred = new FlxSprite(380, 40); + favIconBlurred.frames = Paths.getSparrowAtlas('freeplay/favHeart'); + favIconBlurred.animation.addByPrefix('fav', 'favorite heart', 24, false); + favIconBlurred.animation.play('fav'); + favIconBlurred.setGraphicSize(50, 50); + favIconBlurred.blend = BlendMode.ADD; + favIconBlurred.shader = new GaussianBlurShader(1.2); + favIconBlurred.visible = false; + add(favIconBlurred); + + favIcon = new FlxSprite(favIconBlurred.x, favIconBlurred.y); favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart'); favIcon.animation.addByPrefix('fav', 'favorite heart', 24, false); favIcon.animation.play('fav'); favIcon.setGraphicSize(50, 50); favIcon.visible = false; + favIcon.blend = BlendMode.ADD; add(favIcon); - // grpHide.add(favIcon); + + var weekNumber:CapsuleNumber = new CapsuleNumber(355, 88.5, false, 0); + add(weekNumber); + + weekNumbers.push(weekNumber); setVisibleGrp(false); } + function sparkleEffect(timer:FlxTimer):Void + { + sparkle.setPosition(FlxG.random.float(ranking.x - 20, ranking.x + 3), FlxG.random.float(ranking.y - 29, ranking.y + 4)); + sparkle.animation.play('sparkle', true); + sparkleTimer = new FlxTimer().start(FlxG.random.float(1.2, 4.5), sparkleEffect); + } + + // no way to grab weeks rn, so this needs to be done :/ + // negative values mean weekends + function checkWeek(name:String):Void + { + // trace(name); + var weekNum:Int = 0; + switch (name) + { + case 'bopeebo' | 'fresh' | 'dadbattle': + weekNum = 1; + case 'spookeez' | 'south' | 'monster': + weekNum = 2; + case 'pico' | 'philly-nice' | 'blammed': + weekNum = 3; + case "satin-panties" | 'high' | 'milf': + weekNum = 4; + case "cocoa" | 'eggnog' | 'winter-horrorland': + weekNum = 5; + case 'senpai' | 'roses' | 'thorns': + weekNum = 6; + case 'ugh' | 'guns' | 'stress': + weekNum = 7; + case 'darnell' | 'lit-up' | '2hot' | 'blazin': + weekNum = -1; + default: + weekNum = 0; + } + + weekNumbers[0].digit = Std.int(Math.abs(weekNum)); + + if (weekNum == 0) + { + weekType.visible = false; + weekNumbers[0].visible = false; + } + else + { + weekType.visible = true; + weekNumbers[0].visible = true; + } + if (weekNum > 0) + { + weekType.animation.play('WEEK', true); + } + else + { + weekType.animation.play('WEEKEND', true); + weekNumbers[0].offset.x -= 35; + } + } + + /** + * Checks whether the song is favorited, and/or has a rank, and adjusts the clipping + * for the scenario when the text could be too long + */ + public function checkClip():Void + { + var clipSize:Int = 290; + var clipType:Int = 0; + + if (ranking.visible) + { + favIconBlurred.x = this.x + 370; + favIcon.x = favIconBlurred.x; + clipType += 1; + } + else + { + favIconBlurred.x = favIcon.x = this.x + 405; + } + + if (favIcon.visible) clipType += 1; + + switch (clipType) + { + case 2: + clipSize = 210; + case 1: + clipSize = 245; + } + songText.clipWidth = clipSize; + } + + function updateBPM(newBPM:Int):Void + { + var shiftX:Float = 191; + var tempShift:Float = 0; + + if (Math.floor(newBPM / 100) == 1) + { + shiftX = 186; + } + + for (i in 0...smallNumbers.length) + { + smallNumbers[i].x = this.x + (shiftX + (i * 11)); + switch (i) + { + case 0: + if (newBPM < 100) + { + smallNumbers[i].digit = 0; + } + else + { + smallNumbers[i].digit = Math.floor(newBPM / 100) % 10; + } + + case 1: + if (newBPM < 10) + { + smallNumbers[i].digit = 0; + } + else + { + smallNumbers[i].digit = Math.floor(newBPM / 10) % 10; + + if (Math.floor(newBPM / 10) % 10 == 1) tempShift = -4; + } + case 2: + smallNumbers[i].digit = newBPM % 10; + default: + trace('why the fuck is this being called'); + } + smallNumbers[i].x += tempShift; + } + // diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}')); + // diffRatingSprite.visible = false; + } + + var evilTrail:FlxTrail; + + public function fadeAnim():Void + { + impactThing = new FunkinSprite(0, 0); + impactThing.frames = capsule.frames; + impactThing.frame = capsule.frame; + impactThing.updateHitbox(); + // impactThing.x = capsule.x; + // impactThing.y = capsule.y; + // picoFade.stamp(this, 0, 0); + impactThing.alpha = 0; + impactThing.zIndex = capsule.zIndex - 3; + add(impactThing); + FlxTween.tween(impactThing.scale, {x: 2.5, y: 2.5}, 0.5); + // FlxTween.tween(impactThing, {alpha: 0}, 0.5); + + evilTrail = new FlxTrail(impactThing, null, 15, 2, 0.01, 0.069); + evilTrail.blend = BlendMode.ADD; + evilTrail.zIndex = capsule.zIndex - 5; + FlxTween.tween(evilTrail, {alpha: 0}, 0.6, + { + ease: FlxEase.quadOut, + onComplete: function(_) { + remove(evilTrail); + } + }); + add(evilTrail); + + switch (ranking.rank) + { + case SHIT: + evilTrail.color = 0xFF6044FF; + case GOOD: + evilTrail.color = 0xFFEF8764; + case GREAT: + evilTrail.color = 0xFFEAF6FF; + case EXCELLENT: + evilTrail.color = 0xFFFDCB42; + case PERFECT: + evilTrail.color = 0xFFFF58B4; + case PERFECT_GOLD: + evilTrail.color = 0xFFFFB619; + } + } + + public function getTrailColor():FlxColor + { + return evilTrail.color; + } + function updateDifficultyRating(newRating:Int):Void { var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating'; + + for (i in 0...bigNumbers.length) + { + switch (i) + { + case 0: + if (newRating < 10) + { + bigNumbers[i].digit = 0; + } + else + { + bigNumbers[i].digit = Math.floor(newRating / 10); + } + case 1: + bigNumbers[i].digit = newRating % 10; + default: + trace('why the fuck is this being called'); + } + } // diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}')); // diffRatingSprite.visible = false; } + function updateScoringRank(newRank:Null):Void + { + if (sparkleTimer != null) sparkleTimer.cancel(); + sparkle.visible = false; + + this.ranking.rank = newRank; + this.blurredRanking.rank = newRank; + + if (newRank == PERFECT_GOLD) + { + sparkleTimer = new FlxTimer().start(1, sparkleEffect); + sparkle.visible = true; + } + } + function set_hsvShader(value:HSVShader):HSVShader { this.hsvShader = value; @@ -168,9 +512,14 @@ class SongMenuItem extends FlxSpriteGroup songText.text = songData?.songName ?? 'Random'; // Update capsule character. if (songData?.songCharacter != null) setCharacter(songData.songCharacter); - updateDifficultyRating(songData?.songRating ?? 0); + updateBPM(Std.int(songData?.songStartingBpm) ?? 0); + updateDifficultyRating(songData?.difficultyRating ?? 0); + updateScoringRank(songData?.scoringRank); + newText.visible = songData?.isNew; // Update opacity, offsets, etc. updateSelected(); + + checkWeek(songData?.songId); } /** @@ -289,6 +638,28 @@ class SongMenuItem extends FlxSpriteGroup override function update(elapsed:Float):Void { + if (impactThing != null) impactThing.angle = capsule.angle; + + // if (FlxG.keys.justPressed.I) + // { + // newText.y -= 1; + // trace(this.x - newText.x, this.y - newText.y); + // } + // if (FlxG.keys.justPressed.J) + // { + // newText.x -= 1; + // trace(this.x - newText.x, this.y - newText.y); + // } + // if (FlxG.keys.justPressed.L) + // { + // newText.x += 1; + // trace(this.x - newText.x, this.y - newText.y); + // } + // if (FlxG.keys.justPressed.K) + // { + // newText.y += 1; + // trace(this.x - newText.x, this.y - newText.y); + // } if (doJumpIn) { frameInTicker += elapsed; @@ -357,6 +728,146 @@ class SongMenuItem extends FlxSpriteGroup capsule.offset.x = this.selected ? 0 : -5; capsule.animation.play(this.selected ? "selected" : "unselected"); ranking.alpha = this.selected ? 1 : 0.7; + favIcon.alpha = this.selected ? 1 : 0.6; + favIconBlurred.alpha = this.selected ? 1 : 0; ranking.color = this.selected ? 0xFFFFFFFF : 0xFFAAAAAA; + + if (songText.tooLong) songText.resetText(); + + if (selected && songText.tooLong) songText.initMove(); + } +} + +class FreeplayRank extends FlxSprite +{ + public var rank(default, set):Null = null; + + function set_rank(val:Null):Null + { + rank = val; + + if (rank == null || val == null) + { + this.visible = false; + } + else + { + this.visible = true; + + animation.play(val.getFreeplayRankIconAsset(), true, false); + + centerOffsets(false); + + switch (val) + { + case SHIT: + // offset.x -= 1; + case GOOD: + // offset.x -= 1; + offset.y -= 8; + case GREAT: + // offset.x -= 1; + offset.y -= 8; + case EXCELLENT: + // offset.y += 5; + case PERFECT: + // offset.y += 5; + case PERFECT_GOLD: + // offset.y += 5; + default: + centerOffsets(false); + this.visible = false; + } + updateHitbox(); + } + + return rank = val; + } + + public var baseX:Float = 0; + public var baseY:Float = 0; + + public function new(x:Float, y:Float) + { + super(x, y); + + frames = Paths.getSparrowAtlas('freeplay/rankbadges'); + + animation.addByPrefix('PERFECT', 'PERFECT rank0', 24, false); + animation.addByPrefix('EXCELLENT', 'EXCELLENT rank0', 24, false); + animation.addByPrefix('GOOD', 'GOOD rank0', 24, false); + animation.addByPrefix('PERFECTSICK', 'PERFECT rank GOLD', 24, false); + animation.addByPrefix('GREAT', 'GREAT rank0', 24, false); + animation.addByPrefix('LOSS', 'LOSS rank0', 24, false); + + blend = BlendMode.ADD; + + this.rank = null; + + // setGraphicSize(Std.int(width * 0.9)); + scale.set(0.9, 0.9); + updateHitbox(); + } +} + +class CapsuleNumber extends FlxSprite +{ + public var digit(default, set):Int = 0; + + function set_digit(val):Int + { + animation.play(numToString[val], true, false, 0); + + centerOffsets(false); + + switch (val) + { + case 1: + offset.x -= 4; + case 3: + offset.x -= 1; + + case 6: + + case 4: + // offset.y += 5; + case 9: + // offset.y += 5; + default: + centerOffsets(false); + } + return val; + } + + public var baseY:Float = 0; + public var baseX:Float = 0; + + var numToString:Array = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"]; + + public function new(x:Float, y:Float, big:Bool = false, ?initDigit:Int = 0) + { + super(x, y); + + if (big) + { + frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/bignumbers'); + } + else + { + frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/smallnumbers'); + } + + for (i in 0...10) + { + var stringNum:String = numToString[i]; + animation.addByPrefix(stringNum, '$stringNum', 24, false); + } + + this.digit = initDigit; + + animation.play(numToString[initDigit], true); + + setGraphicSize(Std.int(width * 0.9)); + updateHitbox(); } } diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index 7a21a6e8f..d09536eea 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -42,6 +42,16 @@ class MainMenuState extends MusicBeatState var magenta:FlxSprite; var camFollow:FlxObject; + var overrideMusic:Bool = false; + + static var rememberedSelectedIndex:Int = 0; + + public function new(?_overrideMusic:Bool = false) + { + super(); + overrideMusic = _overrideMusic; + } + override function create():Void { #if discord_rpc @@ -49,10 +59,12 @@ class MainMenuState extends MusicBeatState DiscordClient.changePresence("In the Menus", null); #end + FlxG.cameras.reset(new FunkinCamera('mainMenu')); + transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; - playMenuMusic(); + if (overrideMusic == false) playMenuMusic(); // We want the state to always be able to begin with being able to accept inputs and show the anims of the menu items. persistentUpdate = true; @@ -137,6 +149,8 @@ class MainMenuState extends MusicBeatState menuItem.scrollFactor.y = 0.4; } + menuItems.selectItem(rememberedSelectedIndex); + resetCamStuff(); subStateOpened.add(sub -> { @@ -170,7 +184,6 @@ class MainMenuState extends MusicBeatState function resetCamStuff():Void { - FlxG.cameras.reset(new FunkinCamera('mainMenu')); FlxG.camera.follow(camFollow, null, 0.06); FlxG.camera.snapToTarget(); } @@ -285,6 +298,8 @@ class MainMenuState extends MusicBeatState function startExitState(state:NextState):Void { menuItems.enabled = false; // disable for exit + rememberedSelectedIndex = menuItems.selectedIndex; + var duration = 0.4; menuItems.forEach(function(item) { if (menuItems.selectedIndex != item.ID) @@ -329,6 +344,8 @@ class MainMenuState extends MusicBeatState persistentUpdate = false; FlxG.state.openSubState(new DebugMenuSubState()); + // reset camera when debug menu is closed + subStateClosed.addOnce(_ -> resetCamStuff()); } #end @@ -351,10 +368,36 @@ class MainMenuState extends MusicBeatState maxCombo: 0, totalNotesHit: 0, totalNotes: 0, - }, - accuracy: 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, + // and see if we can maintain that golden P rank. + funkin.save.Save.instance.setSongScore('tutorial', 'easy', + { + score: 1234567, + tallies: + { + sick: 0, + good: 0, + bad: 0, + shit: 1, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 1, + totalNotes: 10, + } + }); + } + + if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.E) + { + funkin.save.Save.instance.debug_dumpSave(); + } #end if (FlxG.sound.music != null && FlxG.sound.music.volume < 0.8) diff --git a/source/funkin/ui/options/ControlsMenu.hx b/source/funkin/ui/options/ControlsMenu.hx index dd7d5ff38..1f40a8455 100644 --- a/source/funkin/ui/options/ControlsMenu.hx +++ b/source/funkin/ui/options/ControlsMenu.hx @@ -28,6 +28,8 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page [NOTE_UP, NOTE_DOWN, NOTE_LEFT, NOTE_RIGHT], [UI_UP, UI_DOWN, UI_LEFT, UI_RIGHT, ACCEPT, BACK], [CUTSCENE_ADVANCE], + [FREEPLAY_FAVORITE, FREEPLAY_LEFT, FREEPLAY_RIGHT], + [WINDOW_FULLSCREEN, WINDOW_SCREENSHOT], [VOLUME_UP, VOLUME_DOWN, VOLUME_MUTE], [DEBUG_MENU, DEBUG_CHART] ]; @@ -108,6 +110,18 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page headers.add(new AtlasText(0, y, "CUTSCENE", AtlasFont.BOLD)).screenCenter(X); y += spacer; } + else if (currentHeader != "FREEPLAY_" && name.indexOf("FREEPLAY_") == 0) + { + currentHeader = "FREEPLAY_"; + headers.add(new AtlasText(0, y, "FREEPLAY", AtlasFont.BOLD)).screenCenter(X); + y += spacer; + } + else if (currentHeader != "WINDOW_" && name.indexOf("WINDOW_") == 0) + { + currentHeader = "WINDOW_"; + headers.add(new AtlasText(0, y, "WINDOW", AtlasFont.BOLD)).screenCenter(X); + y += spacer; + } else if (currentHeader != "VOLUME_" && name.indexOf("VOLUME_") == 0) { currentHeader = "VOLUME_"; @@ -123,10 +137,10 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page if (currentHeader != null && name.indexOf(currentHeader) == 0) name = name.substr(currentHeader.length); - var label = labels.add(new AtlasText(150, y, name, AtlasFont.BOLD)); + var label = labels.add(new AtlasText(100, y, name, AtlasFont.BOLD)); label.alpha = 0.6; for (i in 0...COLUMNS) - createItem(label.x + 400 + i * 300, y, control, i); + createItem(label.x + 550 + i * 400, y, control, i); y += spacer; } diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx index 81331b266..40308d96b 100644 --- a/source/funkin/ui/options/OptionsState.hx +++ b/source/funkin/ui/options/OptionsState.hx @@ -25,6 +25,8 @@ class OptionsState extends MusicBeatState override function create():Void { + persistentUpdate = true; + var menuBG = new FlxSprite().loadGraphic(Paths.image('menuBG')); var hsv = new HSVShader(); hsv.hue = -0.6; @@ -55,8 +57,6 @@ class OptionsState extends MusicBeatState setPage(Controls); } - // disable for intro transition - currentPage.enabled = false; super.create(); } @@ -86,13 +86,6 @@ class OptionsState extends MusicBeatState } } - override function finishTransIn() - { - super.finishTransIn(); - - currentPage.enabled = true; - } - function switchPage(name:PageName) { // TODO: Animate this transition? @@ -266,11 +259,11 @@ class OptionsMenu extends Page #end } -enum PageName +enum abstract PageName(String) { - Options; - Controls; - Colors; - Mods; - Preferences; + var Options = "options"; + var Controls = "controls"; + var Colors = "colors"; + var Mods = "mods"; + var Preferences = "preferences"; } diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx index ffc756e1c..0547404a1 100644 --- a/source/funkin/ui/story/LevelProp.hx +++ b/source/funkin/ui/story/LevelProp.hx @@ -11,11 +11,13 @@ class LevelProp extends Bopper function set_propData(value:LevelPropData):LevelPropData { // Only reset the prop if the asset path has changed. - if (propData == null || value?.assetPath != propData?.assetPath) + if (propData == null || !(thx.Dynamics.equals(value, propData))) { - this.visible = (value != null); this.propData = value; + + this.visible = this.propData != null; danceEvery = this.propData?.danceEvery ?? 0; + applyData(); } diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 0c2214529..06a83ab4d 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -306,7 +306,7 @@ class StoryMenuState extends MusicBeatState { Conductor.instance.update(); - highScoreLerp = Std.int(MathUtil.smoothLerp(highScoreLerp, highScore, elapsed, 0.5)); + highScoreLerp = Std.int(MathUtil.smoothLerp(highScoreLerp, highScore, elapsed, 0.25)); scoreText.text = 'LEVEL SCORE: ${Math.round(highScoreLerp)}'; @@ -387,6 +387,7 @@ class StoryMenuState extends MusicBeatState function changeLevel(change:Int = 0):Void { var currentIndex:Int = levelList.indexOf(currentLevelId); + var prevIndex:Int = currentIndex; currentIndex += change; @@ -417,7 +418,7 @@ class StoryMenuState extends MusicBeatState } } - FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); + if (currentIndex != prevIndex) FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); updateText(); updateBackground(previousLevelId); @@ -466,6 +467,9 @@ class StoryMenuState extends MusicBeatState // Disable the funny music thing for now. // funnyMusicThing(); } + + updateText(); + refresh(); } final FADE_OUT_TIME:Float = 1.5; diff --git a/source/funkin/ui/title/AttractState.hx b/source/funkin/ui/title/AttractState.hx index 3ecb756df..c5a3d0504 100644 --- a/source/funkin/ui/title/AttractState.hx +++ b/source/funkin/ui/title/AttractState.hx @@ -89,7 +89,7 @@ class AttractState extends MusicBeatState super.update(elapsed); // If the user presses any button, skip the video. - if (FlxG.keys.justPressed.ANY) + if (FlxG.keys.justPressed.ANY && !controls.VOLUME_MUTE && !controls.VOLUME_UP && !controls.VOLUME_DOWN) { onAttractEnd(); } diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 49bef5e4a..8087916cb 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -67,9 +67,11 @@ class TitleState extends MusicBeatState // DEBUG BULLSHIT // netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown); - new FlxTimer().start(1, function(tmr:FlxTimer) { + if (!initialized) new FlxTimer().start(1, function(tmr:FlxTimer) { startIntro(); }); + else + startIntro(); } function client_onMetaData(metaData:Dynamic) @@ -118,11 +120,11 @@ class TitleState extends MusicBeatState function startIntro():Void { - playMenuMusic(); + if (!initialized || FlxG.sound.music == null) playMenuMusic(); persistentUpdate = true; - var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK); + var bg:FunkinSprite = new FunkinSprite(-1).makeSolidColor(FlxG.width + 2, FlxG.height, FlxColor.BLACK); bg.screenCenter(); add(bg); @@ -231,7 +233,7 @@ class TitleState extends MusicBeatState overrideExisting: true, restartTrack: true }); - // Fade from 0.0 to 0.7 over 4 seconds + // Fade from 0.0 to 1 over 4 seconds if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0); } @@ -263,6 +265,13 @@ class TitleState extends MusicBeatState if (FlxG.keys.pressed.DOWN) FlxG.sound.music.pitch -= 0.5 * elapsed; #end + #if desktop + if (FlxG.keys.justPressed.ESCAPE) + { + Sys.exit(0); + } + #end + Conductor.instance.update(); /* if (FlxG.onMobile) diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index 28533e35b..bc26ad97a 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -57,8 +57,7 @@ class LoadingState extends MusicBeatSubState funkay.scrollFactor.set(); funkay.screenCenter(); - loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(FlxG.width, 10, 0xFFff16d2); - loadBar.screenCenter(X); + loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(0, 10, 0xFFff16d2); add(loadBar); initSongsManifest().onComplete(function(lib) { @@ -162,7 +161,16 @@ class LoadingState extends MusicBeatSubState { targetShit = FlxMath.remapToRange(callbacks.numRemaining / callbacks.length, 1, 0, 0, 1); - loadBar.scale.x = FlxMath.lerp(loadBar.scale.x, targetShit, 0.50); + var lerpWidth:Int = Std.int(FlxMath.lerp(loadBar.width, FlxG.width * targetShit, 0.2)); + // this if-check prevents the setGraphicSize function + // from setting the width of the loadBar to the height of the loadBar + // this is a behaviour that is implemented in the setGraphicSize function + // if the width parameter is equal to 0 + if (lerpWidth > 0) + { + loadBar.setGraphicSize(lerpWidth, loadBar.height); + loadBar.updateHitbox(); + } FlxG.watch.addQuick('percentage?', callbacks.numRemaining / callbacks.length); } diff --git a/source/funkin/ui/transition/preload/FunkinPreloader.hx b/source/funkin/ui/transition/preload/FunkinPreloader.hx index b71af2b3b..9d2569588 100644 --- a/source/funkin/ui/transition/preload/FunkinPreloader.hx +++ b/source/funkin/ui/transition/preload/FunkinPreloader.hx @@ -136,6 +136,8 @@ class FunkinPreloader extends FlxBasePreloader // We can't even call trace() yet, until Flixel loads. trace('Initializing custom preloader...'); + funkin.util.CLIUtil.resetWorkingDir(); + this.siteLockTitleText = Constants.SITE_LOCK_TITLE; this.siteLockBodyText = Constants.SITE_LOCK_DESC; } diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index c50f17697..1e0978839 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -248,6 +248,11 @@ class Constants */ public static final DEFAULT_ARTIST:String = 'Unknown'; + /** + * The default charter for songs. + */ + public static final DEFAULT_CHARTER:String = 'Unknown'; + /** * The default note style for songs. */ @@ -455,6 +460,17 @@ class Constants public static final JUDGEMENT_BAD_COMBO_BREAK:Bool = true; public static final JUDGEMENT_SHIT_COMBO_BREAK:Bool = true; + // % Sick + public static final RANK_PERFECT_PLAT_THRESHOLD:Float = 1.0; // % Sick + public static final RANK_PERFECT_GOLD_THRESHOLD:Float = 0.85; // % Sick + + // % Hit + public static final RANK_PERFECT_THRESHOLD:Float = 1.00; + public static final RANK_EXCELLENT_THRESHOLD:Float = 0.90; + public static final RANK_GREAT_THRESHOLD:Float = 0.80; + public static final RANK_GOOD_THRESHOLD:Float = 0.60; + + // public static final RANK_SHIT_THRESHOLD:Float = 0.00; /** * FILE EXTENSIONS */ diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 7a7b1422c..00a0a14b7 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -19,6 +19,7 @@ import haxe.ui.containers.dialogs.Dialogs.FileDialogExtensionInfo; class FileUtil { public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc"); + public static final FILE_FILTER_JSON:FileFilter = new FileFilter("JSON Data File (.json)", "*.json"); public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip"); public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png"); diff --git a/source/funkin/util/StructureUtil.hx b/source/funkin/util/StructureUtil.hx deleted file mode 100644 index 2f0c3818a..000000000 --- a/source/funkin/util/StructureUtil.hx +++ /dev/null @@ -1,136 +0,0 @@ -package funkin.util; - -import funkin.util.tools.MapTools; -import haxe.DynamicAccess; - -/** - * Utilities for working with anonymous structures. - */ -class StructureUtil -{ - /** - * Merge two structures, with the second overwriting the first. - * Performs a SHALLOW clone, where child structures are not merged. - * @param a The base structure. - * @param b The new structure. - * @return The merged structure. - */ - public static function merge(a:Dynamic, b:Dynamic):Dynamic - { - var result:DynamicAccess = Reflect.copy(a); - - for (field in Reflect.fields(b)) - { - result.set(field, Reflect.field(b, field)); - } - - return result; - } - - public static function toMap(a:Dynamic):haxe.ds.Map - { - var result:haxe.ds.Map = []; - - for (field in Reflect.fields(a)) - { - result.set(field, Reflect.field(a, field)); - } - - return result; - } - - public static function isMap(a:Dynamic):Bool - { - return Std.isOfType(a, haxe.Constraints.IMap); - } - - public static function isObject(a:Dynamic):Bool - { - switch (Type.typeof(a)) - { - case TObject: - return true; - default: - return false; - } - } - - public static function isPrimitive(a:Dynamic):Bool - { - switch (Type.typeof(a)) - { - case TInt | TFloat | TBool: - return true; - case TClass(c): - return false; - case TEnum(e): - return false; - case TObject: - return false; - case TFunction: - return false; - case TNull: - return true; - case TUnknown: - return false; - default: - return false; - } - } - - /** - * Merge two structures, with the second overwriting the first. - * Performs a DEEP clone, where child structures are also merged recursively. - * @param a The base structure. - * @param b The new structure. - * @return The merged structure. - */ - public static function deepMerge(a:Dynamic, b:Dynamic):Dynamic - { - if (a == null) return b; - if (b == null) return null; - if (isPrimitive(a) && isPrimitive(b)) return b; - if (isMap(b)) - { - if (isMap(a)) - { - return MapTools.merge(a, b); - } - else - { - return StructureUtil.toMap(a).merge(b); - } - } - if (!Reflect.isObject(a) || !Reflect.isObject(b)) return b; - if (Std.isOfType(b, haxe.ds.StringMap)) - { - if (Std.isOfType(a, haxe.ds.StringMap)) - { - return MapTools.merge(a, b); - } - else - { - return StructureUtil.toMap(a).merge(b); - } - } - - var result:DynamicAccess = Reflect.copy(a); - - for (field in Reflect.fields(b)) - { - if (Reflect.isObject(b)) - { - // Note that isObject also returns true for class instances, - // but we just assume that's not a problem here. - result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field))); - } - else - { - // If we're here, b[field] is a primitive. - result.set(field, Reflect.field(b, field)); - } - } - - return result; - } -} diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx index 247ba19db..832ce008a 100644 --- a/source/funkin/util/VersionUtil.hx +++ b/source/funkin/util/VersionUtil.hx @@ -23,6 +23,8 @@ class VersionUtil { try { + var versionRaw:thx.semver.Version.SemVer = version; + trace('${versionRaw} satisfies (${versionRule})? ${version.satisfies(versionRule)}'); return version.satisfies(versionRule); } catch (e) @@ -32,6 +34,40 @@ class VersionUtil } } + public static function repairVersion(version:thx.semver.Version):thx.semver.Version + { + var versionData:thx.semver.Version.SemVer = version; + + if (thx.Types.isAnonymousObject(versionData.version)) + { + // This is bad! versionData.version should be an array! + trace('[SAVE] Version data repair required! (got ${versionData.version})'); + // Turn the objects back into arrays. + // I'd use DynamicsT.values but IDK if it maintains order + versionData.version = [versionData.version[0], versionData.version[1], versionData.version[2]]; + + // This is so jank but it should work. + var buildData:Dynamic = cast versionData.build; + var buildDataFixed:Array = thx.Dynamics.DynamicsT.values(buildData) + .map(function(d:Dynamic) return StringId(d.toString())); + versionData.build = buildDataFixed; + + var preData:Dynamic = cast versionData.pre; + var preDataFixed:Array = thx.Dynamics.DynamicsT.values(preData).map(function(d:Dynamic) return StringId(d.toString())); + versionData.pre = preDataFixed; + + var fixedVersion:thx.semver.Version = versionData; + trace('[SAVE] Fixed version: ${fixedVersion}'); + return fixedVersion; + } + else + { + trace('[SAVE] Version data repair not required (got ${version})'); + // No need for repair. + return version; + } + } + /** * Checks that a given verison number satisisfies a given version rule. * Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports. diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index 763d84853..0fe63fe32 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -24,7 +24,7 @@ class WindowUtil { #if CAN_OPEN_LINKS #if linux - Sys.command('/usr/bin/xdg-open', [targetUrl, '&']); + Sys.command('/usr/bin/xdg-open $targetUrl &'); #else // This should work on Windows and HTML5. FlxG.openURL(targetUrl); @@ -92,7 +92,7 @@ class WindowUtil }); openfl.Lib.current.stage.addEventListener(openfl.events.KeyboardEvent.KEY_DOWN, (e:openfl.events.KeyboardEvent) -> { - for (key in PlayerSettings.player1.controls.getKeysForAction(FULLSCREEN)) + for (key in PlayerSettings.player1.controls.getKeysForAction(WINDOW_FULLSCREEN)) { if (e.keyCode == key) { diff --git a/source/funkin/util/logging/AnsiTrace.hx b/source/funkin/util/logging/AnsiTrace.hx index 9fdc19e1b..2c18d494d 100644 --- a/source/funkin/util/logging/AnsiTrace.hx +++ b/source/funkin/util/logging/AnsiTrace.hx @@ -51,7 +51,7 @@ class AnsiTrace public static function traceBF() { - #if sys + #if (sys && debug) if (colorSupported) { for (line in ansiBF) diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index 8cfa2270f..71d1ad394 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -141,7 +141,9 @@ class CrashHandler fullContents += '\n'; - fullContents += 'Flixel Current State: ${Type.getClassName(Type.getClass(FlxG.state))}\n'; + var currentState = FlxG.state != null ? Type.getClassName(Type.getClass(FlxG.state)) : 'No state loaded'; + + fullContents += 'Flixel Current State: ${currentState}\n'; fullContents += '\n'; diff --git a/source/funkin/util/macro/InlineMacro.hx b/source/funkin/util/macro/InlineMacro.hx index b0e7ed184..c40257409 100644 --- a/source/funkin/util/macro/InlineMacro.hx +++ b/source/funkin/util/macro/InlineMacro.hx @@ -23,7 +23,7 @@ class InlineMacro var fields:Array = haxe.macro.Context.getBuildFields(); // Find the field with the given name. - var targetField:Null = fields.find(function(f) return f.name == field + var targetField:Null = thx.Arrays.find(fields, function(f) return f.name == field && (MacroUtil.isFieldStatic(f) == isStatic)); // If the field was not found, throw an error. diff --git a/source/funkin/util/plugins/ScreenshotPlugin.hx b/source/funkin/util/plugins/ScreenshotPlugin.hx index 9ac21d4b8..c859710de 100644 --- a/source/funkin/util/plugins/ScreenshotPlugin.hx +++ b/source/funkin/util/plugins/ScreenshotPlugin.hx @@ -103,7 +103,7 @@ class ScreenshotPlugin extends FlxBasic public function hasPressedScreenshot():Bool { - return PlayerSettings.player1.controls.SCREENSHOT; + return PlayerSettings.player1.controls.WINDOW_SCREENSHOT; } public function updatePreferences():Void diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx index caf8e8aab..0fe245e3a 100644 --- a/source/funkin/util/tools/ArrayTools.hx +++ b/source/funkin/util/tools/ArrayTools.hx @@ -5,72 +5,6 @@ package funkin.util.tools; */ class ArrayTools { - /** - * Returns a copy of the array with all duplicate elements removed. - * @param array The array to remove duplicates from. - * @return A copy of the array with all duplicate elements removed. - */ - public static function unique(array:Array):Array - { - var result:Array = []; - for (element in array) - { - if (!result.contains(element)) - { - result.push(element); - } - } - return result; - } - - /** - * Returns a copy of the array with all `null` elements removed. - * @param array The array to remove `null` elements from. - * @return A copy of the array with all `null` elements removed. - */ - public static function nonNull(array:Array>):Array - { - var result:Array = []; - for (element in array) - { - if (element != null) - { - result.push(element); - } - } - return result; - } - - /** - * Return the first element of the array that satisfies the predicate, or null if none do. - * @param input The array to search - * @param predicate The predicate to call - * @return The result - */ - public static function find(input:Array, predicate:T->Bool):Null - { - for (element in input) - { - if (predicate(element)) return element; - } - return null; - } - - /** - * Return the index of the first element of the array that satisfies the predicate, or `-1` if none do. - * @param input The array to search - * @param predicate The predicate to call - * @return The index of the result - */ - public static function findIndex(input:Array, predicate:T->Bool):Int - { - for (index in 0...input.length) - { - if (predicate(input[index])) return index; - } - return -1; - } - /* * Push an element to the array if it is not already present. * @param input The array to push to diff --git a/tests/unit/assets/shared/images/arrows.png b/tests/unit/assets/shared/images/arrows.png deleted file mode 100644 index a44368432..000000000 Binary files a/tests/unit/assets/shared/images/arrows.png and /dev/null differ diff --git a/tests/unit/assets/shared/images/arrows.xml b/tests/unit/assets/shared/images/arrows.xml deleted file mode 100644 index 96a73a388..000000000 --- a/tests/unit/assets/shared/images/arrows.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -