1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-02-10 21:43:06 +00:00

Merge branch 'rewrite/master' into feature/album-art-swap

This commit is contained in:
Cameron Taylor 2024-08-16 20:21:51 -04:00
commit 0e46f91a60
76 changed files with 2919 additions and 929 deletions

View file

@ -1,50 +0,0 @@
---
name: Bug Report
about: Report a bug or critical performance issue
title: 'Bug Report: [DESCRIBE YOUR BUG IN DETAIL HERE]'
labels: bug
---
<!-- FILL THIS ISSUE THING OUT AS MUCH AS POSSIBLE
OR ELSE YOUR ISSUE WILL BE LESS LIKELY TO BE SOLVED!
Do not post about issues from other FNF mod engines!
We cannot and probably won't solve those!
You can hopefully go to their respective GitHub issues and report them there, thank you :)
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.
From Joel On Software:
"Its pretty easy to remember the rule for a good bug report. Every good bug report needs exactly three things.
1. Steps to reproduce,
2. What you expected to see, and
3. What you saw instead."
-->
## Describe the bug
<!-- A clear and concise description of what the bug is. -->
## To Reproduce
<!-- Describe in DETAIL how to reproduce the bug/issue you are running into. -->
## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
## Screenshots/Video
<!-- If applicable, add screenshots/video to help explain your problem.
Remember to mark the area in the application thats impacted. -->
## Desktop
- OS:
<!-- [e.g. Windows 10, 11, Mac, Linux Mint, Ubuntu, Arch (btw)] -->
- Browser
<!-- [e.g. chrome, safari, firefox, edge, operaGX] -->
- Version:
<!-- [e.g. 0.4.0, 0.3.3, this can be found in the bottom left corner of the main menu!] -->
## Additional context
<!-- Add any other context about the problem here. -->
<!-- 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.)

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

View file

@ -60,8 +60,8 @@ runs:
haxelib --debug --never deleterepo || true
haxelib --debug --never newrepo
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib --debug --global install hmm
haxelib --debug --always --global git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
haxelib --debug --global --always git hmm https://github.com/FunkinCrew/hmm funkin-patches
echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV"
- name: Restore cached dependencies

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

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.

View file

@ -45,7 +45,11 @@ jobs:
uses: ./.github/actions/setup-haxe
with:
gh-token: ${{ steps.app_token.outputs.token }}
- name: Setup HXCPP dev commit
run: |
cd .haxelib/hxcpp/git/tools/hxcpp
haxe compile.hxml
cd ../../../../..
- name: Build game
if: ${{ matrix.target == 'windows' }}
run: |

View file

@ -9,6 +9,19 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
- 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

View file

@ -81,19 +81,9 @@ which would remove their rank if they had a lower one.
- 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 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!)
- Removed a large number of unused imports to optimize builds (thanks Ethan-makes-music!)
- Fixed a bug where hold notes would be positioned wrong on downscroll (thanks MaybeMaru!)
- Additional fixes to the Loading bar on HTML5 (thanks lemz1!)
- Fixed a crash in Freeplay caused by a level referencing an invalid song (thanks gamerbross!)
- Improved debug logging for unscripted stages (thanks gamerbross!)
- Fixed a bug where changing difficulties in Story mode wouldn't update the score (thanks sectorA!)
- 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 several bugs with the TitleState, including missing music when returning from the Main Menu (thanks gamerbross!)
- Fixed a bug where opening the game from the command line would crash the preloader (thanks NotHyper474!)
- 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 characters would sometimes use the wrong scale value (thanks PurSnake!)
- Additional bug fixes and optimizations.

2
assets

@ -1 +1 @@
Subproject commit 902b12a0cdd9b588cefe573e591530f088a77b4c
Subproject commit d09b1e0f175dc19986a1ef6ef1185c060acd1b71

View file

@ -2,14 +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. Download Git from [git-scm.com](https://www.git-scm.com)
4. Install all haxelibs of the current branch by running `hmm install`
5. Setup lime: `haxelib run lime setup`
6. 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
@ -17,10 +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
7. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
8. `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
# 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

@ -11,26 +11,28 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "a7d8e3bad89a0a3506a4714121f73d8e34522c49",
"ref": "10c2a203c43a78ff1ff26b8368fd736576829d8d",
"url": "https://github.com/FunkinCrew/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
"ref": "a523c3b56622f0640933944171efed46929e360e",
"ref": "9c6fb47968e894eb36bf10e94725cd7640c49281",
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
"name": "flixel-text-input",
"type": "haxelib",
"version": "1.1.0"
"type": "git",
"dir": null,
"ref": "951a0103a17bfa55eed86703ce50b4fb0d7590bc",
"url": "https://github.com/FunkinCrew/flixel-text-input"
},
{
"name": "flixel-ui",
"type": "git",
"dir": null,
"ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15",
"ref": "d0afed7293c71ffdb1184751317fc709b44c9056",
"url": "https://github.com/HaxeFlixel/flixel-ui"
},
{
@ -75,14 +77,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "5dc4c933bdc029f6139a47962e3b8c754060f210",
"ref": "22f7c5a8ffca90d4677cffd6e570f53761709fbc",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "57c1604d6b5174839d7e0e012a4dd5dcbfc129da",
"ref": "28bb710d0ae5d94b5108787593052165be43b980",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
@ -99,8 +101,10 @@
},
{
"name": "hxcpp",
"type": "haxelib",
"version": "4.3.2"
"type": "git",
"dir": null,
"url": "https://github.com/HaxeFoundation/hxcpp",
"ref": "8dc8020f8465027de6c2aaaed90718bc693651ed"
},
{
"name": "hxcpp-debug-server",
@ -121,11 +125,25 @@
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
"url": "https://github.com/FunkinCrew/json2object"
},
{
"name": "jsonpatch",
"type": "git",
"dir": null,
"ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
"url": "https://github.com/EliteMasterEric/jsonpatch"
},
{
"name": "jsonpath",
"type": "git",
"dir": null,
"ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
"url": "https://github.com/EliteMasterEric/jsonpath"
},
{
"name": "lime",
"type": "git",
"dir": null,
"ref": "872ff6db2f2d27c0243d4ff76802121ded550dd7",
"ref": "f6153ffcb1ffcf733f91d531eac5fda4189e07f7",
"url": "https://github.com/FunkinCrew/lime"
},
{
@ -160,21 +178,21 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134",
"ref": "8306425c497766739510ab29e876059c96f77bd2",
"url": "https://github.com/FunkinCrew/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
"ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7",
"ref": "98945c6c7f5ecde01a32c4623d3515bf012a023a",
"url": "https://github.com/larsiusprime/polymod"
},
{
"name": "thx.core",
"type": "git",
"dir": null,
"ref": "6240b6e136f7490d9298edbe8c1891374bd7cdf2",
"ref": "76d87418fadd92eb8e1b61f004cff27d656e53dd",
"url": "https://github.com/fponticelli/thx.core"
},
{

View file

@ -27,6 +27,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState;
import funkin.util.CLIUtil;
@ -176,6 +177,8 @@ class InitState extends FlxState
// Move it to use a BaseRegistry.
CharacterDataParser.loadCharacterCache();
NoteKindManager.loadScripts();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate();

View file

@ -54,7 +54,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
public function initAnalyzer()
{
@:privateAccess
analyzer = new SpectralAnalyzer(snd._channel.__source, 7, 0.1, 40);
analyzer = new SpectralAnalyzer(snd._channel.__audioSource, 7, 0.1, 40);
#if desktop
// On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5

View file

@ -117,7 +117,7 @@ class VisShit
{
// Math.pow3
@:privateAccess
var buf = snd._channel.__source.buffer;
var buf = snd._channel.__audioSource.buffer;
// @:privateAccess
audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!!

View file

@ -16,7 +16,7 @@ class WaveformDataParser
// Method 1. This only works if the sound has been played before.
@:privateAccess
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__source?.buffer;
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__audioSource?.buffer;
if (soundBuffer == null)
{

View file

@ -263,7 +263,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param version The entry's version (use `fetchEntryVersion(id)`).
* @return The created entry.
*/
public function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<J>
public function parseEntryDataWithMigration(id:String, version:Null<thx.semver.Version>):Null<J>
{
if (version == null)
{

View file

@ -46,7 +46,7 @@ class SongEventRegistry
if (event != null)
{
trace(' Loaded built-in song event: (${event.id})');
trace(' Loaded built-in song event: ${event.id}');
eventCache.set(event.id, event);
}
else
@ -59,9 +59,9 @@ class SongEventRegistry
static function registerScriptedEvents()
{
var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
for (eventCls in scriptedEventClassNames)
{
var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");

View file

@ -0,0 +1,31 @@
# Note Style Data Schema Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0]
### Added
- Added several new `assets`:
- `countdownThree`
- `countdownTwo`
- `countdownOne`
- `countdownGo`
- `judgementSick`
- `judgementGood`
- `judgementBad`
- `judgementShit`
- `comboNumber0`
- `comboNumber1`
- `comboNumber2`
- `comboNumber3`
- `comboNumber4`
- `comboNumber5`
- `comboNumber6`
- `comboNumber7`
- `comboNumber8`
- `comboNumber9`
## [1.0.0]
Initial version.

View file

@ -74,6 +74,84 @@ typedef NoteStyleAssetsData =
*/
@:optional
var holdNoteCover:NoteStyleAssetData<NoteStyleData_HoldNoteCover>;
/**
* The THREE sound (and an optional pre-READY graphic).
*/
@:optional
var countdownThree:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The TWO sound and READY graphic.
*/
@:optional
var countdownTwo:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The ONE sound and SET graphic.
*/
@:optional
var countdownOne:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The GO sound and GO! graphic.
*/
@:optional
var countdownGo:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The SICK! judgement.
*/
@:optional
var judgementSick:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The GOOD! judgement.
*/
@:optional
var judgementGood:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The BAD! judgement.
*/
@:optional
var judgementBad:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The SHIT! judgement.
*/
@:optional
var judgementShit:NoteStyleAssetData<NoteStyleData_Judgement>;
@:optional
var comboNumber0:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber1:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber2:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber3:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber4:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber5:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber6:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber7:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber8:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber9:NoteStyleAssetData<NoteStyleData_ComboNum>;
}
/**
@ -109,10 +187,19 @@ typedef NoteStyleAssetData<T> =
@:optional
var isPixel:Bool;
/**
* If true, animations will be played on the graphic.
* @default `false` to save performance.
*/
@:default(false)
@:optional
var animated:Bool;
/**
* The structure of this data depends on the asset.
*/
var data:T;
@:optional
var data:Null<T>;
}
typedef NoteStyleData_Note =
@ -123,7 +210,14 @@ typedef NoteStyleData_Note =
var right:UnnamedAnimationData;
}
typedef NoteStyleData_Countdown =
{
var audioPath:String;
}
typedef NoteStyleData_HoldNote = {}
typedef NoteStyleData_Judgement = {}
typedef NoteStyleData_ComboNum = {}
/**
* Data on animations for each direction of the strumline.

View file

@ -11,9 +11,9 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateNoteStyleData()` function.
*/
public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.0.0";
public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.1.0";
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
public static var instance(get, never):NoteStyleRegistry;
static var _instance:Null<NoteStyleRegistry> = null;

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.2.4]
### Added
- Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent.
- If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent)
- Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player.
- If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player)
## [2.2.3]
### Added
- Added `charter` field to denote authorship of a chart.

View file

