Merge branch 'FunkinCrew:main' into flash_fix

This commit is contained in:
cyn 2024-08-23 20:09:41 -07:00 committed by GitHub
commit c62b53ebe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
113 changed files with 5403 additions and 1289 deletions

View file

@ -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

62
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View file

@ -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.)

View file

@ -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.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: false

70
.github/ISSUE_TEMPLATE/crash.yml vendored Normal file
View file

@ -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

View file

@ -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?

15
.github/ISSUE_TEMPLATE/enhancement.yml vendored Normal file
View file

@ -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?

View file

@ -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?

View file

@ -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.

View file

@ -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?

12
.github/changed-lines-count-labeler.yml vendored Normal file
View file

@ -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

12
.github/labeler.yml vendored Normal file
View file

@ -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'

6
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,6 @@
<!-- Please check for duplicates or similar PRs before submitting this PR. -->
## Does this PR close any issues? If so, link them below.
## Briefly describe the issue(s) fixed.
## Include any relevant screenshots or videos.

27
.github/workflows/labeler.yml vendored Normal file
View file

@ -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

9
.vscode/launch.json vendored
View file

@ -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",

View file

@ -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",

View file

@ -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.

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<project>
<project xmlns="http://lime.openfl.org/project/1.0.4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://lime.openfl.org/project/1.0.4 http://lime.openfl.org/xsd/project-1.0.4.xsd">
<!-- _________________________ Application Settings _________________________ -->
<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.2" company="ninjamuffin99" />
<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.4.1" company="ninjamuffin99" />
<!--Switch Export with Unique ApplicationID and Icon-->
<set name="APP_ID" value="0x0100f6c013bbc000" />
@ -28,7 +29,7 @@
<set name="BUILD_DIR" value="export/debug" if="debug" />
<set name="BUILD_DIR" value="export/release" unless="debug" />
<set name="BUILD_DIR" value="export/32bit" if="32bit" />
<classpath name="source" />
<source path="source" />
<assets path="assets/preload" rename="assets" exclude="*.ogg|*.wav" if="web" />
<assets path="assets/preload" rename="assets" exclude="*.mp3|*.wav" unless="web" />
<define name="PRELOAD_ALL" unless="web" />
@ -125,9 +126,12 @@
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback -->
<haxelib name="funkin.vis"/>
<haxelib name="grig.audio" />
<haxelib name="FlxPartialSound" /> <!-- Loading partial sound data -->
<haxelib name="json2object" /> <!-- JSON parsing -->
<haxelib name="thx.core" /> <!-- General utility library, "the lodash of Haxe" -->
<haxelib name="thx.semver" /> <!-- Version string handling -->
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->

View file

@ -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

2
art

@ -1 +1 @@
Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa
Subproject commit faeba700c5526bd4fd57ccc927d875c82b9d3553

2
assets

@ -1 +1 @@
Subproject commit 962130b2243a839106607d08a11599b1857bf8b3
Subproject commit 2e1594ee4c04c7148628bae471bdd061c9deb6b7

View file

@ -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"

View file

@ -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**.

View file

@ -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.

View file

@ -3,7 +3,7 @@
"description": "An introductory mod.",
"contributors": [
{
"name": "MasterEric"
"name": "EliteMasterEric"
}
],
"api_version": "0.1.0",

View file

@ -3,7 +3,7 @@
"description": "Newgrounds? More like OLDGROUNDS lol.",
"contributors": [
{
"name": "MasterEric"
"name": "EliteMasterEric"
}
],
"api_version": "0.1.0",

View file

@ -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"
},
{

View file

@ -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;
}

View file

@ -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());

View file

@ -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;
}

View file

@ -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;

View file

@ -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
/**

View file

@ -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<FunkinSound>
// 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<FunkinSound>
*/
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<FunkinSound>
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<FunkinSound>) {
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<Promise<Null<FunkinSound>>> = [];
/**
* Creates a new `FunkinSound` object synchronously.
*
@ -415,6 +472,49 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
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<Null<FunkinSound>>
{
var promise:lime.app.Promise<Null<FunkinSound>> = new lime.app.Promise<Null<FunkinSound>>();
// 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;
}

View file

@ -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
{

View file

@ -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<FlxSprite>
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<FlxSprite>
override function draw()
{
#if web
if (analyzer != null) drawFFT();
#end
super.draw();
}
@ -94,12 +95,16 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
*/
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));

View file

@ -1,6 +1,5 @@
package funkin.audio.visualize;
import funkin.audio.visualize.PolygonSpectogram;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.sound.FlxSound;

View file