@ -529,12 +529,26 @@ class SongCharacterData implements ICloneable<SongCharacterData>
@:default([])
public var altInstrumentals:Array<String> = [];
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '')
@:optional
public var opponentVocals:Null<Array<String>> = null;
@:optional
public var playerVocals:Null<Array<String>> = null;
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '', ?altInstrumentals:Array<String>,
?opponentVocals:Array<String>, ?playerVocals:Array<String>)
{
this.player = player;
this.girlfriend = girlfriend;
this.opponent = opponent;
this.instrumental = instrumental;
this.altInstrumentals = altInstrumentals;
this.opponentVocals = opponentVocals;
this.playerVocals = playerVocals;
if (opponentVocals == null) this.opponentVocals = [opponent];
if (playerVocals == null) this.playerVocals = [player];
}
public function clone():SongCharacterData
@ -722,18 +736,6 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
{
return new SongEventDataRaw(this.time, this.eventKind, this.value);
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, eventKind, value, activated, getStepTime, clone)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, eventKind, value);
}
public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
@ -757,27 +759,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
}
}
public inline function getHandler():Null<SongEvent>
public function getHandler():Null<SongEvent>
{
return SongEventRegistry.getEvent(this.eventKind);
}
public inline function getSchema():Null<SongEventSchema>
public function getSchema():Null<SongEventSchema>
{
return SongEventRegistry.getEventSchema(this.eventKind);
}
public inline function getDynamic(key:String):Null<Dynamic>
public function getDynamic(key:String):Null<Dynamic>
{
return this.value == null ? null : Reflect.field(this.value, key);
}
public inline function getBool(key:String):Null<Bool>
public function getBool(key:String):Null<Bool>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getInt(key:String):Null<Int>
public function getInt(key:String):Null<Int>
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
@ -787,7 +789,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result;
}
public inline function getFloat(key:String):Null<Float>
public function getFloat(key:String):Null<Float>
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
@ -797,17 +799,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result;
}
public inline function getString(key:String):String
public function getString(key:String):String
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getArray(key:String):Array<Dynamic>
public function getArray(key:String):Array<Dynamic>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getBoolArray(key:String):Array<Bool>
public function getBoolArray(key:String):Array<Bool>
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
@ -839,6 +841,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return result;
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, eventKind, value, activated, getStepTime, clone, getHandler, getSchema, getDynamic, getBool, getInt, getFloat, getString, getArray,
getBoolArray, buildTooltip, valueAsStruct)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, eventKind, value);
}
public function clone():SongEventData
{
@ -951,12 +966,18 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return this.kind = value;
}
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
@:alias("p")
@:default([])
@:optional
public var params:Array<NoteParamData>;
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
{
this.time = time;
this.data = data;
this.length = length;
this.kind = kind;
this.params = params ?? [];
}
/**
@ -1051,9 +1072,19 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
_stepLength = null;
}
public function cloneParams():Array<NoteParamData>
{
var params:Array<NoteParamData> = [];
for (param in this.params)
{
params.push(param.clone());
}
return params;
}
public function clone():SongNoteDataRaw
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
}
public function toString():String
@ -1069,9 +1100,9 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
@:forward
abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
{
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
{
this = new SongNoteDataRaw(time, data, length, kind);
this = new SongNoteDataRaw(time, data, length, kind, params);
}
public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String
@ -1115,7 +1146,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '' || this.kind == null) return false;
}
return this.time == other.time && this.data == other.data && this.length == other.length;
return this.time == other.time && this.data == other.data && this.length == other.length && this.params == other.params;
}
@:op(A != B)
@ -1134,7 +1165,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '') return true;
}
return this.time != other.time || this.data != other.data || this.length != other.length;
return this.time != other.time || this.data != other.data || this.length != other.length || this.params != other.params;
}
@:op(A > B)
@ -1171,7 +1202,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
public function clone():SongNoteData
{
return new SongNoteData(this.time, this.data, this.length, this.kind);
return new SongNoteData(this.time, this.data, this.length, this.kind, this.params);
}
/**
@ -1183,3 +1214,30 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}
}
class NoteParamData implements ICloneable<NoteParamData>
{
@:alias("n")
public var name:String;
@:alias("v")
@:jcustomparse(funkin.data.DataParse.dynamicValue)
@:jcustomwrite(funkin.data.DataWrite.dynamicValue)
public var value:Dynamic;
public function new(name:String, value:Dynamic)
{
this.name = name;
this.value = value;
}
public function clone():NoteParamData
{
return new NoteParamData(this.name, this.value);
}
public function toString():String
{
return 'NoteParamData(${this.name}, ${this.value})';
}
}

View file

@ -199,6 +199,8 @@ class FNFLegacyImporter
{
// Handle the dumb logic for mustHitSection.
var noteData = note.data;
if (noteData < 0) continue; // Exclude Psych event notes.
if (noteData > (STRUMLINE_SIZE * 2)) noteData = noteData % (2 * STRUMLINE_SIZE); // Handle other engine event notes.
// Flip notes if mustHitSection is FALSE (not true lol).
if (!mustHitSection)

View file

@ -93,8 +93,8 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
public function listBaseGameStageIds():Array<String>
{
return [
"mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets",
"phillyBlazin",
"mainStage", "spookyMansion", "phillyTrain", "phillyTrainErect", "limoRide", "limoRideErect", "mallXmas", "mallEvil", "school", "schoolEvil",
"tankmanBattlefield", "phillyStreets", "phillyBlazin",
];
}

View file

@ -0,0 +1,55 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
class AdjustColorShader extends FlxRuntimeShader
{
public var hue(default, set):Float;
public var saturation(default, set):Float;
public var brightness(default, set):Float;
public var contrast(default, set):Float;
public function new()
{
super(Assets.getText(Paths.frag('adjustColor')));
// FlxG.debugger.addTrackerProfile(new TrackerProfile(HSVShader, ['hue', 'saturation', 'brightness', 'contrast']));
hue = 0;
saturation = 0;
brightness = 0;
contrast = 0;
}
function set_hue(value:Float):Float
{
this.setFloat('hue', value);
this.hue = value;
return this.hue;
}
function set_saturation(value:Float):Float
{
this.setFloat('saturation', value);
this.saturation = value;
return this.saturation;
}
function set_brightness(value:Float):Float
{
this.setFloat('brightness', value);
this.brightness = value;
return this.brightness;
}
function set_contrast(value:Float):Float
{
this.setFloat('contrast', value);
this.contrast = value;
return this.contrast;
}
}

View file

@ -2,6 +2,7 @@ package funkin.graphics.shaders;
import flixel.FlxCamera;
import flixel.FlxG;
import flixel.graphics.frames.FlxFrame;
import flixel.addons.display.FlxRuntimeShader;
import lime.graphics.opengl.GLProgram;
import lime.utils.Log;
@ -32,6 +33,9 @@ class RuntimePostEffectShader extends FlxRuntimeShader
// equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom)
uniform vec4 uCameraBounds;
// equals (frame.left, frame.top, frame.right, frame.bottom)
uniform vec4 uFrameBounds;
// screen coord -> world coord conversion
// returns world coord in px
vec2 screenToWorld(vec2 screenCoord) {
@ -56,6 +60,25 @@ class RuntimePostEffectShader extends FlxRuntimeShader
return (worldCoord - offset) / scale;
}
// screen coord -> frame coord conversion
// returns normalized frame coord
vec2 screenToFrame(vec2 screenCoord) {
float left = uFrameBounds.x;
float top = uFrameBounds.y;
float right = uFrameBounds.z;
float bottom = uFrameBounds.w;
float width = right - left;
float height = bottom - top;
float clampedX = clamp(screenCoord.x, left, right);
float clampedY = clamp(screenCoord.y, top, bottom);
return vec2(
(clampedX - left) / (width),
(clampedY - top) / (height)
);
}
// internally used to get the maximum `openfl_TextureCoordv`
vec2 bitmapCoordScale() {
return openfl_TextureCoordv / screenCoord;
@ -80,6 +103,8 @@ class RuntimePostEffectShader extends FlxRuntimeShader
{
super(fragmentSource, null, glVersion);
uScreenResolution.value = [FlxG.width, FlxG.height];
uCameraBounds.value = [0, 0, FlxG.width, FlxG.height];
uFrameBounds.value = [0, 0, FlxG.width, FlxG.height];
}
// basically `updateViewInfo(FlxG.width, FlxG.height, FlxG.camera)` is good
@ -89,6 +114,12 @@ class RuntimePostEffectShader extends FlxRuntimeShader
uCameraBounds.value = [camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom];
}
public function updateFrameInfo(frame:FlxFrame)
{
// NOTE: uv.width is actually the right pos and uv.height is the bottom pos
uFrameBounds.value = [frame.uv.x, frame.uv.y, frame.uv.width, frame.uv.height];
}
override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram
{
try

View file

@ -32,6 +32,14 @@ class RuntimeRainShader extends RuntimePostEffectShader
return time = value;
}
public var spriteMode(default, set):Bool = false;
function set_spriteMode(value:Bool):Bool
{
this.setBool('uSpriteMode', value);
return spriteMode = value;
}
// The scale of the rain depends on the world coordinate system, so higher resolution makes
// the raindrops smaller. This parameter can be used to adjust the total scale of the scene.
// The size of the raindrops is proportional to the value of this parameter.

View file

@ -7,6 +7,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.data.song.SongRegistry;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.data.stage.StageRegistry;
@ -233,6 +234,8 @@ class PolymodHandler
// NOTE: Scripted classes are automatically aliased to their parent class.
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
// Add blacklisting for prohibited classes and packages.
// `Sys`
@ -251,6 +254,10 @@ class PolymodHandler
// Lib.load() can load malicious DLLs
Polymod.blacklistImport('cpp.Lib');
// `Unserializer`
// Unserializerr.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
Polymod.blacklistImport('Unserializer');
// `polymod.*`
// You can probably unblacklist a module
for (cls in ClassMacro.listClassesInPackage('polymod'))
@ -383,6 +390,7 @@ class PolymodHandler
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
NoteKindManager.loadScripts();
ModuleHandler.loadModuleCache();
}
}

View file

@ -20,7 +20,7 @@ enum abstract ScriptEventType(String) from String to String
var DESTROY = 'DESTROY';
/**
* Called when the relevent object is added to the game state.
* Called when the relevant object is added to the game state.
* This assumes all data is loaded and ready to go.
*
* This event is not cancelable.

View file

@ -11,6 +11,9 @@ import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;
import funkin.util.EaseUtil;
import funkin.audio.FunkinSound;
import openfl.utils.Assets;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
class Countdown
{
@ -19,6 +22,24 @@ class Countdown
*/
public static var countdownStep(default, null):CountdownStep = BEFORE;
/**
* Which alternate graphic/sound on countdown to use.
* This is set via the current notestyle.
* For example, in Week 6 it is `pixel`.
*/
public static var soundSuffix:String = '';
/**
* Which alternate graphic on countdown to use.
* You can set this via script.
* For example, in Week 6 it is `-pixel`.
*/
public static var graphicSuffix:String = '';
static var noteStyle:NoteStyle;
static var fallbackNoteStyle:Null<NoteStyle>;
/**
* The currently running countdown. This will be null if there is no countdown running.
*/
@ -30,7 +51,7 @@ class Countdown
* This will automatically stop and restart the countdown if it is already running.
* @returns `false` if the countdown was cancelled by a script.
*/
public static function performCountdown(isPixelStyle:Bool):Bool
public static function performCountdown():Bool
{
countdownStep = BEFORE;
var cancelled:Bool = propagateCountdownEvent(countdownStep);
@ -65,10 +86,10 @@ class Countdown
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
// Countdown graphic.
showCountdownGraphic(countdownStep, isPixelStyle);
showCountdownGraphic(countdownStep);
// Countdown sound.
playCountdownSound(countdownStep, isPixelStyle);
playCountdownSound(countdownStep);
// Event handling bullshit.
var cancelled:Bool = propagateCountdownEvent(countdownStep);
@ -177,122 +198,69 @@ class Countdown
}
/**
* Retrieves the graphic to use for this step of the countdown.
* TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
*
* This is public so modules can do lol funny shit.
* Reset the countdown configuration to the default.
*/
public static function showCountdownGraphic(index:CountdownStep, isPixelStyle:Bool):Void
public static function reset()
{
var spritePath:String = null;
noteStyle = null;
}
/**
* Retrieve the note style data (if we haven't already)
* @param noteStyleId The id of the note style to fetch. Defaults to the one used by the current PlayState.
* @param force Fetch the note style from the registry even if we've already fetched it.
*/
static function fetchNoteStyle(?noteStyleId:String, force:Bool = false):Void
{
if (noteStyle != null && !force) return;
if (noteStyleId == null) noteStyleId = PlayState.instance?.currentChart?.noteStyle;
noteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
}
/**
* Retrieves the graphic to use for this step of the countdown.
*/
public static function showCountdownGraphic(index:CountdownStep):Void
{
fetchNoteStyle();
var countdownSprite = noteStyle.buildCountdownSprite(index);
if (countdownSprite == null) return;
var fadeEase = FlxEase.cubeInOut;
if (isPixelStyle)
{
fadeEase = EaseUtil.stepped(8);
switch (index)
{
case TWO:
spritePath = 'weeb/pixelUI/ready-pixel';
case ONE:
spritePath = 'weeb/pixelUI/set-pixel';
case GO:
spritePath = 'weeb/pixelUI/date-pixel';
default:
// null
}
}
else
{
switch (index)
{
case TWO:
spritePath = 'ready';
case ONE:
spritePath = 'set';
case GO:
spritePath = 'go';
default:
// null
}
}
if (spritePath == null) return;
var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
countdownSprite.scrollFactor.set(0, 0);
if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
countdownSprite.antialiasing = !isPixelStyle;
countdownSprite.updateHitbox();
countdownSprite.screenCenter();
if (noteStyle.isCountdownSpritePixel(index)) fadeEase = EaseUtil.stepped(8);
// Fade sprite in, then out, then destroy it.
FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100}, Conductor.instance.beatLengthMs / 1000,
FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000,
{
ease: FlxEase.cubeInOut,
ease: fadeEase,
onComplete: function(twn:FlxTween) {
countdownSprite.destroy();
}
});
FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000,
{
ease: fadeEase
});
countdownSprite.cameras = [PlayState.instance.camHUD];
PlayState.instance.add(countdownSprite);
countdownSprite.screenCenter();
var offsets = noteStyle.getCountdownSpriteOffsets(index);
countdownSprite.x += offsets[0];
countdownSprite.y += offsets[1];
}
/**
* Retrieves the sound file to use for this step of the countdown.
* TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
*
* This is public so modules can do lol funny shit.
*/
public static function playCountdownSound(index:CountdownStep, isPixelStyle:Bool):Void
public static function playCountdownSound(step:CountdownStep):FunkinSound
{
var soundPath:String = null;
fetchNoteStyle();
var path = noteStyle.getCountdownSoundPath(step);
if (path == null) return null;
if (isPixelStyle)
{
switch (index)
{
case THREE:
soundPath = 'intro3-pixel';
case TWO:
soundPath = 'intro2-pixel';
case ONE:
soundPath = 'intro1-pixel';
case GO:
soundPath = 'introGo-pixel';
default:
// null
}
}
else
{
switch (index)
{
case THREE:
soundPath = 'intro3';
case TWO:
soundPath = 'intro2';
case ONE:
soundPath = 'intro1';
case GO:
soundPath = 'introGo';
default:
// null
}
}
if (soundPath == null) return;
FunkinSound.playOnce(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME);
return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME);
}
public static function decrement(step:CountdownStep):CountdownStep

View file

@ -306,7 +306,7 @@ class PauseSubState extends MusicBeatSubState
metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
if (PlayState.instance?.currentDifficulty != null)
{
metadataDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase();
metadataDifficulty.text += PlayState.instance.currentDifficulty.replace('-', ' ').toTitleCase();
}
metadataDifficulty.scrollFactor.set(0, 0);
metadata.add(metadataDifficulty);

View file