@ -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<FlxSprite>
lengthOfShit = amnt;
regenLineShit();
// makeGraphic(200, 200, FlxColor.BLACK);
}
public function regenLineShit():Void
@ -89,8 +85,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
{
checkAndSetBuffer();
// vis.checkAndSetBuffer();
if (setBuffer)
{
var samplesToGen:Int = Std.int(sampleRate * seconds);
@ -191,7 +185,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
// 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<FlxSprite>
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<FlxSprite>
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;

View file

@ -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

View file

@ -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;

View file

@ -1,7 +1,5 @@
package funkin.audio.waveform;
import funkin.util.MathUtil;
@:nullSafety
class WaveformData
{

View file

@ -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;

View file

@ -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.

View file

@ -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<Conversation, ConversationData>

View file

@ -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.

View file

@ -30,6 +30,9 @@ class SongMetadata implements ICloneable<SongMetadata>
@:default("Unknown")
public var artist:String;
@:optional
public var charter:Null<String> = null;
@:optional
@:default(96)
public var divisions:Null<Int>; // Optional field
@ -53,6 +56,8 @@ class SongMetadata implements ICloneable<SongMetadata>
@: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<SongTimeChange>;
@ -112,14 +117,23 @@ class SongMetadata implements ICloneable<SongMetadata>
*/
public function serialize(pretty:Bool = true):String
{
// Update generatedBy and version before writing.
updateVersionToLatest();
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongMetadata>(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<SongMusicData>
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<SongChartData>
*/
public function serialize(pretty:Bool = true):String
{
// Update generatedBy and version before writing.
updateVersionToLatest();
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongChartData>(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.

View file

@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
* 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";

View file

@ -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<ChartManifestData>();
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
{
this.version = CHART_MANIFEST_DATA_VERSION;
}
public static function deserialize(contents:String):Null<ChartManifestData>
{
var parser = new json2object.JsonParser<ChartManifestData>();

View file

@ -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;
}

View file

@ -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<StageData>();
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
{
this.version = StageRegistry.STAGE_DATA_VERSION;
}
}
typedef StageDataCharacters =

View file

@ -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<IntervalShake> = new FlxPool<IntervalShake>(IntervalShake.new);
/**
* Internal map for looking up which objects are currently shaking and getting their shake data.
*/
static var _boundObjects:Map<FlxObject, IntervalShake> = new Map<FlxObject, IntervalShake>();
/**
* 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() {}
}

View file

@ -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();

View file

@ -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)

View file

@ -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;

File diff suppressed because it is too large Load diff

View file

@ -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 + ')';
}
}

View file

@ -71,7 +71,7 @@ class GameOverSubState extends MusicBeatSubState
var gameOverMusic:Null<FunkinSound> = 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)
{

View file

@ -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<FlxTween> = 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;

View file

@ -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<FlxTween> = [];
/**
* 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<NoteSprite> = playerStrumline.getNotesMayHit();
var holdNotesInRange:Array<SustainTrail> = playerStrumline.getHoldNotesHitOrMissed();
// If there are notes in range, pressing a key will cause a ghost miss.
var notesByDirection:Array<Array<NoteSprite>> = [[], [], [], []];
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<NoteSprite> = 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<SaveScoreData> = 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<Dynamic> = []; // 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:Null<Float->Float>, strumlines:Array<String>):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.

View file

@ -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<ScoreNum>
{
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<ScoreNum>
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<ScoreNum>
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<ScoreNum>
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;

File diff suppressed because it is too large Load diff

View file

@ -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;
}

View file

@ -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<FlxSprite>
{
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<Int> = [];
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();
}
}

View file

@ -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>):String

View file

@ -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<String> = [];
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:Null<Float->Float> = 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,
}
]);
}
}

View file

@ -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<Conductor>;
function get_conductorInUse():Conductor
@ -134,6 +142,7 @@ class Strumline extends FlxSpriteGroup
this.refresh();
this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
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<SongNoteData>):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();

View file

@ -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<SongNoteData>;
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;

View file

@ -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<SaveScoreData>):Null<ScoringRank>
{
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<ScoringRank>):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<ScoringRank>, b:Null<ScoringRank>):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<ScoringRank>, b:Null<ScoringRank>):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<ScoringRank>, b:Null<ScoringRank>):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<ScoringRank>, b:Null<ScoringRank>):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<String>
{
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';
}
}
}

View file

@ -91,6 +91,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return _metadata.keys().array();
}
// this returns false so that any new song can override this and return true when needed
public function isSongNew(currentDifficulty:String):Bool
{
return false;
}
/**
* Set to false if the song was edited in the charter and should not be saved as a high score.
*/
@ -120,6 +126,18 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return DEFAULT_ARTIST;
}
/**
* The artist of the song.
*/
public var charter(get, never):String;
function get_charter():String
{
if (_data != null) return _data?.charter ?? 'Unknown';
if (_metadata.size() > 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<SongMeta
difficulty.songName = metadata.songName;
difficulty.songArtist = metadata.artist;
difficulty.charter = metadata.charter ?? Constants.DEFAULT_CHARTER;
difficulty.timeFormat = metadata.timeFormat;
difficulty.divisions = metadata.divisions;
difficulty.timeChanges = metadata.timeChanges;
@ -334,6 +353,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
{
difficulty.songName = metadata.songName;
difficulty.songArtist = metadata.artist;
difficulty.charter = metadata.charter ?? Constants.DEFAULT_CHARTER;
difficulty.timeFormat = metadata.timeFormat;
difficulty.divisions = metadata.divisions;
difficulty.timeChanges = metadata.timeChanges;
@ -364,7 +384,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/
public function getDifficulty(?diffId:String, ?variation:String, ?variations:Array<String>):Null<SongDifficulty>
{
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<SongMeta
return null;
}
/**
* Given that this character is selected in the Freeplay menu,
* which variations should be available?
* @param charId The character ID to query.
* @return An array of available variations.
*/
public function getVariationsByCharId(?charId:String):Array<String>
{
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<SongMeta
// so we have to map it to the actual difficulty names.
// We also filter out difficulties that don't match the variation or that don't exist.
var diffFiltered:Array<String> = difficulties.keys().array().map(function(diffId:String):Null<String> {
var difficulty:Null<SongDifficulty> = 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<String> = difficulties.keys()
.array()
.map(function(diffId:String):Null<String> {
var difficulty:Null<SongDifficulty> = 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<Int> = 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);

View file

@ -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<StageData>
{
return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));

View file

@ -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<ScoringRank>
{
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<SaveControlsData>
{
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<Dynamic>
{
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<RawSaveData_v1_0_0>
{
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<RawSaveData>(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<String>;
var mods:SaveDataMods;
/**
@ -777,6 +999,9 @@ typedef SaveHighScoresData =
typedef SaveDataMods =
{
var enabledMods:Array<String>;
// TODO: Make this not trip up the serializer when debugging.
@:jignored
var modOptions:Map<String, Dynamic>;
}
@ -809,11 +1034,6 @@ typedef SaveScoreData =
* The count of each judgement hit.
*/
var tallies:SaveScoreTallyData;
/**
* The accuracy percentage.
*/
var accuracy:Float;
}
typedef SaveScoreTallyData =

View file

@ -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<String>` to `Save`
## [2.0.3] - 2024-01-09
### Added

View file

@ -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);
}

View file

@ -94,7 +94,7 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
if (newIndex != selectedIndex)
{
FunkinSound.playOnce(Paths.sound('scrollMenu'));
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
selectItem(newIndex);
}
@ -142,7 +142,7 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
*/
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);

View file

@ -54,7 +54,7 @@ class CreditsDataHandler
body: [
{line: 'ninjamuffin99'},
{line: 'PhantomArcade'},
{line: 'KawaiSprite'},
{line: 'Kawai Sprite'},
{line: 'evilsk8r'},
]
}

View file

@ -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

View file

@ -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));
}

View file

@ -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> = 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!

View file

@ -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.

View file

@ -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;

View file

@ -384,17 +384,34 @@ class ChartEditorImportExportHandler
if (variationId == '')
{
var variationMetadata:Null<SongMetadata> = 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<SongChartData> = 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<SongMetadata> = 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<SongChartData> = 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()));
}
}
}

View file

@ -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),

View file

@ -58,17 +58,8 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
function initialize():Void
{
toolboxEventsEventKind.dataSource = new ArrayDataSource();
var songEvents:Array<SongEvent> = 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<Dynamic> = 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();

View file

@ -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;

View file

@ -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<SongEvent> = 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<DropDownEntry>
{
// 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.
*/

View file

@ -38,7 +38,7 @@ class AlbumRoll extends FlxSpriteGroup
var newAlbumArt:FlxAtlasSprite;
// var difficultyStars:DifficultyStars;
var difficultyStars:DifficultyStars;
var _exitMovers:Null<FreeplayState.ExitMoverData>;
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();
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -50,8 +50,19 @@ class FreeplayFlames extends FlxSpriteGroup
}
}
var timers:Array<FlxTimer> = [];
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++;
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<FlxSprite>
@ -69,14 +70,19 @@ class LetterSort extends FlxTypedSpriteGroup<FlxSprite>
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);
}
}

View file

@ -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<String> = ["fail", "average", "great", "excellent", "perfect"];
public var ranking:FreeplayRank;
public var blurredRanking:FreeplayRank;
public var fakeRanking:FreeplayRank;
public var fakeBlurredRanking:FreeplayRank;
var ranks:Array<String> = ["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<CapsuleNumber> = [];
public var smallNumbers:Array<CapsuleNumber> = [];
public var weekNumbers:Array<CapsuleNumber> = [];
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<ScoringRank>):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<ScoringRank> = null;
function set_rank(val:Null<ScoringRank>):Null<ScoringRank>
{
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<String> = ["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();
}
}

View file

@ -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)

View file

@ -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;
}

View file

@ -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";
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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();
}

View file

@ -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)

View file

@ -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);
}

Some files were not shown because too many files have changed in this diff Show more