@ -16,6 +16,7 @@ import flixel.tweens.FlxTween;
import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import flixel.util.FlxStringUtil;
import funkin.api.newgrounds.NGio;
import funkin.audio.FunkinSound;
import funkin.audio.VoicesGroup;
@ -48,6 +49,7 @@ import funkin.play.notes.NoteSprite;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.Strumline;
import funkin.play.notes.SustainTrail;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.play.stage.Stage;
@ -502,7 +504,7 @@ class PlayState extends MusicBeatSubState
public var camGame:FlxCamera;
/**
* The camera which contains, and controls visibility of, a video cutscene.
* The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
*/
public var camCutscene:FlxCamera;
@ -577,7 +579,8 @@ class PlayState extends MusicBeatSubState
// TODO: Refactor or document
var generatedMusic:Bool = false;
var perfectMode:Bool = false;
var skipEndingTransition:Bool = false;
static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK;
@ -693,12 +696,7 @@ class PlayState extends MusicBeatSubState
initMinimalMode();
}
initStrumlines();
// Initialize the judgements and combo meter.
comboPopUps = new PopUpStuff();
comboPopUps.zIndex = 900;
add(comboPopUps);
comboPopUps.cameras = [camHUD];
initPopups();
#if discord_rpc
// Initialize Discord Rich Presence.
@ -899,7 +897,7 @@ class PlayState extends MusicBeatSubState
health = Constants.HEALTH_STARTING;
songScore = 0;
Highscore.tallies.combo = 0;
Countdown.performCountdown(currentStageId.startsWith('school'));
Countdown.performCountdown();
needsReset = false;
}
@ -974,7 +972,7 @@ class PlayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
pauseSubState.camera = camHUD;
pauseSubState.camera = camCutscene;
openSubState(pauseSubState);
// boyfriendPos.put(); // TODO: Why is this here?
}
@ -1164,6 +1162,9 @@ class PlayState extends MusicBeatSubState
// super.dispatchEvent(event) dispatches event to module scripts.
super.dispatchEvent(event);
// Dispatch event to note kind scripts
NoteKindManager.callEvent(event);
// Dispatch event to stage script.
ScriptEventDispatcher.callEvent(currentStage, event);
@ -1175,8 +1176,6 @@ class PlayState extends MusicBeatSubState
// Dispatch event to conversation script.
ScriptEventDispatcher.callEvent(currentConversation, event);
// TODO: Dispatch event to note scripts
}
/**
@ -1347,64 +1346,13 @@ class PlayState extends MusicBeatSubState
}
/**
* Removes any references to the current stage, then clears the stage cache,
* then reloads all the stages.
*
* This is useful for when you want to edit a stage without reloading the whole game.
* Reloading works on both the JSON and the HXC, if applicable.
*
* Call this by pressing F5 on a debug build.
*/
override function debug_refreshModules():Void
override function reloadAssets():Void
{
// Prevent further gameplay updates, which will try to reference dead objects.
criticalFailure = true;
// Remove the current stage. If the stage gets deleted while it's still in use,
// it'll probably crash the game or something.
if (this.currentStage != null)
{
remove(currentStage);
var event:ScriptEvent = new ScriptEvent(DESTROY, false);
ScriptEventDispatcher.callEvent(currentStage, event);
currentStage = null;
}
if (!overrideMusic)
{
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.destroy();
FlxG.sound.music = null;
}
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.destroy();
vocals = null;
}
}
else
{
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.stop();
}
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.stop();
}
}
super.debug_refreshModules();
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentSong, event);
funkin.modding.PolymodHandler.forceReloadAssets();
lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
LoadingState.loadPlayState(lastParams);
}
override function stepHit():Bool
@ -1416,17 +1364,6 @@ class PlayState extends MusicBeatSubState
if (isGamePaused) return false;
if (!startingSong
&& FlxG.sound.music != null
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
{
trace("VOCALS NEED RESYNC");
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
resyncVocals();
}
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
@ -1448,6 +1385,17 @@ class PlayState extends MusicBeatSubState
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
}
if (!startingSong
&& FlxG.sound.music != null
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100))
{
trace("VOCALS NEED RESYNC");
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
resyncVocals();
}
// Only bop camera if zoom level is below 135%
if (Preferences.zoomCamera
&& FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
@ -1500,9 +1448,6 @@ class PlayState extends MusicBeatSubState
if (playerStrumline != null) playerStrumline.onBeatHit();
if (opponentStrumline != null) opponentStrumline.onBeatHit();
// Make the characters dance on the beat
danceOnBeat();
return true;
}
@ -1513,26 +1458,6 @@ class PlayState extends MusicBeatSubState
super.destroy();
}
/**
* Handles characters dancing to the beat of the current song.
*
* TODO: Move some of this logic into `Bopper.hx`, or individual character scripts.
*/
function danceOnBeat():Void
{
if (currentStage == null) return;
// TODO: Add HEY! song events to Tutorial.
if (Conductor.instance.currentBeat % 16 == 15
&& currentStage.getDad().characterId == 'gf'
&& Conductor.instance.currentBeat > 16
&& Conductor.instance.currentBeat < 48)
{
currentStage.getBoyfriend().playAnimation('hey', true);
currentStage.getDad().playAnimation('cheer', true);
}
}
/**
* Initializes the game and HUD cameras.
*/
@ -1799,6 +1724,21 @@ class PlayState extends MusicBeatSubState
opponentStrumline.fadeInArrows();
}
/**
* Configures the judgement and combo popups.
*/
function initPopups():Void
{
var noteStyleId:String = currentChart.noteStyle;
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
// Initialize the judgements and combo meter.
comboPopUps = new PopUpStuff(noteStyle);
comboPopUps.zIndex = 900;
add(comboPopUps);
comboPopUps.cameras = [camHUD];
}
/**
* Initializes the Discord Rich Presence.
*/
@ -1929,11 +1869,10 @@ class PlayState extends MusicBeatSubState
public function startCountdown():Void
{
// If Countdown.performCountdown returns false, then the countdown was canceled by a script.
var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school'));
var result:Bool = Countdown.performCountdown();
if (!result) return;
isInCutscene = false;
camCutscene.visible = false;
// TODO: Maybe tween in the camera after any cutscenes.
camHUD.visible = true;
@ -1999,7 +1938,9 @@ class PlayState extends MusicBeatSubState
return;
}
FlxG.sound.music.onComplete = endSong.bind(false);
FlxG.sound.music.onComplete = function() {
endSong(skipEndingTransition);
};
// A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
@ -2039,13 +1980,15 @@ class PlayState extends MusicBeatSubState
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
if (!FlxG.sound.music.playing) return;
var timeToPlayAt:Float = Conductor.instance.songPosition - Conductor.instance.instrumentalOffset;
FlxG.sound.music.pause();
vocals.pause();
FlxG.sound.music.play(FlxG.sound.music.time);
FlxG.sound.music.time = timeToPlayAt;
FlxG.sound.music.play(false, timeToPlayAt);
vocals.time = FlxG.sound.music.time;
vocals.play(false, FlxG.sound.music.time);
vocals.time = timeToPlayAt;
vocals.play(false, timeToPlayAt);
}
/**
@ -2060,7 +2003,9 @@ class PlayState extends MusicBeatSubState
}
else
{
scoreText.text = 'Score:' + songScore;
// TODO: Add an option for this maybe?
var commaSeparated:Bool = true;
scoreText.text = 'Score: ${FlxStringUtil.formatMoney(songScore, false, commaSeparated)}';
}
}
@ -2607,12 +2552,6 @@ class PlayState extends MusicBeatSubState
*/
function debugKeyShit():Void
{
#if !debug
perfectMode = false;
#else
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
#end
#if CHART_EDITOR_SUPPORTED
// Open the stage editor overlaying the current state.
if (controls.DEBUG_STAGE)
@ -2628,14 +2567,25 @@ class PlayState extends MusicBeatSubState
{
disableKeys = true;
persistentUpdate = false;
FlxG.switchState(() -> new ChartEditorState(
{
targetSongId: currentSong.id,
}));
if (isChartingMode)
{
FlxG.sound.music?.pause();
this.close();
}
else
{
FlxG.switchState(() -> new ChartEditorState(
{
targetSongId: currentSong.id,
}));
}
}
#end
#if (debug || FORCE_DEBUG_VERSION)
// H: Hide the HUD.
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
// 1: End the song immediately.
if (FlxG.keys.justPressed.ONE) endSong(true);
@ -3070,6 +3020,7 @@ class PlayState extends MusicBeatSubState
GameOverSubState.reset();
PauseSubState.reset();
Countdown.reset();
// Clear the static reference to this state.
instance = null;

View file

@ -118,22 +118,6 @@ class BaseCharacter extends Bopper
*/
public var cameraFocusPoint(default, null):FlxPoint = new FlxPoint(0, 0);
override function set_animOffsets(value:Array<Float>):Array<Float>
{
if (animOffsets == null) value = [0, 0];
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
// Make sure animOffets are halved when scale is 0.5.
var xDiff = (animOffsets[0] * this.scale.x / (this.isPixel ? 6 : 1)) - value[0];
var yDiff = (animOffsets[1] * this.scale.y / (this.isPixel ? 6 : 1)) - value[1];
// Call the super function so that camera focus point is not affected.
super.set_x(this.x + xDiff);
super.set_y(this.y + yDiff);
return animOffsets = value;
}
/**
* If the x position changes, other than via changing the animation offset,
* then we need to update the camera focus point.
@ -461,7 +445,6 @@ class BaseCharacter extends Bopper
if (!currentAnimation.startsWith('dance') && !currentAnimation.startsWith('idle') && !isAnimationFinished()) return;
}
trace('${characterId}: Actually dancing');
// Otherwise, fallback to the super dance() method, which handles playing the idle animation.
super.dance();
}
@ -522,6 +505,9 @@ class BaseCharacter extends Bopper
{
super.onNoteHit(event);
// If another script cancelled the event, don't do anything.
if (event.eventCanceled) return;
if (event.note.noteData.getMustHitNote() && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
@ -554,6 +540,9 @@ class BaseCharacter extends Bopper
{
super.onNoteMiss(event);
// If another script cancelled the event, don't do anything.
if (event.eventCanceled) return;
if (event.note.noteData.getMustHitNote() && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.

View file

@ -150,13 +150,17 @@ class HealthIcon extends FunkinSprite
{
if (characterId == 'bf-old')
{
isPixel = PlayState.instance.currentStage.getBoyfriend().isPixel;
PlayState.instance.currentStage.getBoyfriend().initHealthIcon(false);
}
else
{
characterId = 'bf-old';
isPixel = false;
loadCharacter(characterId);
}
lerpIconSize(true);
}
/**
@ -200,31 +204,45 @@ class HealthIcon extends FunkinSprite
if (bopEvery != 0)
{
// Lerp the health icon back to its normal size,
// while maintaining aspect ratio.
if (this.width > this.height)
{
// Apply linear interpolation while accounting for frame rate.
var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15));
setGraphicSize(targetSize, 0);
}
else
{
var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15));
setGraphicSize(0, targetSize);
}
lerpIconSize();
// Lerp the health icon back to its normal angle.
this.angle = MathUtil.coolLerp(this.angle, 0, 0.15);
this.updateHitbox();
}
this.updatePosition();
}
/**
* Does the calculation to lerp the icon size. Usually called every frame, but can be forced to the target size.
* Mainly forced when changing to old icon to not have a weird lerp related to changing from pixel icon to non-pixel old icon
* @param force Force the icon immedialtely to be the target size. Defaults to false.
*/
function lerpIconSize(force:Bool = false):Void
{
// Lerp the health icon back to its normal size,
// while maintaining aspect ratio.
if (this.width > this.height)
{
// Apply linear interpolation while accounting for frame rate.
var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15));
if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.x);
setGraphicSize(targetSize, 0);
}
else
{
var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15));
if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.y);
setGraphicSize(0, targetSize);
}
this.updateHitbox();
}
/**
* Update the position (and status) of the health icon.
*/
@ -412,6 +430,8 @@ class HealthIcon extends FunkinSprite
isLegacyStyle = !isNewSpritesheet(charId);
trace(' Loading health icon for character: $charId (legacy: $isLegacyStyle)');
if (!isLegacyStyle)
{
loadSparrow('icons/icon-$charId');

View file

@ -8,58 +8,51 @@ import funkin.graphics.FunkinSprite;
import funkin.play.PlayState;
import funkin.util.TimerUtil;
import funkin.util.EaseUtil;
import openfl.utils.Assets;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
@:nullSafety
class PopUpStuff extends FlxTypedGroup<FunkinSprite>
{
public var offsets:Array<Int> = [0, 0];
/**
* The current note style to use. This determines which graphics to display.
* For example, Week 6 uses the `pixel` note style, and mods can create their own.
*/
var noteStyle:NoteStyle;
override public function new()
override public function new(noteStyle:NoteStyle)
{
super();
this.noteStyle = noteStyle;
}
public function displayRating(daRating:String):Void
public function displayRating(daRating:Null<String>)
{
var perfStart:Float = TimerUtil.start();
if (daRating == null) daRating = "good";
var ratingPath:String = daRating;
if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
var rating:FunkinSprite = FunkinSprite.create(0, 0, ratingPath);
rating.scrollFactor.set(0.2, 0.2);
var rating:Null<FunkinSprite> = noteStyle.buildJudgementSprite(daRating);
if (rating == null) return;
rating.zIndex = 1000;
rating.x = (FlxG.width * 0.474) + offsets[0];
// rating.x -= FlxG.camera.scroll.x * 0.2;
rating.y = (FlxG.camera.height * 0.45 - 60) + offsets[1];
rating.x = (FlxG.width * 0.474);
rating.x -= rating.width / 2;
rating.y = (FlxG.camera.height * 0.45 - 60);
rating.y -= rating.height / 2;
var offsets = noteStyle.getJudgementSpriteOffsets(daRating);
rating.x += offsets[0];
rating.y += offsets[1];
rating.acceleration.y = 550;
rating.velocity.y -= FlxG.random.int(140, 175);
rating.velocity.x -= FlxG.random.int(0, 10);
add(rating);
var fadeEase = null;
if (PlayState.instance.currentStageId.startsWith('school'))
{
rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7));
rating.antialiasing = false;
rating.pixelPerfectRender = true;
rating.pixelPerfectPosition = true;
fadeEase = EaseUtil.stepped(2);
}
else
{
rating.setGraphicSize(Std.int(rating.width * 0.65));
rating.antialiasing = true;
}
rating.updateHitbox();
rating.x -= rating.width / 2;
rating.y -= rating.height / 2;
var fadeEase = noteStyle.isJudgementSpritePixel(daRating) ? EaseUtil.stepped(2) : null;
FlxTween.tween(rating, {alpha: 0}, 0.2,
{
@ -70,62 +63,10 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
startDelay: Conductor.instance.beatLengthMs * 0.001,
ease: fadeEase
});
trace('displayRating took: ${TimerUtil.seconds(perfStart)}');
}
public function displayCombo(?combo:Int = 0):Int
public function displayCombo(combo:Int = 0):Void
{
var perfStart:Float = TimerUtil.start();
if (combo == null) combo = 0;
var pixelShitPart1:String = "";
var pixelShitPart2:String = '';
if (PlayState.instance.currentStageId.startsWith('school'))
{
pixelShitPart1 = 'weeb/pixelUI/';
pixelShitPart2 = '-pixel';
}
var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2);
comboSpr.y = (FlxG.camera.height * 0.44) + offsets[1];
comboSpr.x = (FlxG.width * 0.507) + offsets[0];
// comboSpr.x -= FlxG.camera.scroll.x * 0.2;
comboSpr.acceleration.y = 600;
comboSpr.velocity.y -= 150;
comboSpr.velocity.x += FlxG.random.int(1, 10);
// add(comboSpr);
var fadeEase = null;
if (PlayState.instance.currentStageId.startsWith('school'))
{
comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 1));
comboSpr.antialiasing = false;
comboSpr.pixelPerfectRender = true;
comboSpr.pixelPerfectPosition = true;
fadeEase = EaseUtil.stepped(2);
}
else
{
comboSpr.setGraphicSize(Std.int(comboSpr.width * 0.7));
comboSpr.antialiasing = true;
}
comboSpr.updateHitbox();
FlxTween.tween(comboSpr, {alpha: 0}, 0.2,
{
onComplete: function(tween:FlxTween) {
remove(comboSpr, true);
comboSpr.destroy();
},
startDelay: Conductor.instance.beatLengthMs * 0.001,
ease: fadeEase
});
var seperatedScore:Array<Int> = [];
var tempCombo:Int = combo;
@ -140,31 +81,27 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
// seperatedScore.reverse();
var daLoop:Int = 1;
for (i in seperatedScore)
for (digit in seperatedScore)
{
var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2);
var numScore:Null<FunkinSprite> = noteStyle.buildComboNumSprite(digit);
if (numScore == null) continue;
if (PlayState.instance.currentStageId.startsWith('school'))
{
numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 1));
numScore.antialiasing = false;
numScore.pixelPerfectRender = true;
numScore.pixelPerfectPosition = true;
}
else
{
numScore.setGraphicSize(Std.int(numScore.width * 0.45));
numScore.antialiasing = true;
}
numScore.updateHitbox();
numScore.x = (FlxG.width * 0.507) - (36 * daLoop) - 65;
trace('numScore($daLoop) = ${numScore.x}');
numScore.y = (FlxG.camera.height * 0.44);
var offsets = noteStyle.getComboNumSpriteOffsets(digit);
numScore.x += offsets[0];
numScore.y += offsets[1];
numScore.x = comboSpr.x - (36 * daLoop) - 65; //- 90;
numScore.acceleration.y = FlxG.random.int(250, 300);
numScore.velocity.y -= FlxG.random.int(130, 150);
numScore.velocity.x = FlxG.random.float(-5, 5);
add(numScore);
var fadeEase = noteStyle.isComboNumSpritePixel(digit) ? EaseUtil.stepped(2) : null;
FlxTween.tween(numScore, {alpha: 0}, 0.2,
{
onComplete: function(tween:FlxTween) {
@ -177,9 +114,5 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
daLoop++;
}
trace('displayCombo took: ${TimerUtil.seconds(perfStart)}');
return combo;
}
}

View file

@ -81,7 +81,6 @@ class VideoCutscene
// Trigger the cutscene. Don't play the song in the background.
PlayState.instance.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
PlayState.instance.camCutscene.visible = true;
// Display a black screen to hide the game while the video is playing.
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
@ -305,7 +304,6 @@ class VideoCutscene
vid = null;
#end
PlayState.instance.camCutscene.visible = true;
PlayState.instance.camHUD.visible = true;
FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,

View file

@ -1,6 +1,7 @@
package funkin.play.notes;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.NoteParamData;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
@ -65,6 +66,22 @@ class NoteSprite extends FunkinSprite
return this.noteData.kind = value;
}
/**
* An array of custom parameters for this note
*/
public var params(get, set):Array<NoteParamData>;
function get_params():Array<NoteParamData>
{
return this.noteData?.params ?? [];
}
function set_params(value:Array<NoteParamData>):Array<NoteParamData>
{
if (this.noteData == null) return value;
return this.noteData.params = value;
}
/**
* The data of the note (i.e. the direction.)
*/
@ -74,7 +91,7 @@ class NoteSprite extends FunkinSprite
{
if (frames == null) return value;
animation.play(DIRECTION_COLORS[value] + 'Scroll');
playNoteAnimation(value);
this.direction = value;
return this.direction;
@ -135,19 +152,37 @@ class NoteSprite extends FunkinSprite
this.hsvShader = new HSVShader();
setupNoteGraphic(noteStyle);
// Disables the update() function for performance.
this.active = false;
}
function setupNoteGraphic(noteStyle:NoteStyle):Void
/**
* Creates frames and animations
* @param noteStyle The `NoteStyle` instance
*/
public function setupNoteGraphic(noteStyle:NoteStyle):Void
{
noteStyle.buildNoteSprite(this);
setGraphicSize(Strumline.STRUMLINE_SIZE);
updateHitbox();
this.shader = hsvShader;
// `false` disables the update() function for performance.
this.active = noteStyle.isNoteAnimated();
}
/**
* Retrieve the value of the param with the given name
* @param name Name of the param
* @return Null<Dynamic>
*/
public function getParam(name:String):Null<Dynamic>
{
for (param in params)
{
if (param.name == name)
{
return param.value;
}
}
return null;
}
#if FLX_DEBUG
@ -173,6 +208,11 @@ class NoteSprite extends FunkinSprite
}
#end
function playNoteAnimation(value:Int):Void
{
animation.play(DIRECTION_COLORS[value] + 'Scroll');
}
public function desaturate():Void
{
this.hsvShader.saturation = 0.2;

View file

@ -16,6 +16,7 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.ui.options.PreferencesMenu;
import funkin.util.SortUtil;
import funkin.modding.events.ScriptEvent;
import funkin.play.notes.notekind.NoteKindManager;
/**
* A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player.
@ -598,7 +599,6 @@ class Strumline extends FlxSpriteGroup
{
note.holdNoteSprite.hitNote = true;
note.holdNoteSprite.missedNote = false;
note.holdNoteSprite.alpha = 1.0;
note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
}
@ -709,11 +709,15 @@ class Strumline extends FlxSpriteGroup
if (noteSprite != null)
{
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
noteSprite.setupNoteGraphic(noteKindStyle);
noteSprite.direction = note.getDirection();
noteSprite.noteData = note;
noteSprite.x = this.x;
noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
noteSprite.x -= (noteSprite.width - Strumline.STRUMLINE_SIZE) / 2; // Center it
noteSprite.x -= NUDGE;
// noteSprite.x += INITIAL_OFFSET;
noteSprite.y = -9999;
@ -728,6 +732,9 @@ class Strumline extends FlxSpriteGroup
if (holdNoteSprite != null)
{
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
holdNoteSprite.setupHoldNoteGraphic(noteKindStyle);
holdNoteSprite.parentStrumline = this;
holdNoteSprite.noteData = note;
holdNoteSprite.strumTime = note.time;

View file

@ -75,6 +75,13 @@ class StrumlineNote extends FlxSprite
function setup(noteStyle:NoteStyle):Void
{
if (noteStyle == null)
{
// If you get an exception on this line, check the debug console.
// You probably have a parsing error in your note style's JSON file.
throw "FATAL ERROR: Attempted to initialize PlayState with an invalid NoteStyle.";
}
noteStyle.applyStrumlineFrames(this);
noteStyle.applyStrumlineAnimations(this, this.direction);

View file

@ -99,7 +99,27 @@ class SustainTrail extends FlxSprite
*/
public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle)
{
super(0, 0, noteStyle.getHoldNoteAssetPath());
super(0, 0);
// BASIC SETUP
this.sustainLength = sustainLength;
this.fullSustainLength = sustainLength;
this.noteDirection = noteDirection;
setupHoldNoteGraphic(noteStyle);
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
this.active = true; // This NEEDS to be true for the note to be drawn!
}
/**
* Creates hold note graphic and applies correct zooming
* @param noteStyle The note style
*/
public function setupHoldNoteGraphic(noteStyle:NoteStyle):Void
{
loadGraphic(noteStyle.getHoldNoteAssetPath());
antialiasing = true;
@ -109,13 +129,14 @@ class SustainTrail extends FlxSprite
endOffset = bottomClip = 1;
antialiasing = false;
}
else
{
endOffset = 0.5;
bottomClip = 0.9;
}
zoom = 1.0;
zoom *= noteStyle.fetchHoldNoteScale();
// BASIC SETUP
this.sustainLength = sustainLength;
this.fullSustainLength = sustainLength;
this.noteDirection = noteDirection;
zoom *= 0.7;
// CALCULATE SIZE
@ -131,9 +152,6 @@ class SustainTrail extends FlxSprite
updateColorTransform();
updateClipping();
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
this.active = true; // This NEEDS to be true for the note to be drawn!
}
function getBaseScrollSpeed()
@ -195,6 +213,11 @@ class SustainTrail extends FlxSprite
*/
public function updateClipping(songTime:Float = 0):Void
{
if (graphic == null)
{
return;
}
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight);
if (clipHeight <= 0.1)
{

View file

@ -0,0 +1,119 @@
package funkin.play.notes.notekind;
import funkin.modding.IScriptedClass.INoteScriptedClass;
import funkin.modding.events.ScriptEvent;
import flixel.math.FlxMath;
/**
* Class for note scripts
*/
class NoteKind implements INoteScriptedClass
{
/**
* The name of the note kind
*/
public var noteKind:String;
/**
* Description used in chart editor
*/
public var description:String;
/**
* Custom note style
*/
public var noteStyleId:Null<String>;
/**
* Custom parameters for the chart editor
*/
public var params:Array<NoteKindParam>;
public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array<NoteKindParam>)
{
this.noteKind = noteKind;
this.description = description;
this.noteStyleId = noteStyleId;
this.params = params ?? [];
}
public function toString():String
{
return noteKind;
}
/**
* Retrieve all notes of this kind
* @return Array<NoteSprite>
*/
function getNotes():Array<NoteSprite>
{
var allNotes:Array<NoteSprite> = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members);
return allNotes.filter(function(note:NoteSprite) {
return note != null && note.noteData.kind == this.noteKind;
});
}
public function onScriptEvent(event:ScriptEvent):Void {}
public function onCreate(event:ScriptEvent):Void {}
public function onDestroy(event:ScriptEvent):Void {}
public function onUpdate(event:UpdateScriptEvent):Void {}
public function onNoteIncoming(event:NoteScriptEvent):Void {}
public function onNoteHit(event:HitNoteScriptEvent):Void {}
public function onNoteMiss(event:NoteScriptEvent):Void {}
}
/**
* Abstract for setting the type of the `NoteKindParam`
* This was supposed to be an enum but polymod kept being annoying
*/
abstract NoteKindParamType(String) from String to String
{
public static final STRING:String = 'String';
public static final INT:String = 'Int';
public static final FLOAT:String = 'Float';
}
typedef NoteKindParamData =
{
/**
* If `min` is null, there is no minimum
*/
?min:Null<Float>,
/**
* If `max` is null, there is no maximum
*/
?max:Null<Float>,
/**
* If `step` is null, it will use 1.0
*/
?step:Null<Float>,
/**
* If `precision` is null, there will be 0 decimal places
*/
?precision:Null<Int>,
?defaultValue:Dynamic
}
/**
* Typedef for creating custom parameters in the chart editor
*/
typedef NoteKindParam =
{
name:String,
description:String,
type:NoteKindParamType,
?data:NoteKindParamData
}

View file

@ -0,0 +1,121 @@
package funkin.play.notes.notekind;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.notekind.ScriptedNoteKind;
import funkin.play.notes.notekind.NoteKind.NoteKindParam;
class NoteKindManager
{
static var noteKinds:Map<String, NoteKind> = [];
public static function loadScripts():Void
{
var scriptedClassName:Array<String> = ScriptedNoteKind.listScriptClasses();
if (scriptedClassName.length > 0)
{
trace('Instantiating ${scriptedClassName.length} scripted note kind(s)...');
for (scriptedClass in scriptedClassName)
{
try
{
var script:NoteKind = ScriptedNoteKind.init(scriptedClass, "unknown");
trace(' Initialized scripted note kind: ${script.noteKind}');
noteKinds.set(script.noteKind, script);
ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description);
}
catch (e)
{
trace(' FAILED to instantiate scripted note kind: ${scriptedClass}');
trace(e);
}
}
}
}
/**
* Calls the given event for note kind scripts
* @param event The event
*/
public static function callEvent(event:ScriptEvent):Void
{
// if it is a note script event,
// then only call the event for the specific note kind script
if (Std.isOfType(event, NoteScriptEvent))
{
var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
var noteKind:NoteKind = noteKinds.get(noteEvent.note.kind);
if (noteKind != null)
{
ScriptEventDispatcher.callEvent(noteKind, event);
}
}
else // call the event for all note kind scripts
{
for (noteKind in noteKinds.iterator())
{
ScriptEventDispatcher.callEvent(noteKind, event);
}
}
}
/**
* Retrieve the note style from the given note kind
* @param noteKind note kind name
* @param suffix Used for song note styles
* @return NoteStyle
*/
public static function getNoteStyle(noteKind:String, ?suffix:String):Null<NoteStyle>
{
var noteStyleId:Null<String> = getNoteStyleId(noteKind, suffix);
if (noteStyleId == null)
{
return null;
}
return NoteStyleRegistry.instance.fetchEntry(noteStyleId);
}
/**
* Retrieve the note style id from the given note kind
* @param noteKind Note kind name
* @param suffix Used for song note styles
* @return Null<String>
*/
public static function getNoteStyleId(noteKind:String, ?suffix:String):Null<String>
{
if (suffix == '')
{
suffix = null;
}
var noteStyleId:Null<String> = noteKinds.get(noteKind)?.noteStyleId;
if (noteStyleId != null && suffix != null)
{
noteStyleId = NoteStyleRegistry.instance.hasEntry('$noteStyleId-$suffix') ? '$noteStyleId-$suffix' : noteStyleId;
}
return noteStyleId;
}
/**
* Retrive custom params of the given note kind
* @param noteKind Name of the note kind
* @return Array<NoteKindParam>
*/
public static function getParams(noteKind:Null<String>):Array<NoteKindParam>
{
if (noteKind == null)
{
return [];
}
return noteKinds.get(noteKind)?.params ?? [];
}
}

View file

@ -0,0 +1,9 @@
package funkin.play.notes.notekind;
/**
* A script that can be tied to a NoteKind.
* Create a scripted class that extends NoteKind,
* then call `super('noteKind')` in the constructor to use this.
*/
@:hscriptClass
class ScriptedNoteKind extends NoteKind implements polymod.hscript.HScriptedClass {}

View file

@ -1,5 +1,6 @@
package funkin.play.notes.notestyle;
import funkin.play.Countdown;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.data.animation.AnimationData;
@ -16,6 +17,7 @@ using funkin.data.animation.AnimationData.AnimationDataUtil;
* Holds the data for what assets to use for a note style,
* and provides convenience methods for building sprites based on them.
*/
@:nullSafety
class NoteStyle implements IRegistryEntry<NoteStyleData>
{
/**
@ -42,12 +44,8 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
this.id = id;
_data = _fetchData(id);
if (_data == null)
{
throw 'Could not parse note style data for id: $id';
}
this.fallback = NoteStyleRegistry.instance.fetchEntry(getFallbackID());
var fallbackID = _data.fallback;
if (fallbackID != null) this.fallback = NoteStyleRegistry.instance.fetchEntry(fallbackID);
}
/**
@ -72,7 +70,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
* Get the note style ID of the parent note style.
* @return The string ID, or `null` if there is no parent.
*/
function getFallbackID():Null<String>
public function getFallbackID():Null<String>
{
return _data.fallback;
}
@ -80,7 +78,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
public function buildNoteSprite(target:NoteSprite):Void
{
// Apply the note sprite frames.
var atlas:FlxAtlasFrames = buildNoteFrames(false);
var atlas:Null<FlxAtlasFrames> = buildNoteFrames(false);
if (atlas == null)
{
@ -89,29 +87,40 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
target.frames = atlas;
target.scale.x = _data.assets.note.scale;
target.scale.y = _data.assets.note.scale;
target.antialiasing = !_data.assets.note.isPixel;
target.antialiasing = !(_data.assets?.note?.isPixel ?? false);
// Apply the animations.
buildNoteAnimations(target);
// Set the scale.
target.setGraphicSize(Strumline.STRUMLINE_SIZE * getNoteScale());
target.updateHitbox();
}
var noteFrames:FlxAtlasFrames = null;
var noteFrames:Null<FlxAtlasFrames> = null;
function buildNoteFrames(force:Bool = false):FlxAtlasFrames
function buildNoteFrames(force:Bool = false):Null<FlxAtlasFrames>
{
if (!FunkinSprite.isTextureCached(Paths.image(getNoteAssetPath())))
var noteAssetPath = getNoteAssetPath();
if (noteAssetPath == null) return null;
if (!FunkinSprite.isTextureCached(Paths.image(noteAssetPath)))
{
FlxG.log.warn('Note texture is not cached: ${getNoteAssetPath()}');
FlxG.log.warn('Note texture is not cached: ${noteAssetPath}');
}
// Purge the note frames if the cached atlas is invalid.
if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null;
@:nullSafety(Off)
{
if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null;
}
if (noteFrames != null && !force) return noteFrames;
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
var noteAssetPath = getNoteAssetPath();
if (noteAssetPath == null) return null;
noteFrames = Paths.getSparrowAtlas(noteAssetPath, getNoteAssetLibrary());
if (noteFrames == null)
{
@ -121,17 +130,18 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return noteFrames;
}
function getNoteAssetPath(raw:Bool = false):String
function getNoteAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.note?.assetPath;
if (rawPath == null) return fallback.getNoteAssetPath(true);
if (rawPath == null && fallback != null) return fallback.getNoteAssetPath(true);
return rawPath;
}
// library:path
var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return getNoteAssetPath(true);
return parts[1];
}
@ -139,47 +149,63 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
function getNoteAssetLibrary():Null<String>
{
// library:path
var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return null;
return parts[0];
}
function buildNoteAnimations(target:NoteSprite):Void
{
var leftData:AnimationData = fetchNoteAnimationData(LEFT);
target.animation.addByPrefix('purpleScroll', leftData.prefix, leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY);
var downData:AnimationData = fetchNoteAnimationData(DOWN);
target.animation.addByPrefix('blueScroll', downData.prefix, downData.frameRate, downData.looped, downData.flipX, downData.flipY);
var upData:AnimationData = fetchNoteAnimationData(UP);
target.animation.addByPrefix('greenScroll', upData.prefix, upData.frameRate, upData.looped, upData.flipX, upData.flipY);
var rightData:AnimationData = fetchNoteAnimationData(RIGHT);
target.animation.addByPrefix('redScroll', rightData.prefix, rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
var leftData:Null<AnimationData> = fetchNoteAnimationData(LEFT);
if (leftData != null) target.animation.addByPrefix('purpleScroll', leftData.prefix ?? '', leftData.frameRate ?? 24, leftData.looped ?? false,
leftData.flipX, leftData.flipY);
var downData:Null<AnimationData> = fetchNoteAnimationData(DOWN);
if (downData != null) target.animation.addByPrefix('blueScroll', downData.prefix ?? '', downData.frameRate ?? 24, downData.looped ?? false,
downData.flipX, downData.flipY);
var upData:Null<AnimationData> = fetchNoteAnimationData(UP);
if (upData != null) target.animation.addByPrefix('greenScroll', upData.prefix ?? '', upData.frameRate ?? 24, upData.looped ?? false, upData.flipX,
upData.flipY);
var rightData:Null<AnimationData> = fetchNoteAnimationData(RIGHT);
if (rightData != null) target.animation.addByPrefix('redScroll', rightData.prefix ?? '', rightData.frameRate ?? 24, rightData.looped ?? false,
rightData.flipX, rightData.flipY);
}
function fetchNoteAnimationData(dir:NoteDirection):AnimationData
public function isNoteAnimated():Bool
{
return _data.assets?.note?.animated ?? false;
}
public function getNoteScale():Float
{
return _data.assets?.note?.scale ?? 1.0;
}
function fetchNoteAnimationData(dir:NoteDirection):Null<AnimationData>
{
var result:Null<AnimationData> = switch (dir)
{
case LEFT: _data.assets.note.data.left.toNamed();
case DOWN: _data.assets.note.data.down.toNamed();
case UP: _data.assets.note.data.up.toNamed();
case RIGHT: _data.assets.note.data.right.toNamed();
case LEFT: _data.assets?.note?.data?.left?.toNamed();
case DOWN: _data.assets?.note?.data?.down?.toNamed();
case UP: _data.assets?.note?.data?.up?.toNamed();
case RIGHT: _data.assets?.note?.data?.right?.toNamed();
};
return (result == null) ? fallback.fetchNoteAnimationData(dir) : result;
return (result == null && fallback != null) ? fallback.fetchNoteAnimationData(dir) : result;
}
public function getHoldNoteAssetPath(raw:Bool = false):String
public function getHoldNoteAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
// TODO: figure out why ?. didn't work here
var rawPath:Null<String> = (_data?.assets?.holdNote == null) ? null : _data?.assets?.holdNote?.assetPath;
return (rawPath == null) ? fallback.getHoldNoteAssetPath(true) : rawPath;
return (rawPath == null && fallback != null) ? fallback.getHoldNoteAssetPath(true) : rawPath;
}
// library:path
var parts = getHoldNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
var parts = getHoldNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return Paths.image(parts[0]);
return Paths.image(parts[1], parts[0]);
}
@ -187,15 +213,15 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
public function isHoldNotePixel():Bool
{
var data = _data?.assets?.holdNote;
if (data == null) return fallback.isHoldNotePixel();
return data.isPixel;
if (data == null && fallback != null) return fallback.isHoldNotePixel();
return data?.isPixel ?? false;
}
public function fetchHoldNoteScale():Float
{
var data = _data?.assets?.holdNote;
if (data == null) return fallback.fetchHoldNoteScale();
return data.scale;
if (data == null && fallback != null) return fallback.fetchHoldNoteScale();
return data?.scale ?? 1.0;
}
public function applyStrumlineFrames(target:StrumlineNote):Void
@ -203,7 +229,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
// TODO: Add support for multi-Sparrow.
// Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772
var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath(), getStrumlineAssetLibrary());
var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath() ?? '', getStrumlineAssetLibrary());
if (atlas == null)
{
@ -212,31 +238,30 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
target.frames = atlas;
target.scale.x = _data.assets.noteStrumline.scale;
target.scale.y = _data.assets.noteStrumline.scale;
target.antialiasing = !_data.assets.noteStrumline.isPixel;
target.scale.set(_data.assets.noteStrumline?.scale ?? 1.0);
target.antialiasing = !(_data.assets.noteStrumline?.isPixel ?? false);
}
function getStrumlineAssetPath(raw:Bool = false):String
function getStrumlineAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.noteStrumline?.assetPath;
if (rawPath == null) return fallback.getStrumlineAssetPath(true);
if (rawPath == null && fallback != null) return fallback.getStrumlineAssetPath(true);
return rawPath;
}
// library:path
var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return getStrumlineAssetPath(true);
var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length <= 1) return getStrumlineAssetPath(true);
return parts[1];
}
function getStrumlineAssetLibrary():Null<String>
{
// library:path
var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return null;
var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length <= 1) return null;
return parts[0];
}
@ -247,60 +272,592 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
function getStrumlineAnimationData(dir:NoteDirection):Array<AnimationData>
{
var result:Array<AnimationData> = switch (dir)
var result:Array<Null<AnimationData>> = switch (dir)
{
case NoteDirection.LEFT: [
_data.assets.noteStrumline.data.leftStatic.toNamed('static'),
_data.assets.noteStrumline.data.leftPress.toNamed('press'),
_data.assets.noteStrumline.data.leftConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.leftConfirmHold.toNamed('confirm-hold'),
_data.assets.noteStrumline?.data?.leftStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.leftPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.leftConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.leftConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.DOWN: [
_data.assets.noteStrumline.data.downStatic.toNamed('static'),
_data.assets.noteStrumline.data.downPress.toNamed('press'),
_data.assets.noteStrumline.data.downConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.downConfirmHold.toNamed('confirm-hold'),
_data.assets.noteStrumline?.data?.downStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.downPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.downConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.downConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.UP: [
_data.assets.noteStrumline.data.upStatic.toNamed('static'),
_data.assets.noteStrumline.data.upPress.toNamed('press'),
_data.assets.noteStrumline.data.upConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.upConfirmHold.toNamed('confirm-hold'),
_data.assets.noteStrumline?.data?.upStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.upPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.upConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.upConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.RIGHT: [
_data.assets.noteStrumline.data.rightStatic.toNamed('static'),
_data.assets.noteStrumline.data.rightPress.toNamed('press'),
_data.assets.noteStrumline.data.rightConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.rightConfirmHold.toNamed('confirm-hold'),
_data.assets.noteStrumline?.data?.rightStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.rightPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.rightConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.rightConfirmHold?.toNamed('confirm-hold'),
];
default: [];
};
return result;
return thx.Arrays.filterNull(result);
}
public function applyStrumlineOffsets(target:StrumlineNote)
public function applyStrumlineOffsets(target:StrumlineNote):Void
{
target.x += _data.assets.noteStrumline.offsets[0];
target.y += _data.assets.noteStrumline.offsets[1];
var offsets = _data?.assets?.noteStrumline?.offsets ?? [0.0, 0.0];
target.x += offsets[0];
target.y += offsets[1];
}
public function getStrumlineScale():Float
{
return _data.assets.noteStrumline.scale;
return _data?.assets?.noteStrumline?.scale ?? 1.0;
}
public function isNoteSplashEnabled():Bool
{
var data = _data?.assets?.noteSplash?.data;
if (data == null) return fallback.isNoteSplashEnabled();
return data.enabled;
if (data == null) return fallback?.isNoteSplashEnabled() ?? false;
return data.enabled ?? false;
}
public function isHoldNoteCoverEnabled():Bool
{
var data = _data?.assets?.holdNoteCover?.data;
if (data == null) return fallback.isHoldNoteCoverEnabled();
return data.enabled;
if (data == null) return fallback?.isHoldNoteCoverEnabled() ?? false;
return data.enabled ?? false;
}
/**
* Build a sprite for the given step of the countdown.
* @param step
* @return A `FunkinSprite`, or `null` if no graphic is available for this step.
*/
public function buildCountdownSprite(step:Countdown.CountdownStep):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (step)
{
case THREE:
if (_data.assets.countdownThree == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownThree?.scale ?? 1.0;
result.scale.y = _data.assets.countdownThree?.scale ?? 1.0;
case TWO:
if (_data.assets.countdownTwo == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownTwo?.scale ?? 1.0;
result.scale.y = _data.assets.countdownTwo?.scale ?? 1.0;
case ONE:
if (_data.assets.countdownOne == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownOne?.scale ?? 1.0;
result.scale.y = _data.assets.countdownOne?.scale ?? 1.0;
case GO:
if (_data.assets.countdownGo == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownGo?.scale ?? 1.0;
result.scale.y = _data.assets.countdownGo?.scale ?? 1.0;
default:
// TODO: Do something here?
return null;
}
result.scrollFactor.set(0, 0);
result.antialiasing = !isCountdownSpritePixel(step);
result.updateHitbox();
return result;
}
function buildCountdownSpritePath(step:Countdown.CountdownStep):Null<String>
{
var basePath:Null<String> = null;
switch (step)
{
case THREE:
basePath = _data.assets.countdownThree?.assetPath;
case TWO:
basePath = _data.assets.countdownTwo?.assetPath;
case ONE:
basePath = _data.assets.countdownOne?.assetPath;
case GO:
basePath = _data.assets.countdownGo?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildCountdownSpritePath(step);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
function buildCountdownSpriteLibrary(step:Countdown.CountdownStep):Null<String>
{
var basePath:Null<String> = null;
switch (step)
{
case THREE:
basePath = _data.assets.countdownThree?.assetPath;
case TWO:
basePath = _data.assets.countdownTwo?.assetPath;
case ONE:
basePath = _data.assets.countdownOne?.assetPath;
case GO:
basePath = _data.assets.countdownGo?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildCountdownSpriteLibrary(step);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length <= 1) return null;
return parts[0];
}
public function isCountdownSpritePixel(step:Countdown.CountdownStep):Bool
{
switch (step)
{
case THREE:
var result = _data.assets.countdownThree?.isPixel;
if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step);
return result ?? false;
case TWO:
var result = _data.assets.countdownTwo?.isPixel;
if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step);
return result ?? false;
case ONE:
var result = _data.assets.countdownOne?.isPixel;
if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step);
return result ?? false;
case GO:
var result = _data.assets.countdownGo?.isPixel;
if (result == null && fallback != null) result = fallback.isCountdownSpritePixel(step);
return result ?? false;
default:
return false;
}
}
public function getCountdownSpriteOffsets(step:Countdown.CountdownStep):Array<Float>
{
switch (step)
{
case THREE:
var result = _data.assets.countdownThree?.offsets;
if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step);
return result ?? [0, 0];
case TWO:
var result = _data.assets.countdownTwo?.offsets;
if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step);
return result ?? [0, 0];
case ONE:
var result = _data.assets.countdownOne?.offsets;
if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step);
return result ?? [0, 0];
case GO:
var result = _data.assets.countdownGo?.offsets;
if (result == null && fallback != null) result = fallback.getCountdownSpriteOffsets(step);
return result ?? [0, 0];
default:
return [0, 0];
}
}
public function getCountdownSoundPath(step:Countdown.CountdownStep, raw:Bool = false):Null<String>
{
if (raw)
{
// TODO: figure out why ?. didn't work here
var rawPath:Null<String> = switch (step)
{
case Countdown.CountdownStep.THREE:
_data.assets.countdownThree?.data?.audioPath;
case Countdown.CountdownStep.TWO:
_data.assets.countdownTwo?.data?.audioPath;
case Countdown.CountdownStep.ONE:
_data.assets.countdownOne?.data?.audioPath;
case Countdown.CountdownStep.GO:
_data.assets.countdownGo?.data?.audioPath;
default:
null;
}
return (rawPath == null && fallback != null) ? fallback.getCountdownSoundPath(step, true) : rawPath;
}
// library:path
var parts = getCountdownSoundPath(step, true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return Paths.image(parts[0]);
return Paths.sound(parts[1], parts[0]);
}
public function buildJudgementSprite(rating:String):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (rating)
{
case "sick":
if (_data.assets.judgementSick == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementSick?.scale ?? 1.0;
result.scale.y = _data.assets.judgementSick?.scale ?? 1.0;
case "good":
if (_data.assets.judgementGood == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementGood?.scale ?? 1.0;
result.scale.y = _data.assets.judgementGood?.scale ?? 1.0;
case "bad":
if (_data.assets.judgementBad == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementBad?.scale ?? 1.0;
result.scale.y = _data.assets.judgementBad?.scale ?? 1.0;
case "shit":
if (_data.assets.judgementShit == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementShit?.scale ?? 1.0;
result.scale.y = _data.assets.judgementShit?.scale ?? 1.0;
default:
return null;
}
result.scrollFactor.set(0.2, 0.2);
var isPixel = isJudgementSpritePixel(rating);
result.antialiasing = !isPixel;
result.pixelPerfectRender = isPixel;
result.pixelPerfectPosition = isPixel;
result.updateHitbox();
return result;
}
public function isJudgementSpritePixel(rating:String):Bool
{
switch (rating)
{
case "sick":
var result = _data.assets.judgementSick?.isPixel;
if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating);
return result ?? false;
case "good":
var result = _data.assets.judgementGood?.isPixel;
if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating);
return result ?? false;
case "bad":
var result = _data.assets.judgementBad?.isPixel;
if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating);
return result ?? false;
case "GO":
var result = _data.assets.judgementShit?.isPixel;
if (result == null && fallback != null) result = fallback.isJudgementSpritePixel(rating);
return result ?? false;
default:
return false;
}
}
function buildJudgementSpritePath(rating:String):Null<String>
{
var basePath:Null<String> = null;
switch (rating)
{
case "sick":
basePath = _data.assets.judgementSick?.assetPath;
case "good":
basePath = _data.assets.judgementGood?.assetPath;
case "bad":
basePath = _data.assets.judgementBad?.assetPath;
case "shit":
basePath = _data.assets.judgementShit?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildJudgementSpritePath(rating);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
public function getJudgementSpriteOffsets(rating:String):Array<Float>
{
switch (rating)
{
case "sick":
var result = _data.assets.judgementSick?.offsets;
if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating);
return result ?? [0, 0];
case "good":
var result = _data.assets.judgementGood?.offsets;
if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating);
return result ?? [0, 0];
case "bad":
var result = _data.assets.judgementBad?.offsets;
if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating);
return result ?? [0, 0];
case "shit":
var result = _data.assets.judgementShit?.offsets;
if (result == null && fallback != null) result = fallback.getJudgementSpriteOffsets(rating);
return result ?? [0, 0];
default:
return [0, 0];
}
}
public function buildComboNumSprite(digit:Int):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (digit)
{
case 0:
if (_data.assets.comboNumber0 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber0?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber0?.scale ?? 1.0;
case 1:
if (_data.assets.comboNumber1 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber1?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber1?.scale ?? 1.0;
case 2:
if (_data.assets.comboNumber2 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber2?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber2?.scale ?? 1.0;
case 3:
if (_data.assets.comboNumber3 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber3?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber3?.scale ?? 1.0;
case 4:
if (_data.assets.comboNumber4 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber4?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber4?.scale ?? 1.0;
case 5:
if (_data.assets.comboNumber5 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber5?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber5?.scale ?? 1.0;
case 6:
if (_data.assets.comboNumber6 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber6?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber6?.scale ?? 1.0;
case 7:
if (_data.assets.comboNumber7 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber7?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber7?.scale ?? 1.0;
case 8:
if (_data.assets.comboNumber8 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber8?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber8?.scale ?? 1.0;
case 9:
if (_data.assets.comboNumber9 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber9?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber9?.scale ?? 1.0;
default:
return null;
}
var isPixel = isComboNumSpritePixel(digit);
result.antialiasing = !isPixel;
result.pixelPerfectRender = isPixel;
result.pixelPerfectPosition = isPixel;
result.updateHitbox();
return result;
}
public function isComboNumSpritePixel(digit:Int):Bool
{
switch (digit)
{
case 0:
var result = _data.assets.comboNumber0?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 1:
var result = _data.assets.comboNumber1?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 2:
var result = _data.assets.comboNumber2?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 3:
var result = _data.assets.comboNumber3?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 4:
var result = _data.assets.comboNumber4?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 5:
var result = _data.assets.comboNumber5?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 6:
var result = _data.assets.comboNumber6?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 7:
var result = _data.assets.comboNumber7?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 8:
var result = _data.assets.comboNumber8?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
case 9:
var result = _data.assets.comboNumber9?.isPixel;
if (result == null && fallback != null) result = fallback.isComboNumSpritePixel(digit);
return result ?? false;
default:
return false;
}
}
function buildComboNumSpritePath(digit:Int):Null<String>
{
var basePath:Null<String> = null;
switch (digit)
{
case 0:
basePath = _data.assets.comboNumber0?.assetPath;
case 1:
basePath = _data.assets.comboNumber1?.assetPath;
case 2:
basePath = _data.assets.comboNumber2?.assetPath;
case 3:
basePath = _data.assets.comboNumber3?.assetPath;
case 4:
basePath = _data.assets.comboNumber4?.assetPath;
case 5:
basePath = _data.assets.comboNumber5?.assetPath;
case 6:
basePath = _data.assets.comboNumber6?.assetPath;
case 7:
basePath = _data.assets.comboNumber7?.assetPath;
case 8:
basePath = _data.assets.comboNumber8?.assetPath;
case 9:
basePath = _data.assets.comboNumber9?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildComboNumSpritePath(digit);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
public function getComboNumSpriteOffsets(digit:Int):Array<Float>
{
switch (digit)
{
case 0:
var result = _data.assets.comboNumber0?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 1:
var result = _data.assets.comboNumber1?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 2:
var result = _data.assets.comboNumber2?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 3:
var result = _data.assets.comboNumber3?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 4:
var result = _data.assets.comboNumber4?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 5:
var result = _data.assets.comboNumber5?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 6:
var result = _data.assets.comboNumber6?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 7:
var result = _data.assets.comboNumber7?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 8:
var result = _data.assets.comboNumber8?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
case 9:
var result = _data.assets.comboNumber9?.offsets;
if (result == null && fallback != null) result = fallback.getComboNumSpriteOffsets(digit);
return result ?? [0, 0];
default:
return [0, 0];
}
}
public function destroy():Void {}
@ -310,8 +867,17 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return 'NoteStyle($id)';
}
static function _fetchData(id:String):Null<NoteStyleData>
static function _fetchData(id:String):NoteStyleData
{
return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id));
var result = NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id));
if (result == null)
{
throw 'Could not parse note style data for id: $id';
}
else
{
return result;
}
}
}

View file

@ -277,7 +277,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
// If there are no difficulties in the metadata, there's a problem.
if (metadata.playData.difficulties.length == 0)
{
throw 'Song $id has no difficulties listed in metadata!';
trace('[SONG] Warning: Song $id (variation ${metadata.variation}) has no difficulties listed in metadata!');
continue;
}
// There may be more difficulties in the chart file than in the metadata,
@ -494,6 +495,24 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return diffFiltered;
}
public function listSuffixedDifficulties(variationIds:Array<String>, ?showLocked:Bool, ?showHidden:Bool):Array<String>
{
var result = [];
for (variation in variationIds)
{
var difficulties = listDifficulties(variation, null, showLocked, showHidden);
for (difficulty in difficulties)
{
var suffixedDifficulty = (variation != Constants.DEFAULT_VARIATION
&& variation != 'erect') ? '$difficulty-${variation}' : difficulty;
result.push(suffixedDifficulty);
}
}
return result;
}
public function hasDifficulty(diffId:String, ?variationId:String, ?variationIds:Array<String>):Bool
{
if (variationIds == null) variationIds = [];
@ -706,10 +725,11 @@ class SongDifficulty
* Cache the vocals for a given character.
* @param id The character we are about to play.
*/
public inline function cacheVocals():Void
public function cacheVocals():Void
{
for (voice in buildVoiceList())
{
trace('Caching vocal track: $voice');
FlxG.sound.cache(voice);
}
}
@ -721,6 +741,20 @@ class SongDifficulty
* @param id The character we are about to play.
*/
public function buildVoiceList():Array<String>
{
var result:Array<String> = [];
result = result.concat(buildPlayerVoiceList());
result = result.concat(buildOpponentVoiceList());
if (result.length == 0)
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Try to use `Voices.ogg` if no other voices are found.
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
}
return result;
}
public function buildPlayerVoiceList():Array<String>
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
@ -728,62 +762,88 @@ class SongDifficulty
// For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
// Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
var playerId:String = characters.player;
var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix');
while (voicePlayer != null && !Assets.exists(voicePlayer))
if (characters.playerVocals == null)
{
// Remove the last suffix.
// For example, bf-car becomes bf.
playerId = playerId.split('-').slice(0, -1).join('-');
// Try again.
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
if (voicePlayer == null)
{
// Try again without $suffix.
playerId = characters.player;
voicePlayer = Paths.voices(this.song.id, '-${playerId}');
while (voicePlayer != null && !Assets.exists(voicePlayer))
var playerId:String = characters.player;
var playerVoice:String = Paths.voices(this.song.id, '-${playerId}$suffix');
while (playerVoice != null && !Assets.exists(playerVoice))
{
// Remove the last suffix.
// For example, bf-car becomes bf.
playerId = playerId.split('-').slice(0, -1).join('-');
// Try again.
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
if (playerVoice == null)
{
// Try again without $suffix.
playerId = characters.player;
playerVoice = Paths.voices(this.song.id, '-${playerId}');
while (playerVoice != null && !Assets.exists(playerVoice))
{
// Remove the last suffix.
playerId = playerId.split('-').slice(0, -1).join('-');
// Try again.
playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
}
}
var opponentId:String = characters.opponent;
var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
{
// Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again.
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
return playerVoice != null ? [playerVoice] : [];
}
if (voiceOpponent == null)
else
{
// Try again without $suffix.
opponentId = characters.opponent;
voiceOpponent = Paths.voices(this.song.id, '-${opponentId}');
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
// The metadata explicitly defines the list of voices.
var playerIds:Array<String> = characters?.playerVocals ?? [characters.player];
var playerVoices:Array<String> = playerIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
return playerVoices;
}
}
public function buildOpponentVoiceList():Array<String>
{
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Automatically resolve voices by removing suffixes.
// For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
// Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
if (characters.opponentVocals == null)
{
var opponentId:String = characters.opponent;
var opponentVoice:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
while (opponentVoice != null && !Assets.exists(opponentVoice))
{
// Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again.
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
}
if (opponentVoice == null)
{
// Try again without $suffix.
opponentId = characters.opponent;
opponentVoice = Paths.voices(this.song.id, '-${opponentId}');
while (opponentVoice != null && !Assets.exists(opponentVoice))
{
// Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again.
opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
}
}
}
var result:Array<String> = [];
if (voicePlayer != null) result.push(voicePlayer);
if (voiceOpponent != null) result.push(voiceOpponent);
if (voicePlayer == null && voiceOpponent == null)
{
// Try to use `Voices.ogg` if no other voices are found.
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
return opponentVoice != null ? [opponentVoice] : [];
}
else
{
// The metadata explicitly defines the list of voices.
var opponentIds:Array<String> = characters?.opponentVocals ?? [characters.opponent];
var opponentVoices:Array<String> = opponentIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
return opponentVoices;
}
return result;
}
/**
@ -795,26 +855,19 @@ class SongDifficulty
{
var result:VoicesGroup = new VoicesGroup();
var voiceList:Array<String> = buildVoiceList();
if (voiceList.length == 0)
{
trace('Could not find any voices for song ${this.song.id}');
return result;
}
var playerVoiceList:Array<String> = this.buildPlayerVoiceList();
var opponentVoiceList:Array<String> = this.buildOpponentVoiceList();
// Add player vocals.
if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(voiceList[0]));
// Add opponent vocals.
if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(voiceList[1]));
// Add additional vocals.
if (voiceList.length > 2)
for (playerVoice in playerVoiceList)
{
for (i in 2...voiceList.length)
{
result.add(FunkinSound.load(Assets.getSound(voiceList[i])));
}
result.addPlayerVoice(FunkinSound.load(playerVoice));
}
// Add opponent vocals.
for (opponentVoice in opponentVoiceList)
{
result.addOpponentVoice(FunkinSound.load(opponentVoice));
}
result.playerVoicesOffset = offsets.getVocalOffset(characters.player);

View file

@ -1,6 +1,7 @@
package funkin.play.stage;
import flixel.FlxSprite;
import flixel.FlxCamera;
import flixel.math.FlxPoint;
import flixel.util.FlxTimer;
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
@ -45,8 +46,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
public var idleSuffix(default, set):String = '';
/**
* If this bopper is rendered with pixel art,
* disable anti-aliasing and render at 6x scale.
* If this bopper is rendered with pixel art, disable anti-aliasing.
* @default `false`
*/
public var isPixel(default, set):Bool = false;
@ -79,11 +80,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
if (globalOffsets == null) globalOffsets = [0, 0];
if (globalOffsets == value) return value;
var xDiff = globalOffsets[0] - value[0];
var yDiff = globalOffsets[1] - value[1];
this.x += xDiff;
this.y += yDiff;
return globalOffsets = value;
}
@ -97,12 +93,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
if (animOffsets == null) animOffsets = [0, 0];
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
var xDiff = animOffsets[0] - value[0];
var yDiff = animOffsets[1] - value[1];
this.x += xDiff;
this.y += yDiff;
return animOffsets = value;
}
@ -177,10 +167,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
*/
public function onStepHit(event:SongTimeScriptEvent)
{
if (danceEvery > 0) trace('step hit(${danceEvery}): ${event.step % (danceEvery * Constants.STEPS_PER_BEAT)} == 0?');
if (danceEvery > 0 && (event.step % (danceEvery * Constants.STEPS_PER_BEAT)) == 0)
{
trace('dance onStepHit!');
dance(shouldBop);
}
}
@ -322,14 +310,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
function applyAnimationOffsets(name:String):Void
{
var offsets = animationOffsets.get(name);
if (offsets != null && !(offsets[0] == 0 && offsets[1] == 0))
{
this.animOffsets = [offsets[0] + globalOffsets[0], offsets[1] + globalOffsets[1]];
}
else
{
this.animOffsets = globalOffsets;
}
this.animOffsets = offsets;
}
public function isAnimationFinished():Bool
@ -353,6 +334,15 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
return this.animation.curAnim.name;
}
// override getScreenPosition (used by FlxSprite's draw method) to account for animation offsets.
override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
{
var output:FlxPoint = super.getScreenPosition(result, camera);
output.x -= (animOffsets[0] - globalOffsets[0]) * this.scale.x;
output.y -= (animOffsets[1] - globalOffsets[1]) * this.scale.y;
return output;
}
public function onPause(event:PauseScriptEvent) {}
public function onResume(event:ScriptEvent) {}

View file

@ -769,7 +769,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
* A function that gets called once per step in the song.
* @param curStep The current step number.
*/
public function onStepHit(event:SongTimeScriptEvent):Void {}
public function onStepHit(event:SongTimeScriptEvent):Void
{
// Override me in your scripted stage to perform custom behavior!
// Make sure to call super.onStepHit(event) if you want to keep the boppers dancing.
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
/**
* A function that gets called once per beat in the song (once every four steps).
@ -786,7 +795,13 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
}
}
public function onUpdate(event:UpdateScriptEvent) {}
public function onUpdate(event:UpdateScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public override function kill()
{
@ -866,35 +881,131 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));
}
public function onScriptEvent(event:ScriptEvent) {}
public function onScriptEvent(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onPause(event:PauseScriptEvent) {}
public function onPause(event:PauseScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onResume(event:ScriptEvent) {}
public function onResume(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongStart(event:ScriptEvent) {}
public function onSongStart(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongEnd(event:ScriptEvent) {}
public function onSongEnd(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onGameOver(event:ScriptEvent) {}
public function onGameOver(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onCountdownStart(event:CountdownScriptEvent) {}
public function onCountdownStart(event:CountdownScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onCountdownStep(event:CountdownScriptEvent) {}
public function onCountdownStep(event:CountdownScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onCountdownEnd(event:CountdownScriptEvent) {}
public function onCountdownEnd(event:CountdownScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onNoteIncoming(event:NoteScriptEvent) {}
public function onNoteIncoming(event:NoteScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteHit(event:HitNoteScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onNoteMiss(event:NoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongEvent(event:SongEventScriptEvent) {}
public function onSongEvent(event:SongEventScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongLoaded(event:SongLoadScriptEvent) {}
public function onSongLoaded(event:SongLoadScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
public function onSongRetry(event:ScriptEvent) {}
public function onSongRetry(event:ScriptEvent)
{
for (bopper in boppers)
{
ScriptEventDispatcher.callEvent(bopper, event);
}
}
}

View file

@ -152,6 +152,32 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
}
}
public function getWidth():Int
{
var width = 0;
for (char in this.text.split(""))
{
switch (char)
{
case " ":
{
width += 40;
}
case "\n":
{}
case char:
{
var sprite = new AtlasChar(atlas, char);
sprite.revive();
sprite.char = char;
sprite.alpha = 1;
width += Std.int(sprite.width);
}
}
}
return width;
}
override function toString()
{
return "InputItem, " + FlxStringUtil.getDebugString([

View file

@ -78,9 +78,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
{
// Emergency exit button.
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5) debug_refreshModules();
}
override function update(elapsed:Float)
@ -114,12 +111,10 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
ModuleHandler.callEvent(event);
}
function debug_refreshModules()
function reloadAssets()
{
PolymodHandler.forceReloadAssets();
this.destroy();
// Create a new instance of the current state, so old data is cleared.
FlxG.resetState();
}

View file

@ -72,9 +72,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
// Emergency exit button.
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
// This can now be used in EVERY STATE YAY!
if (FlxG.keys.justPressed.F5) debug_refreshModules();
// Display Conductor info in the watch window.
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
Conductor.watchQuick(conductorInUse);
@ -82,7 +79,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
dispatchEvent(new UpdateScriptEvent(elapsed));
}
function debug_refreshModules()
function reloadAssets()
{
PolymodHandler.forceReloadAssets();

View file

@ -36,7 +36,7 @@ class MemoryCounter extends TextField
@:noCompletion
#if !flash override #end function __enterFrame(deltaTime:Float):Void
{
var mem:Float = Math.round(MemoryUtil.getMemoryUsed() / BYTES_PER_MEG / ROUND_TO) * ROUND_TO;
var mem:Float = Math.fround(MemoryUtil.getMemoryUsed() / BYTES_PER_MEG / ROUND_TO) * ROUND_TO;
if (mem > memPeak) memPeak = mem;

View file

@ -35,6 +35,7 @@ import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongData.NoteParamData;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageData;
@ -45,6 +46,7 @@ import funkin.input.TurboActionHandler;
import funkin.input.TurboButtonHandler;
import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
@ -282,6 +284,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0;
/**
* A map of the keys for every live input style.
*/
public static final LIVE_INPUT_KEYS:Map<ChartEditorLiveInputStyle, Array<FlxKey>> = [
NumberKeys => [
FIVE, SIX, SEVEN, EIGHT,
ONE, TWO, THREE, FOUR
],
WASDKeys => [
LEFT, DOWN, UP, RIGHT,
A, S, W, D
],
None => []
];
/**
* INSTANCE DATA
*/
@ -538,6 +555,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var noteKindToPlace:Null<String> = null;
/**
* The note params to use for notes being placed in the chart. Defaults to `[]`.
*/
var noteParamsToPlace:Array<NoteParamData> = [];
/**
* The event type to use for events being placed in the chart. Defaults to `''`.
*/
@ -1401,7 +1423,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_currentSongNoteStyle():String
{
if (currentSongMetadata.playData.noteStyle == null)
if (currentSongMetadata.playData.noteStyle == null
|| currentSongMetadata.playData.noteStyle == ''
|| currentSongMetadata.playData.noteStyle == 'item')
{
// Initialize to the default value if not set.
currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE;
@ -2436,7 +2460,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
gridGhostNote = new ChartEditorNoteSprite(this);
gridGhostNote.alpha = 0.6;
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "");
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "", []);
gridGhostNote.visible = false;
add(gridGhostNote);
gridGhostNote.zIndex = 11;
@ -3584,6 +3608,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// The note sprite handles animation playback and positioning.
noteSprite.noteData = noteData;
noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
noteSprite.overrideStepTime = null;
noteSprite.overrideData = null;
@ -3607,6 +3632,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
holdNoteSprite.setHeightDirectly(noteLengthPixels);
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height);
@ -3669,9 +3696,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
holdNoteSprite.noteData = noteData;
holdNoteSprite.noteDirection = noteData.getDirection();
holdNoteSprite.setHeightDirectly(noteLengthPixels);
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
displayedHoldNoteData.push(noteData);
@ -4569,7 +4597,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
gridGhostHoldNote.noteData = currentPlaceNoteData;
gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
}
else
@ -4726,7 +4754,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
else
{
// Create a note and place it in the chart.
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace);
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace,
ChartEditorState.cloneNoteParams(noteParamsToPlace));
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
@ -4885,12 +4914,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace);
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace,
ChartEditorState.cloneNoteParams(noteParamsToPlace));
if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind)
if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind || noteParamsToPlace != noteData.params)
{
noteData.kind = noteKindToPlace;
noteData.params = noteParamsToPlace;
noteData.data = cursorColumn;
gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
gridGhostNote.playNoteAnimation();
}
noteData.time = cursorSnappedMs;
@ -5129,46 +5161,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handlePlayhead():Void
{
// Place notes at the playhead with the keyboard.
switch (currentLiveInputStyle)
for (note => key in LIVE_INPUT_KEYS[currentLiveInputStyle])
{
case ChartEditorLiveInputStyle.WASDKeys:
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.NumberKeys:
// Flipped because Dad is on the left but represents data 0-3.
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
case ChartEditorLiveInputStyle.None:
// Do nothing.
if (FlxG.keys.checkStatus(key, JUST_PRESSED)) placeNoteAtPlayhead(note)
else if (FlxG.keys.checkStatus(key, JUST_RELEASED)) finishPlaceNoteAtPlayhead(note);
}
// Place events at playhead.
@ -5196,7 +5192,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (notesAtPos.length == 0 && !removeNoteInstead)
{
trace('Placing note. ${column}');
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace, ChartEditorState.cloneNoteParams(noteParamsToPlace));
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
currentLiveInputPlaceNoteData[column] = newNoteData;
}
@ -5282,6 +5278,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
ghostHold.visible = true;
ghostHold.alpha = 0.6;
ghostHold.setHeightDirectly(0);
ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
ghostHold.updateHoldNotePosition(renderedHoldNotes);
}
@ -5648,6 +5645,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace);
FlxG.watch.addQuick('noteParamsToPlace', noteParamsToPlace);
FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace);
FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
@ -5703,11 +5701,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
case 'mainStage':
PlayStatePlaylist.campaignId = 'week1';
case 'spookyMansion':
case 'spookyMansion' | 'spookyMansionErect':
PlayStatePlaylist.campaignId = 'week2';
case 'phillyTrain':
case 'phillyTrain' | 'phillyTrainErect':
PlayStatePlaylist.campaignId = 'week3';
case 'limoRide':
case 'limoRide' | 'limoRideErect':
PlayStatePlaylist.campaignId = 'week4';
case 'mallXmas' | 'mallEvil':
PlayStatePlaylist.campaignId = 'week5';
@ -6511,6 +6509,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
return input;
}
public static function cloneNoteParams(paramsToClone:Array<NoteParamData>):Array<NoteParamData>
{
var params:Array<NoteParamData> = [];
for (param in paramsToClone)
{
params.push(param.clone());
}
return params;
}
}
/**

View file

@ -2,6 +2,7 @@ package funkin.ui.debug.charting.components;
import funkin.play.notes.Strumline;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection;
@ -15,6 +16,7 @@ import flixel.math.FlxMath;
* A sprite that can be used to display the trail of a hold note in a chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
@:access(funkin.ui.debug.charting.ChartEditorState)
@:nullSafety
class ChartEditorHoldNoteSprite extends SustainTrail
{
@ -23,6 +25,22 @@ class ChartEditorHoldNoteSprite extends SustainTrail
*/
public var parentState:ChartEditorState;
@:isVar
public var noteStyle(get, set):Null<String>;
function get_noteStyle():Null<String>
{
return this.noteStyle ?? this.parentState.currentSongNoteStyle;
}
@:nullSafety(Off)
function set_noteStyle(value:Null<String>):Null<String>
{
this.noteStyle = value;
this.updateHoldNoteGraphic();
return value;
}
public function new(parent:ChartEditorState)
{
var noteStyle = NoteStyleRegistry.instance.fetchDefault();
@ -30,14 +48,52 @@ class ChartEditorHoldNoteSprite extends SustainTrail
super(0, 100, noteStyle);
this.parentState = parent;
}
@:nullSafety(Off)
function updateHoldNoteGraphic():Void
{
var bruhStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyle);
if (bruhStyle == null) bruhStyle = NoteStyleRegistry.instance.fetchDefault();
setupHoldNoteGraphic(bruhStyle);
}
override function setupHoldNoteGraphic(noteStyle:NoteStyle):Void
{
var graphicPath = noteStyle.getHoldNoteAssetPath();
if (graphicPath == null) return;
loadGraphic(graphicPath);
antialiasing = true;
this.isPixel = noteStyle.isHoldNotePixel();
if (isPixel)
{
endOffset = bottomClip = 1;
antialiasing = false;
}
else
{
endOffset = 0.5;
bottomClip = 0.9;
}
zoom = 1.0;
zoom *= noteStyle.fetchHoldNoteScale();
zoom *= 0.7;
zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
graphicHeight = sustainLength * 0.45; // sustainHeight
flipY = false;
alpha = 1.0;
updateColorTransform();
updateClipping();
setup();
}

View file

@ -7,7 +7,11 @@ import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFrame;
import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint;
import funkin.data.animation.AnimationData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.NoteDirection;
/**
* A sprite that can be used to display a note in a chart.
@ -36,7 +40,8 @@ class ChartEditorNoteSprite extends FlxSprite
/**
* The name of the note style currently in use.
*/
public var noteStyle(get, never):String;
@:isVar
public var noteStyle(get, set):Null<String>;
public var overrideStepTime(default, set):Null<Float> = null;
@ -66,72 +71,80 @@ class ChartEditorNoteSprite extends FlxSprite
this.parentState = parent;
var entries:Array<String> = NoteStyleRegistry.instance.listEntryIds();
if (noteFrameCollection == null)
{
initFrameCollection();
buildEmptyFrameCollection();
for (entry in entries)
{
addNoteStyleFrames(fetchNoteStyle(entry));
}
}
if (noteFrameCollection == null) throw 'ERROR: Could not initialize note sprite animations.';
this.frames = noteFrameCollection;
// Initialize all the animations, not just the one we're going to use immediately,
// so that later we can reuse the sprite without having to initialize more animations during scrolling.
this.animation.addByPrefix('tapLeftFunkin', 'purple instance');
this.animation.addByPrefix('tapDownFunkin', 'blue instance');
this.animation.addByPrefix('tapUpFunkin', 'green instance');
this.animation.addByPrefix('tapRightFunkin', 'red instance');
this.animation.addByPrefix('holdLeftFunkin', 'LeftHoldPiece');
this.animation.addByPrefix('holdDownFunkin', 'DownHoldPiece');
this.animation.addByPrefix('holdUpFunkin', 'UpHoldPiece');
this.animation.addByPrefix('holdRightFunkin', 'RightHoldPiece');
this.animation.addByPrefix('holdEndLeftFunkin', 'LeftHoldEnd');
this.animation.addByPrefix('holdEndDownFunkin', 'DownHoldEnd');
this.animation.addByPrefix('holdEndUpFunkin', 'UpHoldEnd');
this.animation.addByPrefix('holdEndRightFunkin', 'RightHoldEnd');
this.animation.addByPrefix('tapLeftPixel', 'pixel4');
this.animation.addByPrefix('tapDownPixel', 'pixel5');
this.animation.addByPrefix('tapUpPixel', 'pixel6');
this.animation.addByPrefix('tapRightPixel', 'pixel7');
for (entry in entries)
{
addNoteStyleAnimations(fetchNoteStyle(entry));
}
}
static var noteFrameCollection:Null<FlxFramesCollection> = null;
/**
* We load all the note frames once, then reuse them.
*/
static function initFrameCollection():Void
function fetchNoteStyle(noteStyleId:String):NoteStyle
{
buildEmptyFrameCollection();
if (noteFrameCollection == null) return;
var result = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (result != null) return result;
return NoteStyleRegistry.instance.fetchDefault();
}
// TODO: Automatically iterate over the list of note skins.
@:access(funkin.play.notes.notestyle.NoteStyle)
@:nullSafety(Off)
static function addNoteStyleFrames(noteStyle:NoteStyle):Void
{
var prefix:String = noteStyle.id.toTitleCase();
// Normal notes
var frameCollectionNormal:FlxAtlasFrames = Paths.getSparrowAtlas('NOTE_assets');
for (frame in frameCollectionNormal.frames)
var frameCollection:FlxAtlasFrames = Paths.getSparrowAtlas(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary());
if (frameCollection == null)
{
noteFrameCollection.pushFrame(frame);
trace('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}');
FlxG.log.error('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}');
return;
}
// Pixel notes
var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null);
if (graphicPixel == null) trace('ERROR: Could not load graphic: ' + Paths.image('weeb/pixelUI/arrows-pixels', 'week6'));
var frameCollectionPixel = FlxTileFrames.fromGraphic(graphicPixel, new FlxPoint(17, 17));
for (i in 0...frameCollectionPixel.frames.length)
for (frame in frameCollection.frames)
{
var frame:Null<FlxFrame> = frameCollectionPixel.frames[i];
if (frame == null) continue;
frame.name = 'pixel' + i;
noteFrameCollection.pushFrame(frame);
// cloning the frame because else
// we will fuck up the frame data used in game
var clonedFrame:FlxFrame = frame.copyTo();
clonedFrame.name = '$prefix${clonedFrame.name}';
noteFrameCollection.pushFrame(clonedFrame);
}
}
@:access(funkin.play.notes.notestyle.NoteStyle)
@:nullSafety(Off)
function addNoteStyleAnimations(noteStyle:NoteStyle):Void
{
var prefix:String = noteStyle.id.toTitleCase();
var suffix:String = noteStyle.id.toTitleCase();
var leftData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.LEFT);
this.animation.addByPrefix('tapLeft$suffix', '$prefix${leftData.prefix}', leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY);
var downData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.DOWN);
this.animation.addByPrefix('tapDown$suffix', '$prefix${downData.prefix}', downData.frameRate, downData.looped, downData.flipX, downData.flipY);
var upData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.UP);
this.animation.addByPrefix('tapUp$suffix', '$prefix${upData.prefix}', upData.frameRate, upData.looped, upData.flipX, upData.flipY);
var rightData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.RIGHT);
this.animation.addByPrefix('tapRight$suffix', '$prefix${rightData.prefix}', rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
}
@:nullSafety(Off)
static function buildEmptyFrameCollection():Void
{
@ -185,12 +198,24 @@ class ChartEditorNoteSprite extends FlxSprite
}
}
function get_noteStyle():String
function get_noteStyle():Null<String>
{
// Fall back to Funkin' if it's not a valid note style.
return if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle)) this.parentState.currentSongNoteStyle else 'funkin';
if (this.noteStyle == null)
{
var result = this.parentState.currentSongNoteStyle;
return result;
}
return this.noteStyle;
}
function set_noteStyle(value:Null<String>):Null<String>
{
this.noteStyle = value;
this.playNoteAnimation();
return value;
}
@:nullSafety(Off)
public function playNoteAnimation():Void
{
if (this.noteData == null) return;
@ -200,6 +225,7 @@ class ChartEditorNoteSprite extends FlxSprite
// Play the appropriate animation for the type, direction, and skin.
var dirName:String = overrideData != null ? SongNoteData.buildDirectionName(overrideData) : this.noteData.getDirectionName();
var noteStyleSuffix:String = this.noteStyle?.toTitleCase() ?? Constants.DEFAULT_NOTE_STYLE.toTitleCase();
var animationName:String = '${baseAnimationName}${dirName}${this.noteStyle.toTitleCase()}';
this.animation.play(animationName);
@ -209,12 +235,12 @@ class ChartEditorNoteSprite extends FlxSprite
switch (baseAnimationName)
{
case 'tap':
this.setGraphicSize(0, ChartEditorState.GRID_SIZE);
this.setGraphicSize(ChartEditorState.GRID_SIZE, 0);
this.updateHitbox();
}
this.updateHitbox();
// TODO: Make this an attribute of the note skin.
this.antialiasing = (this.parentState.currentSongNoteStyle != 'Pixel');
var bruhStyle:NoteStyle = fetchNoteStyle(this.noteStyle);
this.antialiasing = !bruhStyle._data?.assets?.note?.isPixel ?? true;
}
/**

View file

@ -2,8 +2,16 @@ package funkin.ui.debug.charting.toolboxes;
import haxe.ui.components.DropDown;
import haxe.ui.components.TextField;
import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
import haxe.ui.containers.Grid;
import haxe.ui.core.Component;
import haxe.ui.events.UIEvent;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.notes.notekind.NoteKind.NoteKindParam;
import funkin.play.notes.notekind.NoteKind.NoteKindParamType;
import funkin.data.song.SongData.NoteParamData;
/**
* The toolbox which allows modifying information like Note Kind.
@ -12,8 +20,22 @@ import funkin.ui.debug.charting.util.ChartEditorDropdowns;
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml"))
class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
{
// 100 is the height used in note-data.xml
static final DIALOG_HEIGHT:Int = 100;
// toolboxNotesGrid.height + 45
// this is what i found out by printing this.height and grid.height
// and then seeing that this.height is 100 and grid.height is 55
static final HEIGHT_OFFSET:Int = 45;
// minimizing creates a gray bar the bottom, which would obscure the components,
// which is why we use an extra offset of 20
static final MINIMIZE_FIX:Int = 20;
var toolboxNotesGrid:Grid;
var toolboxNotesNoteKind:DropDown;
var toolboxNotesCustomKind:TextField;
var toolboxNotesParams:Array<ToolboxNoteKindParam> = [];
var _initializing:Bool = true;
@ -54,12 +76,35 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
}
createNoteKindParams(noteKind);
if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
{
// Edit the note data of any selected notes.
for (note in chartEditorState.currentNoteSelection)
{
// Edit the note data of any selected notes.
note.kind = chartEditorState.noteKindToPlace;
note.params = ChartEditorState.cloneNoteParams(chartEditorState.noteParamsToPlace);
// update note sprites
for (noteSprite in chartEditorState.renderedNotes.members)
{
if (noteSprite.noteData == note)
{
noteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle;
break;
}
}
// update hold note sprites
for (holdNoteSprite in chartEditorState.renderedHoldNotes.members)
{
if (holdNoteSprite.noteData == note)
{
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle;
break;
}
}
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
@ -94,6 +139,8 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
toolboxNotesNoteKind.value = ChartEditorDropdowns.lookupNoteKind(chartEditorState.noteKindToPlace);
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
createNoteKindParams(chartEditorState.noteKindToPlace);
}
function showCustom():Void
@ -108,8 +155,149 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
toolboxNotesCustomKind.hidden = true;
}
function createNoteKindParams(noteKind:Null<String>):Void
{
clearNoteKindParams();
var setParamsToPlace:Bool = false;
if (!_initializing)
{
for (note in chartEditorState.currentNoteSelection)
{
if (note.kind == chartEditorState.noteKindToPlace)
{
chartEditorState.noteParamsToPlace = ChartEditorState.cloneNoteParams(note.params);
setParamsToPlace = true;
break;
}
}
}
var noteKindParams:Array<NoteKindParam> = NoteKindManager.getParams(noteKind);
for (i in 0...noteKindParams.length)
{
var param:NoteKindParam = noteKindParams[i];
var paramLabel:Label = new Label();
paramLabel.value = param.description;
paramLabel.verticalAlign = "center";
paramLabel.horizontalAlign = "right";
var paramComponent:Component = null;
switch (param.type)
{
case NoteKindParamType.INT | NoteKindParamType.FLOAT:
var paramStepper:NumberStepper = new NumberStepper();
paramStepper.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? 0.0;
paramStepper.percentWidth = 100;
paramStepper.step = param.data?.step ?? 1.0;
// this check should be unnecessary but for some reason
// even when these are null it will set it to 0
if (param.data?.min != null)
{
paramStepper.min = param.data.min;
}
if (param.data?.max != null)
{
paramStepper.max = param.data.max;
}
if (param.data?.precision != null)
{
paramStepper.precision = param.data.precision;
}
paramComponent = paramStepper;
case NoteKindParamType.STRING:
var paramTextField:TextField = new TextField();
paramTextField.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? '';
paramTextField.percentWidth = 100;
paramComponent = paramTextField;
}
if (paramComponent == null)
{
continue;
}
paramComponent.onChange = function(event:UIEvent) {
chartEditorState.noteParamsToPlace[i].value = paramComponent.value;
for (note in chartEditorState.currentNoteSelection)
{
if (note.params.length != noteKindParams.length)
{
break;
}
if (note.params[i].name == param.name)
{
note.params[i].value = paramComponent.value;
}
}
}
addNoteKindParam(paramLabel, paramComponent);
}
if (!setParamsToPlace)
{
var noteParamData:Array<NoteParamData> = [];
for (i in 0...noteKindParams.length)
{
noteParamData.push(new NoteParamData(noteKindParams[i].name, toolboxNotesParams[i].component.value));
}
chartEditorState.noteParamsToPlace = noteParamData;
}
}
function addNoteKindParam(label:Label, component:Component):Void
{
toolboxNotesParams.push({label: label, component: component});
toolboxNotesGrid.addComponent(label);
toolboxNotesGrid.addComponent(component);
this.height = Math.max(DIALOG_HEIGHT, DIALOG_HEIGHT - 30 + toolboxNotesParams.length * 30);
}
function clearNoteKindParams():Void
{
for (param in toolboxNotesParams)
{
toolboxNotesGrid.removeComponent(param.component);
toolboxNotesGrid.removeComponent(param.label);
}
toolboxNotesParams = [];
this.height = DIALOG_HEIGHT;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// current dialog is minimized, dont change the height
if (this.minimized)
{
return;
}
var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, (toolboxNotesGrid?.height ?? 50.0) + HEIGHT_OFFSET)) + MINIMIZE_FIX;
if (this.height != heightToSet)
{
this.height = heightToSet;
}
}
public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox
{
return new ChartEditorNoteDataToolbox(chartEditorState);
}
}
typedef ToolboxNoteKindParam =
{
var label:Label;
var component:Component;
}

View file

@ -135,6 +135,14 @@ class ChartEditorDropdowns
var noteStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) continue;
// check if the note style has all necessary assets (strums, notes, holdNotes)
if (noteStyle._data?.assets?.noteStrumline == null
|| noteStyle._data?.assets?.note == null
|| noteStyle._data?.assets?.holdNote == null)
{
continue;
}
var value = {id: noteStyleId, text: noteStyle.getName()};
if (startingStyleId == noteStyleId) returnValue = value;
@ -146,7 +154,7 @@ class ChartEditorDropdowns
return returnValue;
}
static final NOTE_KINDS:Map<String, String> = [
public static final NOTE_KINDS:Map<String, String> = [
// Base
"" => "Default",
"~CUSTOM~" => "Custom",
@ -187,11 +195,11 @@ class ChartEditorDropdowns
{
dropDown.dataSource.clear();
var returnValue:DropDownEntry = lookupNoteKind('~CUSTOM');
var returnValue:DropDownEntry = lookupNoteKind('');
for (noteKindId in NOTE_KINDS.keys())
{
var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Default';
var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Unknown';
var value:DropDownEntry = {id: noteKindId, text: noteKind};
if (startingKindId == noteKindId) returnValue = value;
@ -208,7 +216,7 @@ class ChartEditorDropdowns
{
if (noteKindId == null) return lookupNoteKind('');
if (!NOTE_KINDS.exists(noteKindId)) return {id: '~CUSTOM~', text: 'Custom'};
return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Default'};
return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Unknown'};
}
/**

View file

@ -339,7 +339,7 @@ class FreeplayState extends MusicBeatSubState
// Only display songs which actually have available difficulties for the current character.
var displayedVariations = song.getVariationsByCharacter(currentCharacter);
trace('Displayed Variations (${songId}): $displayedVariations');
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
var availableDifficultiesForSong:Array<String> = song.listSuffixedDifficulties(displayedVariations, false, false);
trace('Available Difficulties: $availableDifficultiesForSong');
if (availableDifficultiesForSong.length == 0) continue;
@ -1120,7 +1120,7 @@ class FreeplayState extends MusicBeatSubState
// NOW we can interact with the menu
busy = false;
grpCapsules.members[curSelected].sparkle.alpha = 0.7;
capsule.sparkle.alpha = 0.7;
playCurSongPreview(capsule);
}, null);
@ -1527,7 +1527,7 @@ class FreeplayState extends MusicBeatSubState
var moveDataX = funnyMoveShit.x ?? spr.x;
var moveDataY = funnyMoveShit.y ?? spr.y;
var moveDataSpeed = funnyMoveShit.speed ?? 0.2;
var moveDataWait = funnyMoveShit.wait ?? 0;
var moveDataWait = funnyMoveShit.wait ?? 0.0;
FlxTween.tween(spr, {x: moveDataX, y: moveDataY}, moveDataSpeed, {ease: FlxEase.expoIn});
@ -1674,6 +1674,9 @@ class FreeplayState extends MusicBeatSubState
songCapsule.init(null, null, null);
}
}
// Reset the song preview in case we changed variations (normal->erect etc)
playCurSongPreview();
}
// Set the album graphic and play the animation if relevant.
@ -1912,8 +1915,10 @@ class FreeplayState extends MusicBeatSubState
}
}
public function playCurSongPreview(daSongCapsule:SongMenuItem):Void
public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void
{
if (daSongCapsule == null) daSongCapsule = grpCapsules.members[curSelected];
if (curSelected == 0)
{
FunkinSound.playMusic('freeplayRandom',
@ -2145,7 +2150,7 @@ class FreeplaySongData
function updateValues(variations:Array<String>):Void
{
this.songDifficulties = song.listDifficulties(null, variations, false, false);
this.songDifficulties = song.listSuffixedDifficulties(variations, false, false);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations);
@ -2207,15 +2212,26 @@ class DifficultySprite extends FlxSprite
difficultyId = diffId;
if (Assets.exists(Paths.file('images/freeplay/freeplay${diffId}.xml')))
var assetDiffId:String = diffId;
while (!Assets.exists(Paths.image('freeplay/freeplay${assetDiffId}')))
{
this.frames = Paths.getSparrowAtlas('freeplay/freeplay${diffId}');
// Remove the last suffix of the difficulty id until we find an asset or there are no more suffixes.
var assetDiffIdParts:Array<String> = assetDiffId.split('-');
assetDiffIdParts.pop();
if (assetDiffIdParts.length == 0) break;
assetDiffId = assetDiffIdParts.join('-');
}
// Check for an XML to use an animation instead of an image.
if (Assets.exists(Paths.file('images/freeplay/freeplay${assetDiffId}.xml')))
{
this.frames = Paths.getSparrowAtlas('freeplay/freeplay${assetDiffId}');
this.animation.addByPrefix('idle', 'idle0', 24, true);
if (Preferences.flashingLights) this.animation.play('idle');
}
else
{
this.loadGraphic(Paths.image('freeplay/freeplay' + diffId));
this.loadGraphic(Paths.image('freeplay/freeplay' + assetDiffId));
}
}
}

View file

@ -412,8 +412,8 @@ class MainMenuState extends MusicBeatState
if (controls.BACK && menuItems.enabled && !menuItems.busy)
{
FunkinSound.playOnce(Paths.sound('cancelMenu'));
FlxG.switchState(() -> new TitleState());
FunkinSound.playOnce(Paths.sound('cancelMenu'));
}
}
}

View file

@ -0,0 +1,10 @@
package funkin.ui.options;
// Add enums for use with `EnumPreferenceItem` here!
/* Example:
class MyOptionEnum
{
public static inline var YuhUh = "true"; // "true" is the value's ID
public static inline var NuhUh = "false";
}
*/

View file

@ -145,8 +145,8 @@ class Page extends FlxGroup
{
if (canExit && controls.BACK)
{
FunkinSound.playOnce(Paths.sound('cancelMenu'));
exit();
FunkinSound.playOnce(Paths.sound('cancelMenu'));
}
}

View file

@ -8,6 +8,11 @@ import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.options.OptionsState.Page;
import funkin.graphics.FunkinCamera;
import funkin.ui.TextMenuList.TextMenuItem;
import funkin.audio.FunkinSound;
import funkin.ui.options.MenuItemEnums;
import funkin.ui.options.items.CheckboxPreferenceItem;
import funkin.ui.options.items.NumberPreferenceItem;
import funkin.ui.options.items.EnumPreferenceItem;
class PreferencesMenu extends Page
{
@ -69,11 +74,51 @@ class PreferencesMenu extends Page
}, Preferences.autoPause);
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// Indent the selected item.
items.forEach(function(daItem:TextMenuItem) {
var thyOffset:Int = 0;
// Initializing thy text width (if thou text present)
var thyTextWidth:Int = 0;
if (Std.isOfType(daItem, EnumPreferenceItem)) thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth();
else if (Std.isOfType(daItem, NumberPreferenceItem)) thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth();
if (thyTextWidth != 0)
{
// Magic number because of the weird offset thats being added by default
thyOffset += thyTextWidth - 75;
}
if (items.selectedItem == daItem)
{
thyOffset += 150;
}
else
{
thyOffset += 120;
}
daItem.x = thyOffset;
});
}
// - Preference item creation methods -
// Should be moved into a separate PreferenceItems class but you can't access PreferencesMenu.items and PreferencesMenu.preferenceItems from outside.
/**
* Creates a pref item that works with booleans
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
*/
function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
{
var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
var value = !checkbox.currentValue;
onChange(value);
checkbox.currentValue = value;
@ -82,62 +127,54 @@ class PreferencesMenu extends Page
preferenceItems.add(checkbox);
}
override function update(elapsed:Float)
/**
* Creates a pref item that works with general numbers
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed value looks
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
* @param min Minimum value (example: 0)
* @param max Maximum value (example: 10)
* @param step The value to increment/decrement by (default = 0.1)
* @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12)
*/
function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Int, min:Int, max:Int,
step:Float = 0.1, precision:Int):Void
{
super.update(elapsed);
var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter);
items.addItem(prefName, item);
preferenceItems.add(item.lefthandText);
}
// Indent the selected item.
// TODO: Only do this on menu change?
items.forEach(function(daItem:TextMenuItem) {
if (items.selectedItem == daItem) daItem.x = 150;
else
daItem.x = 120;
});
}
}
class CheckboxPreferenceItem extends FlxSprite
{
public var currentValue(default, set):Bool;
public function new(x:Float, y:Float, defaultValue:Bool = false)
{
super(x, y);
frames = Paths.getSparrowAtlas('checkboxThingie');
animation.addByPrefix('static', 'Check Box unselected', 24, false);
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
this.currentValue = defaultValue;
}
override function update(elapsed:Float)
{
super.update(elapsed);
switch (animation.curAnim.name)
{
case 'static':
offset.set();
case 'checked':
offset.set(17, 70);
}
}
function set_currentValue(value:Bool):Bool
{
if (value)
{
animation.play('checked', true);
}
else
{
animation.play('static');
}
return currentValue = value;
/**
* Creates a pref item that works with number percentages
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
* @param min Minimum value (default = 0)
* @param max Maximum value (default = 100)
*/
function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void
{
var newCallback = function(value:Float) {
onChange(Std.int(value));
};
var formatter = function(value:Float) {
return '${value}%';
};
var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter);
items.addItem(prefName, item);
preferenceItems.add(item.lefthandText);
}
/**
* Creates a pref item that works with enums
* @param values Maps enum values to display strings _(ex: `NoteHitSoundType.PingPong => "Ping pong"`)_
* @param onChange Gets called every time the player changes the value; use this to apply the value
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
*/
function createPrefItemEnum(prefName:String, prefDesc:String, values:Map<String, String>, onChange:String->Void, defaultValue:String):Void
{
var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultValue, onChange);
items.addItem(prefName, item);
preferenceItems.add(item.lefthandText);
}
}

View file

@ -0,0 +1,49 @@
package funkin.ui.options.items;
import flixel.FlxSprite.FlxSprite;
class CheckboxPreferenceItem extends FlxSprite
{
public var currentValue(default, set):Bool;
public function new(x:Float, y:Float, defaultValue:Bool = false)
{
super(x, y);
frames = Paths.getSparrowAtlas('checkboxThingie');
animation.addByPrefix('static', 'Check Box unselected', 24, false);
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
this.currentValue = defaultValue;
}
override function update(elapsed:Float)
{
super.update(elapsed);
switch (animation.curAnim.name)
{
case 'static':
offset.set();
case 'checked':
offset.set(17, 70);
}
}
function set_currentValue(value:Bool):Bool
{
if (value)
{
animation.play('checked', true);
}
else
{
animation.play('static');
}
return currentValue = value;
}
}

View file

@ -0,0 +1,84 @@
package funkin.ui.options.items;
import funkin.ui.TextMenuList;
import funkin.ui.AtlasText;
import funkin.input.Controls;
import funkin.ui.options.MenuItemEnums;
import haxe.EnumTools;
/**
* Preference item that allows the player to pick a value from an enum (list of values)
*/
class EnumPreferenceItem extends TextMenuItem
{
function controls():Controls
{
return PlayerSettings.player1.controls;
}
public var lefthandText:AtlasText;
public var currentValue:String;
public var onChangeCallback:Null<String->Void>;
public var map:Map<String, String>;
public var keys:Array<String> = [];
var index = 0;
public function new(x:Float, y:Float, name:String, map:Map<String, String>, defaultValue:String, ?callback:String->Void)
{
super(x, y, name, function() {
callback(this.currentValue);
});
updateHitbox();
this.map = map;
this.currentValue = defaultValue;
this.onChangeCallback = callback;
var i:Int = 0;
for (key in map.keys())
{
this.keys.push(key);
if (this.currentValue == key) index = i;
i += 1;
}
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// var fancyTextFancyColor:Color;
if (selected)
{
var shouldDecrease:Bool = controls().UI_LEFT_P;
var shouldIncrease:Bool = controls().UI_RIGHT_P;
if (shouldDecrease) index -= 1;
if (shouldIncrease) index += 1;
if (index > keys.length - 1) index = 0;
if (index < 0) index = keys.length - 1;
currentValue = keys[index];
if (onChangeCallback != null && (shouldIncrease || shouldDecrease))
{
onChangeCallback(currentValue);
}
}
lefthandText.text = formatted(currentValue);
}
function formatted(value:String):String
{
// FIXME: Can't add arrows around the text because the font doesn't support < >
// var leftArrow:String = selected ? '<' : '';
// var rightArrow:String = selected ? '>' : '';
return '${map.get(value) ?? value}';
}
}

View file

@ -0,0 +1,136 @@
package funkin.ui.options.items;
import funkin.ui.TextMenuList;
import funkin.ui.AtlasText;
import funkin.input.Controls;
/**
* Preference item that allows the player to pick a value between min and max
*/
class NumberPreferenceItem extends TextMenuItem
{
function controls():Controls
{
return PlayerSettings.player1.controls;
}
// Widgets
public var lefthandText:AtlasText;
// Constants
static final HOLD_DELAY:Float = 0.3; // seconds
static final CHANGE_RATE:Float = 0.08; // seconds
// Constructor-initialized variables
public var currentValue:Float;
public var min:Float;
public var max:Float;
public var step:Float;
public var precision:Int;
public var onChangeCallback:Null<Float->Void>;
public var valueFormatter:Null<Float->String>;
// Variables
var holdDelayTimer:Float = HOLD_DELAY; // seconds
var changeRateTimer:Float = 0.0; // seconds
/**
* @param min Minimum value (example: 0)
* @param max Maximum value (example: 100)
* @param step The value to increment/decrement by (example: 10)
* @param callback Will get called every time the user changes the setting; use this to apply/save the setting.
* @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed string looks
*/
public function new(x:Float, y:Float, name:String, defaultValue:Float, min:Float, max:Float, step:Float, precision:Int, ?callback:Float->Void,
?valueFormatter:Float->String):Void
{
super(x, y, name, function() {
callback(this.currentValue);
});
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
updateHitbox();
this.currentValue = defaultValue;
this.min = min;
this.max = max;
this.step = step;
this.precision = precision;
this.onChangeCallback = callback;
this.valueFormatter = valueFormatter;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// var fancyTextFancyColor:Color;
if (selected)
{
holdDelayTimer -= elapsed;
if (holdDelayTimer <= 0.0)
{
changeRateTimer -= elapsed;
}
var jpLeft:Bool = controls().UI_LEFT_P;
var jpRight:Bool = controls().UI_RIGHT_P;
if (jpLeft || jpRight)
{
holdDelayTimer = HOLD_DELAY;
changeRateTimer = 0.0;
}
var shouldDecrease:Bool = jpLeft;
var shouldIncrease:Bool = jpRight;
if (controls().UI_LEFT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
{
shouldDecrease = true;
changeRateTimer = CHANGE_RATE;
}
else if (controls().UI_RIGHT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
{
shouldIncrease = true;
changeRateTimer = CHANGE_RATE;
}
// Actually increasing/decreasing the value
if (shouldDecrease)
{
var isBelowMin:Bool = currentValue - step < min;
currentValue = (currentValue - step).clamp(min, max);
if (onChangeCallback != null && !isBelowMin) onChangeCallback(currentValue);
}
else if (shouldIncrease)
{
var isAboveMax:Bool = currentValue + step > max;
currentValue = (currentValue + step).clamp(min, max);
if (onChangeCallback != null && !isAboveMax) onChangeCallback(currentValue);
}
}
lefthandText.text = formatted(currentValue);
}
/** Turns the float into a string */
function formatted(value:Float):String
{
var float:Float = toFixed(value);
if (valueFormatter != null)
{
return valueFormatter(float);
}
else
{
return '${float}';
}
}
function toFixed(value:Float):Float
{
var multiplier:Float = Math.pow(10, precision);
return Math.floor(value * multiplier) / multiplier;
}
}

View file

@ -16,7 +16,7 @@ class LevelProp extends Bopper
this.propData = value;
this.visible = this.propData != null;
danceEvery = this.propData?.danceEvery ?? 0;
danceEvery = this.propData?.danceEvery ?? 0.0;
applyData();
}

View file

@ -390,9 +390,9 @@ class StoryMenuState extends MusicBeatState
if (controls.BACK && !exitingMenu && !selectedLevel)
{
FunkinSound.playOnce(Paths.sound('cancelMenu'));
exitingMenu = true;
FlxG.switchState(() -> new MainMenuState());
FunkinSound.playOnce(Paths.sound('cancelMenu'));
}
}

View file

@ -291,28 +291,47 @@ class LoadingState extends MusicBeatSubState
FunkinSprite.preparePurgeCache();
FunkinSprite.cacheTexture(Paths.image('healthBar'));
FunkinSprite.cacheTexture(Paths.image('menuDesat'));
FunkinSprite.cacheTexture(Paths.image('combo'));
FunkinSprite.cacheTexture(Paths.image('num0'));
FunkinSprite.cacheTexture(Paths.image('num1'));
FunkinSprite.cacheTexture(Paths.image('num2'));
FunkinSprite.cacheTexture(Paths.image('num3'));
FunkinSprite.cacheTexture(Paths.image('num4'));
FunkinSprite.cacheTexture(Paths.image('num5'));
FunkinSprite.cacheTexture(Paths.image('num6'));
FunkinSprite.cacheTexture(Paths.image('num7'));
FunkinSprite.cacheTexture(Paths.image('num8'));
FunkinSprite.cacheTexture(Paths.image('num9'));
// Lord have mercy on me and this caching -anysad
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/combo'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num0'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num1'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num2'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num3'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num4'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num5'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num6'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num7'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num8'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/num9'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/combo'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num0'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num1'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num2'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num3'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num4'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num5'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num6'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num7'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num8'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num9'));
FunkinSprite.cacheTexture(Paths.image('notes', 'shared'));
FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));
FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets'));
FunkinSprite.cacheTexture(Paths.image('ready', 'shared'));
FunkinSprite.cacheTexture(Paths.image('set', 'shared'));
FunkinSprite.cacheTexture(Paths.image('go', 'shared'));
FunkinSprite.cacheTexture(Paths.image('sick', 'shared'));
FunkinSprite.cacheTexture(Paths.image('good', 'shared'));
FunkinSprite.cacheTexture(Paths.image('bad', 'shared'));
FunkinSprite.cacheTexture(Paths.image('shit', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/ready', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/set', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/go', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/ready', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/set', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/go', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/sick'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/good'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/bad'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/shit'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/sick'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/good'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/bad'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/shit'));
FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
// List all image assets in the level's library.

View file

@ -258,6 +258,11 @@ class Constants
*/
public static final DEFAULT_NOTE_STYLE:String = 'funkin';
/**
* The default pixel note style for songs.
*/
public static final DEFAULT_PIXEL_NOTE_STYLE:String = 'pixel';
/**
* The default album for songs in Freeplay.
*/

View file

@ -48,11 +48,11 @@ class MemoryUtil
* Calculate the total memory usage of the program, in bytes.
* @return Int
*/
public static function getMemoryUsed():Int
public static function getMemoryUsed():#if cpp Float #else Int #end
{
#if cpp
// There is also Gc.MEM_INFO_RESERVED, MEM_INFO_CURRENT, and MEM_INFO_LARGE.
return cpp.vm.Gc.memInfo(cpp.vm.Gc.MEM_INFO_USAGE);
return cpp.vm.Gc.memInfo64(cpp.vm.Gc.MEM_INFO_USAGE);
#else
return openfl.system.System.totalMemory;
#end

View file

@ -1,6 +1,9 @@
package funkin.util.plugins;
import flixel.FlxG;
import flixel.FlxBasic;
import funkin.ui.MusicBeatState;
import funkin.ui.MusicBeatSubState;
/**
* A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state.
@ -28,10 +31,15 @@ class ReloadAssetsDebugPlugin extends FlxBasic
if (FlxG.keys.justPressed.F5)
#end
{
funkin.modding.PolymodHandler.forceReloadAssets();
var state:Dynamic = FlxG.state;
if (state is MusicBeatState || state is MusicBeatSubState) state.reloadAssets();
else
{
funkin.modding.PolymodHandler.forceReloadAssets();
// Create a new instance of the current state, so old data is cleared.
FlxG.resetState();
// Create a new instance of the current state, so old data is cleared.
FlxG.resetState();
}
}
}