1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-09-07 14:38:06 +00:00

Compare commits

..

20 commits

Author SHA1 Message Date
cyn 0cf464cc0e
Merge 9894a5351e into d0f0974d1e 2025-07-27 15:47:02 -05:00
Abnormal d0f0974d1e erm...soggy!!! compile error fixie 2025-07-27 15:46:26 -05:00
Abnormal 369b936056 i love mergy conflicties 2025-07-27 01:09:57 -05:00
TechnikTil c40e1e3958 fix fullscreen bug and show clear save data if logged out of ng 2025-07-27 01:09:57 -05:00
TechnikTil 0ade24bac4 HOPEFULLY FIX EVERYTHING WRONG (this time it was tested between two computers) 2025-07-27 01:09:57 -05:00
TechnikTil 7370ef3249 hopefully fix everything with load from ng 2025-07-27 01:09:57 -05:00
TechnikTil b2f162a8ae move clear save data to the top, add clear ng save data 2025-07-27 01:09:57 -05:00
TechnikTil 11fa9b4050 rewrite NGSaveSlot.load to be async, dont precache assets, error handling 2025-07-27 01:09:57 -05:00
TechnikTil 89bd7a3f43 make newgrounds functions in Save static and only available if newgrounds feature flag is enabled, make sure base save slot is loaded and is not empty, and recover save data in case newgrounds doesnt wanna play nice 2025-07-27 01:09:57 -05:00
TechnikTil 942fb5efc9 Add Save Data Options, also allow user to load and save with Newgrounds Save Slots. 2025-07-27 01:09:57 -05:00
cyn0x8 9894a5351e
rework polymod buildImports 2025-07-22 22:09:42 -07:00
Lasercar c2eff142bd Bunker infiltrated 2025-07-22 14:45:47 -07:00
Eric 2784fa18c0
Merge pull request #5476 from Trofem/api-version-example-mods-for-0.7.0
[CHORE] Updates example_mods's API version up to 0.7.0
2025-07-22 12:03:19 -04:00
Eric ad07fddf89
Merge pull request #5489 from FunkinCrew/main
Update develop to latest main
2025-07-22 11:59:29 -04:00
trofim.al 4768eedd5b api version 0.7.0
Update example mod's API version so it actually loads. part 2.
2025-07-22 15:04:20 +11:00
Hyper_ f1c3e99a11 lasercar................................................................................................................................................................................................. 2025-07-22 07:56:29 +08:00
Hyper_ eefe8927c4 The most deranged line loss of all time 2025-07-21 17:43:55 -05:00
cherry 3ff4f14510 Clear key from correct map 2025-07-21 17:13:42 -05:00
Abnormal 955b0db542 THE REALLY COOL STUTTERING FIX!!!!!!!!!!!! 2025-07-21 15:06:19 -05:00
Eric d2df4f0832
Merge pull request #5440 from FunkinCrew/main
Update develop branch
2025-07-21 13:08:44 -04:00
161 changed files with 1847 additions and 3194 deletions

View file

@ -53,8 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- You can import them in another script as usual. - You can import them in another script as usual.
- Added support for renaming imported classes using the `as` keyword. (Thanks KoloInDaCrib!) - Added support for renaming imported classes using the `as` keyword. (Thanks KoloInDaCrib!)
- Fixed `try`/`catch` blocks not working properly. (Thanks NotHyper-474!) - Fixed `try`/`catch` blocks not working properly. (Thanks NotHyper-474!)
- Fixed null-safe field access not working properly for functions (ex. `class?.someFunction()`). (Thanks KoloInDaCrib!) - Fixed null-safe field access not working properly for functions (ex. `class?.someFunction()). (Thanks KoloInDaCrib!)
- Fixed Linux being case-sensitive with filenames. (Thanks mikolka9144!)
### Fixed ### Fixed
@ -89,7 +88,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* @Smokey555 made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318) * @Smokey555 made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318)
* @CCobaltDev made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318) * @CCobaltDev made their first contribution in [#3318](https://github.com/FunkinCrew/Funkin/pull/3318)
* @mikolka9144 made their first contribution in [polymod#212](https://github.com/larsiusprime/polymod/pull/212)
@ -707,7 +705,6 @@ This patch resolves a critical issue that could cause user's save data to become
## [0.5.0] - 2024-09-12 ## [0.5.0] - 2024-09-12
The Playable Pico Update! The Playable Pico Update!
### Added ### Added
- Added a new Character Select screen to switch between playable characters in Freeplay. - Added a new Character Select screen to switch between playable characters in Freeplay.
@ -828,7 +825,7 @@ The Playable Pico Update!
### Fixed ### Fixed
- Control binds in the controls menu no longer overlap their names. - Control binds in the controls menu no longer overlap their names.
- Attempting to exit the gameover screen and retry the song at the same time no longer crashes the game. ([thanks DM-kun for the PR!](https://github.com/FunkinCrew/Funkin/pull/2709)) - Attempting to exit the gameover screen and retry the song at the same time no longer crashes the game. ([thanks DMMaster636 for the PR!](https://github.com/FunkinCrew/Funkin/pull/2709))
- Botplay mode now handles the player's animations properly during hold notes. ([thanks Hundrec!](https://github.com/FunkinCrew/Funkin/pull/2683)) - Botplay mode now handles the player's animations properly during hold notes. ([thanks Hundrec!](https://github.com/FunkinCrew/Funkin/pull/2683))
- Camera movement now pauses when the game is paused. ([thanks Matriculaso!](https://github.com/FunkinCrew/Funkin/pull/2684)) - Camera movement now pauses when the game is paused. ([thanks Matriculaso!](https://github.com/FunkinCrew/Funkin/pull/2684))
- Pico's gameplay sprite no longer appears on the gameover screen when dying from an explosion in 2hot. - Pico's gameplay sprite no longer appears on the gameover screen when dying from an explosion in 2hot.
@ -842,14 +839,13 @@ The Playable Pico Update!
## New Contributors for 0.4.1 ## New Contributors for 0.4.1
* @Hundrec made their first contribution in [#2661](https://github.com/FunkinCrew/Funkin/pull/2661) * @Hundrec made their first contribution in [#2661](https://github.com/FunkinCrew/Funkin/pull/2661)
* @DM-kun made their first contribution in [#2709](https://github.com/FunkinCrew/Funkin/pull/2709) * @DMMaster636 made their first contribution in [#2709](https://github.com/FunkinCrew/Funkin/pull/2709)
* @eltociear made their first contribution in [#2730](https://github.com/FunkinCrew/Funkin/pull/2730) * @eltociear made their first contribution in [#2730](https://github.com/FunkinCrew/Funkin/pull/2730)
## [0.4.0] - 2024-06-06 ## [0.4.0] - 2024-06-06
The Pit Stop 1 update! The Pit Stop 1 update!
### Added ### Added
- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu! - 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu!
@ -1021,7 +1017,6 @@ The Pit Stop 1 update!
## [0.3.0] - 2024-04-30 ## [0.3.0] - 2024-04-30
The Weekend 1 update! The Weekend 1 update!
### Added ### Added
- New Story Level: Weekend 1, starring Pico, Darnell, and Nene. - New Story Level: Weekend 1, starring Pico, Darnell, and Nene.

View file

@ -1,25 +1,14 @@
<div align='center'><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/Friday_Night_Funkin%27_logo.svg/2560px-Friday_Night_Funkin%27_logo.svg.png" width="800"> # Friday Night Funkin'
<h2>Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for <a href="https://ldjam.com/events/ludum-dare/47">Ludum Dare 47.</a></h2> Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
This game was made with love to Newgrounds and its community. Extra love to Tom Fulp. This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
</div>
- [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371) - [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
- [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin) - [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
- [Download Android builds from Google Play!](https://play.google.com/store/apps/details?id=me.funkin.fnf) - [Download Android builds from Google Play!](https://play.google.com/store/apps/details?id=me.funkin.fnf)
- [Download iOS builds from the App Store!](https://apps.apple.com/app/id6740428530) - [Download iOS builds from the App Store!](https://apps.apple.com/app/id6740428530)
<div align='center'>
<table>
<tr>
<td><img src="https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif" alt="Title Screen" width="350"/></td>
<td><img src="https://fridaynightfunkin.wiki.gg/images/9/99/Menu.png" alt="Main Menu" width="350"/></td>
</tr>
</table>
</div>
# Getting Started # Getting Started
**PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME** **PLEASE USE THE LINKS ABOVE IF YOU JUST WANT TO PLAY THE GAME**

2
art

@ -1 +1 @@
Subproject commit 094ff109197e35d21342bdf2d51132be23948d3f Subproject commit 67e550dbd22a8ea429eecc8ce078a36f74ea7723

2
assets

@ -1 +1 @@
Subproject commit 1268fe6c3c246623ede83a57faab89181755f93f Subproject commit 69c02fa5019f603324a7d2ae362327a1eef9d109

View file

@ -1,64 +0,0 @@
{
"input": "./assets",
"output": "./astc-textures/",
"quality": "medium",
"blocksize": "10x10",
"colorprofile": "cl",
"custom": [
{
"asset": "assets/week1/images/erect/server.png",
"blocksize": "8x8"
},
{
"asset": "assets/week1/images/erect/crowd.png",
"blocksize": "8x8"
},
{
"asset": "assets/week1/image/erect/bg.png",
"blocksize": "8x8"
},
{
"asset": "assets/week1/image/erect/lights.png",
"blocksize": "6x6"
},
{
"asset": "assets/week2/images/erect/stairsDark.png",
"blocksize": "6x6"
},
{
"asset": "assets/week2/images/erect/stairsLight.png",
"blocksize": "6x6"
}
],
"excludes": [
"assets/preload/images/cursor/",
"assets/preload/images/freeplay/",
"assets/preload/images/icons/",
"assets/preload/images/fonts/",
"assets/preload/images/titleEnter.png",
"assets/preload/images/titleEnter_mobile.png",
"assets/preload/images/soundtray/",
"assets/preload/images/stageBuild/",
"assets/preload/images/ui/popup/pixel/*",
"assets/shared/images/characters/abotPixel/*",
"assets/shared/images/characters/bfPixel.png",
"assets/shared/images/characters/bfPixelsDEAD.png",
"assets/shared/images/characters/gfPixel.png",
"assets/shared/images/characters/nenePixel/*",
"assets/shared/images/characters/picoPixel/*",
"assets/shared/images/characters/senpai.png",
"assets/shared/images/characters/spirit.png",
"assets/shared/images/ui/chart-editor/",
"assets/shared/images/ui/countdown/pixel/*",
"assets/shared/images/resultScreen/*",
"assets/shared/images/resultScreen/clearPercent/",
"assets/shared/images/resultScreen/rankText/",
"assets/week1/*",
"assets/week1/images/erect/brightLightSmall.png",
"assets/week1/images/erect/lightAbove.png",
"assets/week1/images/erect/lightgreen.png",
"assets/week1/images/erect/lightred.png",
"assets/week1/images/erect/orangeLight.png",
"assets/week6/"
]
}

View file

@ -80,7 +80,7 @@
{ {
"props": { "props": {
"ignoreExtern": true, "ignoreExtern": true,
"format": "^?:_[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$", "format": "^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$",
"tokens": ["INLINE", "NOTINLINE"] "tokens": ["INLINE", "NOTINLINE"]
}, },
"type": "ConstantName" "type": "ConstantName"

21
compression-excludes.txt Normal file
View file

@ -0,0 +1,21 @@
assets/preload/images/cursor/*
assets/preload/images/freeplay/*
assets/preload/images/icons/*
assets/preload/images/titleEnter.png
assets/preload/images/titleEnter_mobile.png
assets/preload/images/soundtray/*
assets/preload/images/stageBuild/*
assets/preload/images/ui/popup/pixel/
assets/shared/images/characters/abotPixel/
assets/shared/images/characters/bfPixel.png
assets/shared/images/characters/bfPixelsDEAD.png
assets/shared/images/characters/gfPixel.png
assets/shared/images/characters/nenePixel/
assets/shared/images/characters/picoPixel/
assets/shared/images/characters/senpai.png
assets/shared/images/characters/spirit.png
assets/shared/images/resultScreen/*
assets/shared/images/ui/chart-editor/*
assets/shared/images/ui/countdown/pixel/
assets/week1/
assets/week6/*

View file

@ -11,41 +11,41 @@
"name": "astc-compressor", "name": "astc-compressor",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "9b0819150b67bf0d49145f250099b75d21be5839", "ref": "8c9f56927c523df7b849352c6951f04112fe15cc",
"url": "https://github.com/KarimAkra/astc-compressor" "url": "https://github.com/KarimAkra/astc-compressor"
}, },
{ {
"name": "extension-admob", "name": "extension-admob",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "02334589ff9603a5f483077a44395009644f6274", "ref": "a53d5916bdcb2e48913f94d9ae1d949b049dcdc1",
"url": "https://github.com/FunkinCrew/extension-admob" "url": "https://github.com/FunkinCrew/extension-admob"
}, },
{ {
"name": "extension-androidtools", "name": "extension-androidtools",
"type": "haxelib", "type": "haxelib",
"version": "2.2.2" "version": "2.2.1"
}, },
{ {
"name": "extension-haptics", "name": "extension-haptics",
"type": "haxelib", "type": "haxelib",
"version": "1.0.4" "version": "1.0.3"
}, },
{ {
"name": "extension-iapcore", "name": "extension-iapcore",
"type": "haxelib", "type": "haxelib",
"version": "1.0.4" "version": "1.0.3"
}, },
{ {
"name": "extension-iarcore", "name": "extension-iarcore",
"type": "haxelib", "type": "haxelib",
"version": "1.0.3" "version": "1.0.2"
}, },
{ {
"name": "flixel", "name": "flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "ac2a34fe4b3400bc04218f46320a65af0f337fc0", "ref": "08fc955ca87f192a971719a675f1d3b21709725d",
"url": "https://github.com/FunkinCrew/flixel" "url": "https://github.com/FunkinCrew/flixel"
}, },
{ {
@ -59,7 +59,7 @@
"name": "flxanimate", "name": "flxanimate",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "49214278b9124823582cdcecd94f4a1de9a4b36b", "ref": "39c1572add28869c558b218fffed13df1b64f376",
"url": "https://github.com/FunkinCrew/flxanimate" "url": "https://github.com/FunkinCrew/flxanimate"
}, },
{ {
@ -104,14 +104,14 @@
"name": "hscript", "name": "hscript",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "8c1d238b069ef97cec13f5121b29d2afe2b8985d", "ref": "d60bb2947fa609fdc875ccfae89666a6984eeaf2",
"url": "https://github.com/FunkinCrew/hscript" "url": "https://github.com/FunkinCrew/hscript"
}, },
{ {
"name": "hxcpp", "name": "hxcpp",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "5a0dc3f644dc676a4a092b7e6c8edc8be941f024", "ref": "4e24283a047f11bded6affabbc9ec405156e026e",
"url": "https://github.com/FunkinCrew/hxcpp" "url": "https://github.com/FunkinCrew/hxcpp"
}, },
{ {
@ -170,9 +170,37 @@
"name": "lime", "name": "lime",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "e5f8c27124598505917a001588b560244731adfb", "ref": "c750ebf6b48c4bc018abe9855fbae5ffdbc4771a",
"url": "https://github.com/FunkinCrew/lime" "url": "https://github.com/FunkinCrew/lime"
}, },
{
"name": "mconsole",
"type": "git",
"dir": null,
"ref": "06c0499ed8f80628a0e6e55ffa32c3cbd688a838",
"url": "https://github.com/massive-oss/mconsole"
},
{
"name": "mcover",
"type": "git",
"dir": "src",
"ref": "c3c47cd682b0b202a41caee95321989391b617ef",
"url": "https://github.com/massive-oss/mcover"
},
{
"name": "mockatoo",
"type": "git",
"dir": "src",
"ref": "13d77a0a8eaf5e789ef5dae6cd33eee812deda36",
"url": "https://github.com/FunkinCrew/mockatoo"
},
{
"name": "munit",
"type": "git",
"dir": "src",
"ref": "f61be7f7ba796595f45023ca65164a485aba0e7e",
"url": "https://github.com/FunkinCrew/MassiveUnit"
},
{ {
"name": "newgrounds", "name": "newgrounds",
"type": "git", "type": "git",
@ -191,14 +219,14 @@
"name": "polymod", "name": "polymod",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "db5dcf3ac4cb59197188a4442d75d4e76b350f40", "ref": "866f19edbcd872b3358f9a41f2f6a24c71c191d1",
"url": "https://github.com/larsiusprime/polymod" "url": "https://github.com/larsiusprime/polymod"
}, },
{ {
"name": "thx.core", "name": "thx.core",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "2bf2b992e06159510f595554e6b952e47922f128", "ref": "76d87418fadd92eb8e1b61f004cff27d656e53dd",
"url": "https://github.com/fponticelli/thx.core" "url": "https://github.com/fponticelli/thx.core"
}, },
{ {

View file

@ -5,10 +5,8 @@ import hxp.*;
import lime.tools.*; import lime.tools.*;
import sys.FileSystem; import sys.FileSystem;
import sys.io.File; import sys.io.File;
import haxe.io.Bytes;
import haxe.io.Path; import haxe.io.Path;
import haxe.ds.Map; import haxe.ds.Map;
import haxe.xml.Printer;
using StringTools; using StringTools;
@ -60,11 +58,6 @@ class Project extends HXProject
*/ */
static final SOURCE_DIR:String = "source"; static final SOURCE_DIR:String = "source";
/**
* The relative location of the templates folder.
*/
static final TEMPLATES_DIR:String = "templates";
/** /**
* The fully qualified class path for the game's preloader. * The fully qualified class path for the game's preloader.
* Particularly important on HTML5 but we use it on all platforms. * Particularly important on HTML5 but we use it on all platforms.
@ -110,11 +103,30 @@ class Project extends HXProject
static final ANDROID_EXTENSIONS:Array<String> = ["funkin.extensions.CallbackUtil"]; static final ANDROID_EXTENSIONS:Array<String> = ["funkin.extensions.CallbackUtil"];
//
// ASTC COMPRESSION
//
/**
* The quality of the compression process.
* This doesn't exactly reduce the quality of the outputted texture..
* But it changes how hard the CPU gotta work to maintin the quality and details of the image while compressing.
* possible values: fastest, fast, medium, thorough, exhaustive.
*/
static var ASTC_QUALITY:String = "medium";
/** /**
* The team ID to use for the iOS app. Configured in XCode. * The team ID to use for the iOS app. Configured in XCode.
*/ */
static var IOS_TEAM_ID:String = "Z7G7AVNGSH"; static var IOS_TEAM_ID:String = "Z7G7AVNGSH";
/**
* The block size of the ASTC compressed textures.
* Higher block size means lower quality and lighter file size for the generated compressed textures.
* possible block sizes: 4x4, 4x6, 6x6, 8x8 ... 12x12.
*/
static var ASTC_BLOCKSIZE:String = "10x10";
/** /**
* A list of asset file globs to exclude from ASTC compression when creating optimized mobile builds. * A list of asset file globs to exclude from ASTC compression when creating optimized mobile builds.
*/ */
@ -318,10 +330,10 @@ class Project extends HXProject
static final FEATURE_HAPTICS:FeatureFlag = "FEATURE_HAPTICS"; static final FEATURE_HAPTICS:FeatureFlag = "FEATURE_HAPTICS";
/** /**
* `-DFEATURE_LAG_ADJUSTMENT` * `-DFEATURE_INPUT_OFFSETS`
* If this flag is enabled, the input offsets menu will be available to configure your audio and visual offsets. * If this flag is enabled, the input offsets menu will be available to configure your audio and visual offsets.
*/ */
static final FEATURE_LAG_ADJUSTMENT:FeatureFlag = "FEATURE_LAG_ADJUSTMENT"; static final FEATURE_INPUT_OFFSETS:FeatureFlag = "FEATURE_INPUT_OFFSETS";
/** /**
* `-DFEATURE_LOG_TRACE` * `-DFEATURE_LOG_TRACE`
@ -475,7 +487,6 @@ class Project extends HXProject
envConfig = readEnvironmentFile("./.env"); envConfig = readEnvironmentFile("./.env");
flair(); flair();
configureApp(); configureApp();
displayTarget(); displayTarget();
@ -487,10 +498,16 @@ class Project extends HXProject
configureOutputDir(); configureOutputDir();
configurePolymod(); configurePolymod();
configureHaxelibs(); configureHaxelibs();
configureASTCTextures();
configureAssets(); configureAssets();
configureIcons(); configureIcons();
readASTCExclusion();
if (FEATURE_COMPRESSED_TEXTURES.isEnabled(this))
{
runASTCCompressor();
}
if (FEATURE_MOBILE_ADVERTISEMENTS.isEnabled(this)) if (FEATURE_MOBILE_ADVERTISEMENTS.isEnabled(this))
{ {
configureAdMobKeys(); configureAdMobKeys();
@ -505,6 +522,8 @@ class Project extends HXProject
{ {
configureIOS(); configureIOS();
} }
info("Done configuring project.");
} }
/** /**
@ -513,9 +532,10 @@ class Project extends HXProject
function flair() function flair()
{ {
// TODO: Implement this. // TODO: Implement this.
info("Friday Night Funkin' - " + VERSION); info("Friday Night Funkin'");
info("Initializing build..."); info("Initializing build...");
info("Target Version: " + VERSION);
info("Git Branch: " + getGitBranch()); info("Git Branch: " + getGitBranch());
info("Git Commit: " + getGitCommit()); info("Git Commit: " + getGitCommit());
info("Git Modified? " + getGitModified()); info("Git Modified? " + getGitModified());
@ -546,9 +566,6 @@ class Project extends HXProject
// If for some reason we have multiple source directories, we can add more entries here. // If for some reason we have multiple source directories, we can add more entries here.
this.sources.push(SOURCE_DIR); this.sources.push(SOURCE_DIR);
// Templates stuff
this.templatePaths.push(TEMPLATES_DIR);
// Tell Lime to run some prebuild and postbuild scripts. // Tell Lime to run some prebuild and postbuild scripts.
this.preBuildCallbacks.push(buildHaxeCLICommand(PREBUILD_HX)); this.preBuildCallbacks.push(buildHaxeCLICommand(PREBUILD_HX));
this.postBuildCallbacks.push(buildHaxeCLICommand(POSTBUILD_HX)); this.postBuildCallbacks.push(buildHaxeCLICommand(POSTBUILD_HX));
@ -812,7 +829,7 @@ class Project extends HXProject
FEATURE_STAGE_EDITOR.apply(this, !(isWeb() || isMobile())); FEATURE_STAGE_EDITOR.apply(this, !(isWeb() || isMobile()));
// Should be true except on web builds (some asset stuff breaks it) // Should be true except on web builds (some asset stuff breaks it)
FEATURE_LAG_ADJUSTMENT.apply(this, !isWeb()); FEATURE_INPUT_OFFSETS.apply(this, !isWeb());
// Should be true except on web and mobile builds. // Should be true except on web and mobile builds.
// Screenshots doesn't work there, and mobile has its own screenshots anyway. // Screenshots doesn't work there, and mobile has its own screenshots anyway.
@ -843,79 +860,6 @@ class Project extends HXProject
// Since mobile is pretty memory safe (I'm looking at you Apple...) we need them BADLY to have the game run properly. // Since mobile is pretty memory safe (I'm looking at you Apple...) we need them BADLY to have the game run properly.
// It's kept off for desktop due to the low ASTC support on desktop GPUs (which seem to be only among Intergrated Graphics). // It's kept off for desktop due to the low ASTC support on desktop GPUs (which seem to be only among Intergrated Graphics).
FEATURE_COMPRESSED_TEXTURES.apply(this, isMobile() && isRelease()); FEATURE_COMPRESSED_TEXTURES.apply(this, isMobile() && isRelease());
renderFlagsTable();
}
/**
* Renders a cute little table of which feature flags are enabled and which are disabled
*/
function renderFlagsTable():Void
{
var enabledWidth:Int = 0;
var disabledWidth:Int = 0;
var enabledFlags:Array<String> = [];
var disabledFlags:Array<String> = [];
var unicodeCheck:String = " ";
var unicodeCross:String = "× ";
if (isWindowsHost())
{
#if (!target.unicode)
// lime build tools uses neko (unless compiling with `-eval`), and neko on windows doesn't support UTF8/unicode characters
// so we need to remove our cute emoji!
unicodeCheck = "o ";
unicodeCross = "x ";
#end
}
for (flagStr in this.haxedefs.keys())
{
if (!flagStr.startsWith(FeatureFlag.INVERSE_PREFIX))
{
// we do flagStr.length + 3 to accomodate for adding the "✓ " at the beginning
enabledWidth = Std.int(Math.max(flagStr.length + 3, enabledWidth));
enabledFlags.push(unicodeCheck + flagStr);
}
else
{
disabledWidth = Std.int(Math.max(flagStr.length, disabledWidth));
disabledFlags.push(unicodeCross + flagStr.substring(FeatureFlag.INVERSE_PREFIX.length));
}
}
enabledFlags.sort(function(a, b) return a > b ? 1 : -1);
disabledFlags.sort(function(a, b) return a > b ? 1 : -1);
var horizontalDivider:String = "".rpad("-", enabledWidth + disabledWidth);
info(horizontalDivider);
// Header
info("FEATURE FLAGS");
info('Enabled Flags $unicodeCheck'.rpad(" ", enabledWidth + 5) + 'Disabled Flags $unicodeCross');
info(horizontalDivider);
while (enabledFlags.length > 0 || disabledFlags.length > 0)
{
var outputLine:String = "";
if (enabledFlags.length > 0)
outputLine += enabledFlags.shift();
outputLine = outputLine.rpad(" ", enabledWidth + 5);
outputLine += "| ";
if (disabledFlags.length > 0)
outputLine += disabledFlags.shift() ?? "";
info(outputLine);
}
info(horizontalDivider);
} }
/** /**
@ -946,7 +890,7 @@ class Project extends HXProject
if (FEATURE_LOG_TRACE.isDisabled(this)) if (FEATURE_LOG_TRACE.isDisabled(this))
{ {
setHaxedef("no-traces"); addHaxeFlag("--no-traces");
} }
// Disable the built in pause screen when unfocusing the game. // Disable the built in pause screen when unfocusing the game.
@ -1029,17 +973,17 @@ class Project extends HXProject
// we use a dedicated 'tracy' folder, since it generally needs a recompile when in use // we use a dedicated 'tracy' folder, since it generally needs a recompile when in use
if (FEATURE_DEBUG_TRACY.isEnabled(this)) buildDir += "-tracy"; if (FEATURE_DEBUG_TRACY.isEnabled(this)) buildDir += "-tracy";
info('Output directory: $buildDir');
// trailing slash might not be needed, works fine on macOS without it, but I haven't tested on Windows! // trailing slash might not be needed, works fine on macOS without it, but I haven't tested on Windows!
buildDir += "/"; buildDir += "/";
info('Output directory: $buildDir');
// setenv('BUILD_DIR', buildDir);
app.path = buildDir; app.path = buildDir;
} }
function configureAndroid() function configureAndroid()
{ {
javaPaths.push(Path.join([SOURCE_DIR, 'funkin/external/android/java'])); javaPaths.push(Path.join([SOURCE_DIR, 'funkin/mobile/external/android/java']));
if (isRelease()) if (isRelease())
{ {
@ -1051,37 +995,47 @@ class Project extends HXProject
keystore.password = Std.string(envConfig.get('ANDROID_KEYSTORE_PASSWORD')); keystore.password = Std.string(envConfig.get('ANDROID_KEYSTORE_PASSWORD'));
} }
Reflect.setField(config.get('android.manifest'), 'xmlns:tools', 'http://schemas.android.com/tools');
final permissionsConfig:Array<String> = [
'<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />',
'<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />'
];
final xmlChildren:Null<Array<String>> = config.get('android.manifest').xmlChildren;
if (xmlChildren != null)
{
config.get('android.manifest').xmlChildren = xmlChildren.concat(permissionsConfig);
}
else
{
config.get('android.manifest').xmlChildren = permissionsConfig;
}
if (isBundle()) if (isBundle())
{ {
config.set("android.gradle-project-directory", "bin-bundle"); config.set("android.gradle-project-directory", "bin-bundle");
} }
} }
final modsFolderProvider:Xml = Xml.createElement('provider'); final modsFolderProvider = [
modsFolderProvider.set('android:authorities', '$PACKAGE_NAME.docprovider'); '<provider android:authorities="$PACKAGE_NAME.docprovider" android:name="funkin.provider.DataFolderProvider" android:grantUriPermissions="true" android:exported="true" android:permission="android.permission.MANAGE_DOCUMENTS">',
modsFolderProvider.set('android:name', 'funkin.provider.DataFolderProvider'); ' <intent-filter>',
modsFolderProvider.set('android:grantUriPermissions', 'true'); ' <action android:name="android.content.action.DOCUMENTS_PROVIDER" />',
modsFolderProvider.set('android:exported', 'true'); ' </intent-filter>',
modsFolderProvider.set('android:permission', 'android.permission.MANAGE_DOCUMENTS'); '</provider>'
];
final intentFilter:Xml = Xml.createElement('intent-filter'); final xmlChildren:Null<Array<String>> = config.get('android.application').xmlChildren;
final action:Xml = Xml.createElement('action'); if (xmlChildren != null)
action.set('android:name', 'android.content.action.DOCUMENTS_PROVIDER');
intentFilter.addChild(action);
modsFolderProvider.addChild(intentFilter);
@:nullSafety(Off)
{ {
if (config.get('android.application').xmlChildren != null) config.get('android.application').xmlChildren = xmlChildren.concat(modsFolderProvider);
{
config.get('android.application').xmlChildren.push(Printer.print(modsFolderProvider));
} }
else else
{ {
config.get('android.application').xmlChildren = [Printer.print(modsFolderProvider)]; config.get('android.application').xmlChildren = modsFolderProvider;
}
} }
final extensions:Null<Array<String>> = config.getArrayString('android.extension'); final extensions:Null<Array<String>> = config.getArrayString('android.extension');
@ -1284,14 +1238,7 @@ class Project extends HXProject
addAsset("LICENSE.md", "LICENSE.md", "art", shouldEmbedArt); addAsset("LICENSE.md", "LICENSE.md", "art", shouldEmbedArt);
addAsset("CHANGELOG.md", "CHANGELOG.md", "art", shouldEmbedArt); addAsset("CHANGELOG.md", "CHANGELOG.md", "art", shouldEmbedArt);
if (shouldEmbed) info('Done configuring assets.');
{
info('Done embedding assets.');
}
else
{
info('Done including assets.');
}
} }
/** /**
@ -1304,7 +1251,27 @@ class Project extends HXProject
if (isAndroid()) if (isAndroid())
{ {
// Adaptive icons // Adaptive icons
adaptiveIcon = new AdaptiveIcon('art/icons/android/', true); // TODO: Add Adapative Icons in Lime
templatePaths.push("templates");
var androidTheme:String = "@style/LimeAppMainTheme";
if (window.fullscreen != null && window.fullscreen)
{
androidTheme += "Fullscreen";
}
final iconXmlChild:Dynamic = {
"android:label": meta.title,
"android:allowBackup": "true",
"android:theme": androidTheme,
"android:hardwareAccelerated": "true",
"android:allowNativeHeapPointerTagging": ANDROID_TARGET_SDK_VERSION >= 30 ? "false" : null,
"android:largeHeap": "true",
"android:icon": "@mipmap/ic_launcher",
"android:roundIcon": "@mipmap/ic_launcher_round"
};
config.set('android.application', iconXmlChild);
} }
else if (isIOS()) else if (isIOS())
{ {
@ -1336,25 +1303,8 @@ class Project extends HXProject
addIcon("art/icons/icon64.png", 64); addIcon("art/icons/icon64.png", 64);
addIcon("art/icons/iconOG.png"); addIcon("art/icons/iconOG.png");
}
info('Done configuring icons.'); info('Done configuring icons.');
} }
/**
* Configure the astc textures.
*/
function configureASTCTextures()
{
if (command != "display")
{
readASTCExclusion();
if (FEATURE_COMPRESSED_TEXTURES.isEnabled(this))
{
runASTCCompressor();
}
}
} }
/** /**
@ -1410,21 +1360,6 @@ class Project extends HXProject
return this.architectures.contains(Architecture.X64); return this.architectures.contains(Architecture.X64);
} }
public function isWindowsHost():Bool
{
return System.hostPlatform == WINDOWS;
}
public function isMacHost():Bool
{
return System.hostPlatform == MAC;
}
public function isLinuxHost():Bool
{
return System.hostPlatform == LINUX;
}
public function isWindows():Bool public function isWindows():Bool
{ {
return this.target == Platform.WINDOWS; return this.target == Platform.WINDOWS;
@ -1768,8 +1703,7 @@ class Project extends HXProject
*/ */
public function error(message:String):Void public function error(message:String):Void
{ {
Sys.stderr().write(Bytes.ofString('[ERROR] ${message}')); Log.error('${message}');
Sys.exit(1);
} }
/** /**
@ -1779,7 +1713,7 @@ class Project extends HXProject
{ {
if (command != "display") if (command != "display")
{ {
Sys.println('[INFO] ${message}'); Log.info('[INFO] ${message}');
} }
} }
@ -1806,67 +1740,46 @@ class Project extends HXProject
var env = new Map<String, Dynamic>(); var env = new Map<String, Dynamic>();
for (line in envFile.split('\n')) for (line in envFile.split('\n'))
{ {
if (line.length <= 0 || line.startsWith("#") || shouldExcludeEnvKey(line)) continue; if (line == "" || line.startsWith("#")) continue;
var index:Int = line.indexOf('='); var parts = line.split('=');
if (parts.length != 2) continue;
if (index == -1) continue; env.set(parts[0], parts[1]);
var field:String = line.substr(0, index);
var value:String = line.substr(index + 1);
env.set(field, value);
} }
return env; return env;
} }
private function shouldExcludeEnvKey(key:String):Bool
{
final android:Bool = key.startsWith('ANDROID_');
final ios:Bool = key.startsWith('IOS_');
final mobile:Bool = key.startsWith('MOBILE_') || ios || android;
final web:Bool = key.startsWith('WEB_');
final desktop:Bool = key.startsWith('DESKTOP_');
if (isWeb() && (mobile || desktop)) return true;
if (isDesktop() && (mobile || web)) return true;
if (isAndroid() && (ios || web || desktop)) return true;
if (isIOS() && (android || web || desktop)) return true;
return false;
}
public function readASTCExclusion():Void public function readASTCExclusion():Void
{ {
@:nullSafety(Off) astcExcludes = File.getContent('./compression-excludes.txt').trim().split('\n');
astcExcludes = haxe.Json.parse(File.getContent('./astc-compression-data.json')).excludes;
for (i in 0...astcExcludes.length) for (i in 0...astcExcludes.length)
astcExcludes[i] = astcExcludes[i].trim(); astcExcludes[i] = astcExcludes[i].trim();
} }
public function isASTCExcluded(file:String):Bool public function isASTCExcluded(filePath:String):Bool
{ {
for (exclusion in astcExcludes) for (exclusion in astcExcludes)
{ {
if (exclusion.endsWith("/")) if (exclusion.endsWith("/*"))
{ {
var normalizedFilePath = Path.normalize(file); var normalizedFilePath = Path.normalize(filePath);
var normalizedExclusion = Path.normalize(exclusion); var normalizedExclusion = Path.normalize(exclusion.substr(0, exclusion.length - 2));
if (normalizedFilePath.startsWith(normalizedExclusion)) return true; if (normalizedFilePath.startsWith(normalizedExclusion)) return true;
} }
else if (exclusion.endsWith("/*")) else if (exclusion.endsWith("/"))
{ {
var normalizedExclusion = Path.normalize(exclusion.substr(0, exclusion.length - 2)); var normalizedExclusion = Path.normalize(exclusion);
var fileDirectory = Path.directory(Path.normalize(file)); var fileDirectory = Path.directory(Path.normalize(filePath));
if (fileDirectory == normalizedExclusion) return true; if (fileDirectory == normalizedExclusion) return true;
} }
else else
{ {
if (file == exclusion) return true; if (filePath == exclusion) return true;
} }
} }
@ -1877,12 +1790,16 @@ class Project extends HXProject
{ {
info('Compressing ASTC textures...'); info('Compressing ASTC textures...');
var args:Array<String> = ['run', 'astc-compressor', 'compress-from-json']; var args:Array<String> = ['run', 'astc-compressor'];
args = args.concat(['-json', './astc-compression-data.json']); args = args.concat(['-i', './assets']);
args = args.concat(['-o', './astc-textures/assets/']);
args = args.concat(['-blocksize', ASTC_BLOCKSIZE]);
args = args.concat(['-quality', ASTC_QUALITY]);
args = args.concat(['-excludes', './compression-excludes.txt']);
Sys.command('haxelib', args); Sys.command('haxelib', args);
info('Done compressing ASTC textures.'); info('Finished compressing ASTC textures!');
} }
} }
@ -1890,9 +1807,9 @@ class Project extends HXProject
* An object representing a feature flag, which can be enabled or disabled. * An object representing a feature flag, which can be enabled or disabled.
* Includes features such as automatic generation of compile defines and inversion. * Includes features such as automatic generation of compile defines and inversion.
*/ */
abstract FeatureFlag(String) from String to String abstract FeatureFlag(String)
{ {
public static final INVERSE_PREFIX:String = "NO_"; static final INVERSE_PREFIX:String = "NO_";
public function new(input:String) public function new(input:String)
{ {
@ -1917,11 +1834,13 @@ abstract FeatureFlag(String) from String to String
if (isEnabled(project)) if (isEnabled(project))
{ {
// If this flag was already enabled, disable the inverse. // If this flag was already enabled, disable the inverse.
project.info('Enabling feature flag ${this}');
getInverse().disable(project, false); getInverse().disable(project, false);
} }
else if (getInverse().isEnabled(project)) else if (getInverse().isEnabled(project))
{ {
// If the inverse flag was already enabled, disable this flag. // If the inverse flag was already enabled, disable this flag.
project.info('Disabling feature flag ${this}');
disable(project, false); disable(project, false);
} }
else else
@ -1929,11 +1848,13 @@ abstract FeatureFlag(String) from String to String
if (enableByDefault) if (enableByDefault)
{ {
// Enable this flag if it was unset, and disable the inverse. // Enable this flag if it was unset, and disable the inverse.
project.info('Enabling feature flag ${this}');
enable(project, true); enable(project, true);
} }
else else
{ {
// Disable this flag if it was unset, and enable the inverse. // Disable this flag if it was unset, and enable the inverse.
project.info('Disabling feature flag ${this}');
disable(project, true); disable(project, true);
} }
} }

View file

@ -145,7 +145,6 @@ class Main extends Sprite
#if FEATURE_DEBUG_FUNCTIONS #if FEATURE_DEBUG_FUNCTIONS
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil()); game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
funkin.util.macro.ConsoleMacro.init();
#end #end
#if !html5 #if !html5

View file

@ -29,7 +29,7 @@ class Postbuild
var buildTime:Float = roundToTwoDecimals(end - start); var buildTime:Float = roundToTwoDecimals(end - start);
Sys.println('[INFO] Build took: ${buildTime} seconds'); trace('Build took: ${buildTime} seconds');
} }
} }

View file

@ -9,16 +9,28 @@ class Prebuild
{ {
static inline final BUILD_TIME_FILE:String = '.build_time'; static inline final BUILD_TIME_FILE:String = '.build_time';
static final NG_CREDS_PATH:String = './source/funkin/api/newgrounds/NewgroundsCredentials.hx';
static final NG_CREDS_TEMPLATE:String = "package funkin.api.newgrounds;
class NewgroundsCredentials
{
public static final APP_ID:String = #if API_NG_APP_ID haxe.macro.Compiler.getDefine(\"API_NG_APP_ID\") #else 'INSERT APP ID HERE' #end;
public static final ENCRYPTION_KEY:String = #if API_NG_ENC_KEY haxe.macro.Compiler.getDefine(\"API_NG_ENC_KEY\") #else 'INSERT ENCRYPTION KEY HERE' #end;
}";
static function main():Void static function main():Void
{ {
var start:Float = Sys.time(); var start:Float = Sys.time();
// Sys.println('[INFO] Performing pre-build tasks...'); trace('[PREBUILD] Performing pre-build tasks...');
saveBuildTime(); saveBuildTime();
buildCredsFile();
var end:Float = Sys.time(); var end:Float = Sys.time();
var duration:Float = end - start; var duration:Float = end - start;
// Sys.println('[INFO] Finished pre-build tasks in $duration seconds.'); trace('[PREBUILD] Finished pre-build tasks in $duration seconds.');
} }
static function saveBuildTime():Void static function saveBuildTime():Void
@ -29,4 +41,20 @@ class Prebuild
fo.writeDouble(now); fo.writeDouble(now);
fo.close(); fo.close();
} }
static function buildCredsFile():Void
{
if (sys.FileSystem.exists(NG_CREDS_PATH))
{
trace('[PREBUILD] NewgroundsCredentials.hx already exists, skipping.');
}
else
{
trace('[PREBUILD] Creating NewgroundsCredentials.hx...');
var fileContents:String = NG_CREDS_TEMPLATE;
sys.io.File.saveContent(NG_CREDS_PATH, fileContents);
}
}
} }

View file

@ -5,7 +5,6 @@ import flixel.util.FlxSignal;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import funkin.data.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils;
import funkin.play.PlayState;
import funkin.save.Save; import funkin.save.Save;
import funkin.util.TimerUtil.SongSequence; import funkin.util.TimerUtil.SongSequence;
import haxe.Timer; import haxe.Timer;
@ -114,17 +113,6 @@ class Conductor
if (currentTimeChange == null) return Constants.DEFAULT_BPM; if (currentTimeChange == null) return Constants.DEFAULT_BPM;
@:privateAccess
if (PlayState.instance != null && PlayState.instance.startingSong)
{
for (i in 0...timeChanges.length)
{
if (PlayState.instance.startTimestamp >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i];
if (PlayState.instance.startTimestamp < timeChanges[i].timeStamp) break;
}
}
return currentTimeChange.bpm; return currentTimeChange.bpm;
} }
@ -429,7 +417,7 @@ class Conductor
// If the song is playing, limit the song position to the length of the song or beginning of the song. // If the song is playing, limit the song position to the length of the song or beginning of the song.
if (FlxG.sound.music != null && FlxG.sound.music.playing) if (FlxG.sound.music != null && FlxG.sound.music.playing)
{ {
this.songPosition = Math.min(this.combinedOffset, 0).clamp(songPos, currentLength); this.songPosition = FlxMath.bound(Math.min(this.combinedOffset, 0), songPos, currentLength);
this.songPositionDelta += FlxG.elapsed * 1000 * FlxG.sound.music.pitch; this.songPositionDelta += FlxG.elapsed * 1000 * FlxG.sound.music.pitch;
} }
else else

View file

@ -117,7 +117,10 @@ class FunkinMemory
*/ */
public static function cacheTexture(key:String):Void public static function cacheTexture(key:String):Void
{ {
if (currentCachedTextures.exists(key)) return; if (currentCachedTextures.exists(key))
{
return; // Already cached.
}
if (previousCachedTextures.exists(key)) if (previousCachedTextures.exists(key))
{ {
@ -132,13 +135,13 @@ class FunkinMemory
if (graphic == null) if (graphic == null)
{ {
FlxG.log.warn('Failed to cache graphic: $key'); FlxG.log.warn('Failed to cache graphic: $key');
return;
} }
else
{
trace('Successfully cached graphic: $key'); trace('Successfully cached graphic: $key');
graphic.persist = true; graphic.persist = true;
currentCachedTextures.set(key, graphic); currentCachedTextures.set(key, graphic);
forceRender(graphic); }
} }
/** /**
@ -147,54 +150,26 @@ class FunkinMemory
*/ */
static function permanentCacheTexture(key:String):Void static function permanentCacheTexture(key:String):Void
{ {
if (permanentCachedTextures.exists(key)) return; if (permanentCachedTextures.exists(key))
{
return; // Already cached.
}
var graphic:Null<FlxGraphic> = FlxGraphic.fromAssetKey(key, false, null, true); var graphic:Null<FlxGraphic> = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null) if (graphic == null)
{ {
FlxG.log.warn('Failed to cache graphic: $key'); FlxG.log.warn('Failed to cache graphic: $key');
return;
} }
else
{
trace('Successfully cached graphic: $key'); trace('Successfully cached graphic: $key');
graphic.persist = true; graphic.persist = true;
permanentCachedTextures.set(key, graphic); permanentCachedTextures.set(key, graphic);
forceRender(graphic); }
currentCachedTextures = permanentCachedTextures; currentCachedTextures = permanentCachedTextures;
} }
/**
* Forces the GPU to load and upload a FlxGraphic.
*/
private static function forceRender(graphic:FlxGraphic):Void
{
if (graphic == null) return;
var bmp:Null<FlxGraphic> = FlxG.bitmap.get(graphic.key);
if (bmp != null && bmp.bitmap != null) var _:Int = bmp.bitmap.width; // Trigger
// Draws sprite and actually caches it.
var sprite = new flixel.FlxSprite();
sprite.loadGraphic(graphic);
sprite.draw(); // Draw sprite and load it into game's memory.
sprite.destroy();
}
/**
* Checks, if graphic with given path cached in memory.
*/
public static inline function isGraphicCached(path:String):Bool
return permanentCachedTextures.exists(path) || currentCachedTextures.exists(path) || previousCachedTextures.exists(path);
public static function getCachedGraphic(path:String):Null<FlxGraphic>
{
if (permanentCachedTextures.exists(path)) return permanentCachedTextures.get(path);
if (currentCachedTextures.exists(path)) return currentCachedTextures.get(path);
if (previousCachedTextures.exists(path)) return previousCachedTextures.get(path); // just in case
return null;
}
/** /**
* Prepares the cache for purging unused textures. * Prepares the cache for purging unused textures.
*/ */

View file

@ -82,8 +82,11 @@ class InitState extends FlxState
// GAME SETUP // GAME SETUP
// //
// Setup window events (like callbacks for onWindowClose) and fullscreen keybind setup // Setup window events (like callbacks for onWindowClose)
// and fullscreen keybind setup
WindowUtil.initWindowEvents(); WindowUtil.initWindowEvents();
// Disable the thing on Windows where it tries to send a bug report to Microsoft because why do they care?
WindowUtil.disableCrashHandler();
#if FEATURE_DEBUG_TRACY #if FEATURE_DEBUG_TRACY
funkin.util.WindowUtil.initTracy(); funkin.util.WindowUtil.initTracy();
@ -111,7 +114,7 @@ class InitState extends FlxState
#if ios #if ios
// Setup Audio session // Setup Audio session
funkin.external.ios.AudioSession.initialize(); funkin.mobile.external.ios.AudioSession.initialize();
#end #end
// This ain't a pixel art game! (most of the time) // This ain't a pixel art game! (most of the time)
@ -199,7 +202,7 @@ class InitState extends FlxState
// //
#if android #if android
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK]; FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
funkin.external.android.CallbackUtil.init(); funkin.mobile.external.android.CallbackUtil.init();
#end #end
// //

View file

@ -1,6 +1,5 @@
package funkin.api.discord; package funkin.api.discord;
import funkin.util.macro.EnvironmentConfigMacro;
#if FEATURE_DISCORD_RPC #if FEATURE_DISCORD_RPC
import hxdiscord_rpc.Discord; import hxdiscord_rpc.Discord;
import hxdiscord_rpc.Types.DiscordButton; import hxdiscord_rpc.Types.DiscordButton;
@ -12,7 +11,7 @@ import sys.thread.Thread;
@:nullSafety @:nullSafety
class DiscordClient class DiscordClient
{ {
static final CLIENT_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("DESKTOP_DISCORD_CLIENT_ID"); static final CLIENT_ID:String = "816168432860790794";
public static var instance(get, never):DiscordClient; public static var instance(get, never):DiscordClient;
static var _instance:Null<DiscordClient> = null; static var _instance:Null<DiscordClient> = null;
@ -41,28 +40,12 @@ class DiscordClient
{ {
trace('[DISCORD] Initializing connection...'); trace('[DISCORD] Initializing connection...');
if (!hasValidCredentials()) // Discord.initialize(CLIENT_ID, handlers, true, null);
{
FlxG.log.warn("Tried to initialize Discord connection, but credentials are invalid!");
return;
}
@:nullSafety(Off)
{
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, ""); Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
}
createDaemon(); createDaemon();
} }
/**
* @returns `false` if the client ID is invalid.
*/
static function hasValidCredentials():Bool
{
return !(CLIENT_ID == null || CLIENT_ID == "" || (CLIENT_ID != null && CLIENT_ID.contains(" ")));
}
var daemon:Null<Thread> = null; var daemon:Null<Thread> = null;
function createDaemon():Void function createDaemon():Void

View file

@ -1,6 +1,5 @@
package funkin.api.newgrounds; package funkin.api.newgrounds;
import funkin.util.macro.EnvironmentConfigMacro;
import funkin.save.Save; import funkin.save.Save;
import funkin.api.newgrounds.Medals.Medal; import funkin.api.newgrounds.Medals.Medal;
#if FEATURE_NEWGROUNDS #if FEATURE_NEWGROUNDS
@ -18,11 +17,7 @@ import io.newgrounds.objects.User;
@:nullSafety @:nullSafety
class NewgroundsClient class NewgroundsClient
{ {
static final APP_ID:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_APP_ID");
static final ENCRYPTION_KEY:Null<String> = EnvironmentConfigMacro.environmentConfig?.get("API_NG_ENC_KEY");
public static var instance(get, never):NewgroundsClient; public static var instance(get, never):NewgroundsClient;
static var _instance:Null<NewgroundsClient> = null; static var _instance:Null<NewgroundsClient> = null;
static function get_instance():NewgroundsClient static function get_instance():NewgroundsClient
@ -42,8 +37,8 @@ class NewgroundsClient
trace('[NEWGROUNDS] Initializing client...'); trace('[NEWGROUNDS] Initializing client...');
#if FEATURE_NEWGROUNDS_DEBUG #if FEATURE_NEWGROUNDS_DEBUG
trace('[NEWGROUNDS] App ID: ${APP_ID}'); trace('[NEWGROUNDS] App ID: ${NewgroundsCredentials.APP_ID}');
trace('[NEWGROUNDS] Encryption Key: ${ENCRYPTION_KEY}'); trace('[NEWGROUNDS] Encryption Key: ${NewgroundsCredentials.ENCRYPTION_KEY}');
#end #end
if (!hasValidCredentials()) if (!hasValidCredentials())
@ -52,12 +47,9 @@ class NewgroundsClient
return; return;
} }
@:nullSafety(Off) var debug = #if FEATURE_NEWGROUNDS_DEBUG true #else false #end;
{ NG.create(NewgroundsCredentials.APP_ID, getSessionId(), debug, onLoginResolved);
NG.create(APP_ID, getSessionId(), #if FEATURE_NEWGROUNDS_DEBUG true #else false #end, onLoginResolved); NG.core.setupEncryption(NewgroundsCredentials.ENCRYPTION_KEY);
NG.core.setupEncryption(ENCRYPTION_KEY);
}
} }
public function init() public function init()
@ -176,12 +168,12 @@ class NewgroundsClient
*/ */
static function hasValidCredentials():Bool static function hasValidCredentials():Bool
{ {
return !(APP_ID == null return !(NewgroundsCredentials.APP_ID == null
|| APP_ID == "" || NewgroundsCredentials.APP_ID == ""
|| (APP_ID != null && APP_ID.contains(" ")) || NewgroundsCredentials.APP_ID.contains(" ")
|| ENCRYPTION_KEY == null || NewgroundsCredentials.ENCRYPTION_KEY == null
|| ENCRYPTION_KEY == "" || NewgroundsCredentials.ENCRYPTION_KEY == ""
|| (ENCRYPTION_KEY != null && ENCRYPTION_KEY.contains(" "))); || NewgroundsCredentials.ENCRYPTION_KEY.contains(" "));
} }
function onLoginResolved(outcome:LoginOutcome):Void function onLoginResolved(outcome:LoginOutcome):Void

View file

@ -72,7 +72,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
override function set_volume(value:Float):Float override function set_volume(value:Float):Float
{ {
// Uncap the volume. // Uncap the volume.
_volume = value.clamp(0.0, MAX_VOLUME); _volume = FlxMath.bound(value, 0.0, MAX_VOLUME);
updateTransform(); updateTransform();
return _volume; return _volume;
} }
@ -568,8 +568,8 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
if (_sound == null) return; if (_sound == null) return;
// Create a channel manually if the sound is considered important. // Create a channel manually if the sound is considered important.
var pan:Float = (SoundMixer.__soundTransform.pan + _transform.pan).clamp(-1, 1); var pan:Float = FlxMath.bound(SoundMixer.__soundTransform.pan + _transform.pan, -1, 1);
var volume:Float = (SoundMixer.__soundTransform.volume * _transform.volume).clamp(0, MAX_VOLUME); var volume:Float = FlxMath.bound(SoundMixer.__soundTransform.volume * _transform.volume, 0, MAX_VOLUME);
var audioSource:AudioSource = new AudioSource(_sound.__buffer); var audioSource:AudioSource = new AudioSource(_sound.__buffer);
audioSource.offset = Std.int(startTime); audioSource.offset = Std.int(startTime);

View file

@ -165,7 +165,7 @@ typedef NoteStyleAssetData<T> =
var assetPath:String; var assetPath:String;
/** /**
* The scale to render the note at. * The scale to render the prop at.
* @default 1.0 * @default 1.0
*/ */
@:default(1.0) @:default(1.0)
@ -181,7 +181,7 @@ typedef NoteStyleAssetData<T> =
var offsets:Null<Array<Float>>; var offsets:Null<Array<Float>>;
/** /**
* If true, the note is a pixel sprite, and will be rendered without anti-aliasing. * If true, the prop is a pixel sprite, and will be rendered without anti-aliasing.
*/ */
@:default(false) @:default(false)
@:optional @:optional

View file

@ -1117,23 +1117,6 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}' return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')'); + (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
} }
public function buildTooltip():String
{
if ((this.kind?.length ?? 0) == 0) return "";
var result:String = 'Kind: ${this.kind}';
if (this.params.length == 0) return result;
result += "\nParams:";
for (param in params)
{
result += '\n- ${param.name}: ${param.value}';
}
return result;
}
} }
/** /**

View file

@ -10,8 +10,6 @@ class ChartManifestData
*/ */
public static final CHART_MANIFEST_DATA_VERSION:thx.semver.Version = "1.0.0"; public static final CHART_MANIFEST_DATA_VERSION:thx.semver.Version = "1.0.0";
public static final invalidIdRegex:EReg = ~/[\/\\:*?"<>|]/g;
@:default(funkin.data.song.importer.ChartManifestData.CHART_MANIFEST_DATA_VERSION) @:default(funkin.data.song.importer.ChartManifestData.CHART_MANIFEST_DATA_VERSION)
@:jcustomparse(funkin.data.DataParse.semverVersion) @:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion) @:jcustomwrite(funkin.data.DataWrite.semverVersion)
@ -21,11 +19,7 @@ class ChartManifestData
* The internal song ID for this chart. * The internal song ID for this chart.
* The metadata and chart data file names are derived from this. * The metadata and chart data file names are derived from this.
*/ */
public var songId(default, set):String; public var songId:String;
public function set_songId(value:String):String
{
return songId = invalidIdRegex.replace(value.trim(), '');
}
public function new(songId:String) public function new(songId:String)
{ {

View file

@ -1,75 +0,0 @@
package funkin.external.windows;
#if (windows && cpp)
/**
* This class provides handling for Windows API-related functions.
*/
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
@:include('winapi.hpp')
extern class WinAPI
{
/**
* Shows a message box with an error icon.
*
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowError')
static function showError(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Shows a message box with a warning icon.
*
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowWarning')
static function showWarning(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Shows a message box with an information icon.
*
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowInformation')
static function showInformation(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Shows a message box with a question icon.
*
* @param message The message to display.
* @param title The title of the message box.
*/
@:native('WINAPI_ShowQuestion')
static function showQuestion(message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
/**
* Disables the "Report to Microsoft" dialog that appears when the application crashes.
*/
@:native('WINAPI_DisableErrorReporting')
static function disableErrorReporting():Void;
/**
* Disables Windows ghosting, which prevents the system from marking unresponsive windows as "Not Responding."
*/
@:native('WINAPI_DisableWindowsGhosting')
static function disableWindowsGhosting():Void;
/**
* Sets the dark mode for the active window.
*
* @param enable A boolean value indicating whether to enable (true) or disable (false) dark mode.
*/
@:native('WINAPI_SetDarkMode')
static function setDarkMode(enable:Bool):Void;
/**
* Checks if the system is using dark mode.
*
* @return True if system is in dark mode, false otherwise.
*/
@:native('WINAPI_IsSystemDarkMode')
static function isSystemDarkMode():Bool;
}
#end

View file

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<xml>
<pragma once="true" />
<files id="haxe">
<compilerflag value="-I${this_dir}/include" />
</files>
<files id="__main__">
<compilerflag value="-I${this_dir}/include" />
</files>
<files id="winapi">
<compilerflag value="-I${this_dir}/include" />
<file name="${this_dir}/src/winapi.cpp" />
</files>
<target id="haxe">
<section if="mingw">
<lib name="-luser32" />
<lib name="-lkernel32" />
<lib name="-ldwmapi" />
<lib name="-ladvapi32" />
</section>
<section if="windows" unless="mingw">
<lib name="user32.lib" />
<lib name="kernel32.lib" />
<lib name="dwmapi.lib" />
<lib name="advapi32.lib" />
</section>
<files id="winapi" />
</target>
</xml>

View file

@ -1,51 +0,0 @@
#pragma once
/**
* @brief Shows an error message box
* @param message The error message to display
* @param title The title of the message box
*/
void WINAPI_ShowError(const char *message, const char *title);
/**
* @brief Shows a warning message box
* @param message The warning message to display
* @param title The title of the message box
*/
void WINAPI_ShowWarning(const char *message, const char *title);
/**
* @brief Shows an information message box
* @param message The information message to display
* @param title The title of the message box
*/
void WINAPI_ShowInformation(const char *message, const char *title);
/**
* @brief Shows a question message box with OK/Cancel buttons
* @param message The question message to display
* @param title The title of the message box
*/
void WINAPI_ShowQuestion(const char *message, const char *title);
/**
* @brief Disables Windows error reporting dialogs
*/
void WINAPI_DisableErrorReporting();
/**
* @brief Disables Windows ghosting for the current process
*/
void WINAPI_DisableWindowsGhosting();
/**
* @brief Sets dark mode for the active window
* @param enable True to enable dark mode, false to disable
*/
void WINAPI_SetDarkMode(bool enable);
/**
* @brief Checks if the system is using dark mode
* @return True if system is in dark mode, false otherwise
*/
bool WINAPI_IsSystemDarkMode();

View file

@ -1,75 +0,0 @@
#define WIN32_LEAN_AND_MEAN // Excludes rarely-used APIs like cryptography, DDE, RPC, and shell functions, reducing compile time and binary size.
#define NOMINMAX // Prevents Windows from defining min() and max() macros, which can conflict with standard C++ functions.
#define NOCRYPT // Excludes Cryptographic APIs, such as Encrypt/Decrypt functions.
#define NOCOMM // Excludes serial communication APIs, such as COM port handling.
#define NOKANJI // Excludes Kanji character set support (not needed unless working with Japanese text processing).
#define NOHELP // Excludes Windows Help APIs, removing functions related to WinHelp and other help systems.
#include <windows.h>
#include <psapi.h>
#include <dwmapi.h>
#include <stdint.h>
#include <stdio.h>
void WINAPI_ShowError(const char *message, const char *title)
{
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONERROR);
}
void WINAPI_ShowWarning(const char *message, const char *title)
{
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONWARNING);
}
void WINAPI_ShowInformation(const char *message, const char *title)
{
MessageBox(GetActiveWindow(), message, title, MB_OK | MB_ICONINFORMATION);
}
void WINAPI_ShowQuestion(const char *message, const char *title)
{
MessageBox(GetActiveWindow(), message, title, MB_OKCANCEL | MB_ICONQUESTION);
}
void WINAPI_DisableErrorReporting()
{
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
}
void WINAPI_DisableWindowsGhosting()
{
DisableProcessWindowsGhosting();
}
void WINAPI_SetDarkMode(bool enable)
{
HWND window = GetActiveWindow();
int darkMode = enable ? 1 : 0;
if (DwmSetWindowAttribute(window, 20, &darkMode, sizeof(darkMode)) != S_OK)
DwmSetWindowAttribute(window, 19, &darkMode, sizeof(darkMode));
UpdateWindow(window);
}
bool WINAPI_IsSystemDarkMode()
{
HKEY hKey;
DWORD dwValue = 0;
DWORD dwSize = sizeof(DWORD);
DWORD dwType = REG_DWORD;
if (RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", 0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
if (RegQueryValueEx(hKey, "AppsUseLightTheme", NULL, &dwType, (LPBYTE)&dwValue, &dwSize) == ERROR_SUCCESS)
{
RegCloseKey(hKey);
return dwValue == 0;
}
RegCloseKey(hKey);
}
return false;
}

View file

@ -12,7 +12,6 @@ import flixel.math.FlxPoint;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle; import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
import flixel.FlxCamera; import flixel.FlxCamera;
import openfl.system.System; import openfl.system.System;
import funkin.FunkinMemory;
using StringTools; using StringTools;
@ -24,6 +23,20 @@ using StringTools;
@:nullSafety @:nullSafety
class FunkinSprite extends FlxSprite class FunkinSprite extends FlxSprite
{ {
/**
* An internal list of all the textures cached with `cacheTexture`.
* This excludes any temporary textures like those from `FlxText` or `makeSolidColor`.
*/
static var currentCachedTextures:Map<String, FlxGraphic> = [];
/**
* An internal list of textures that were cached in the previous state.
* We don't know whether we want to keep them cached or not.
*/
static var previousCachedTextures:Map<String, FlxGraphic> = [];
static var permanentCachedTextures:Map<String, FlxGraphic> = [];
/** /**
* @param x Starting X position * @param x Starting X position
* @param y Starting Y position * @param y Starting Y position
@ -203,40 +216,122 @@ class FunkinSprite extends FlxSprite
return FlxG.bitmap.get(key) != null; return FlxG.bitmap.get(key) != null;
} }
@:deprecated("Use FunkinMemory.cacheTexture() instead") /**
* Ensure the texture with the given key is cached.
* @param key The key of the texture to cache.
*/
public static function cacheTexture(key:String):Void public static function cacheTexture(key:String):Void
{ {
FunkinMemory.cacheTexture(Paths.image(key)); // We don't want to cache the same texture twice.
if (currentCachedTextures.exists(key)) return;
if (previousCachedTextures.exists(key))
{
// Move the graphic from the previous cache to the current cache.
var graphic = previousCachedTextures.get(key);
previousCachedTextures.remove(key);
if (graphic != null) currentCachedTextures.set(key, graphic);
return;
}
// Else, texture is currently uncached.
var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
currentCachedTextures.set(key, graphic);
}
} }
@:deprecated("Use FunkinMemory.permanentCacheTexture() instead")
public static function permanentCacheTexture(key:String):Void public static function permanentCacheTexture(key:String):Void
{ {
@:privateAccess FunkinMemory.permanentCacheTexture(Paths.image(key)); // We don't want to cache the same texture twice.
if (permanentCachedTextures.exists(key)) return;
// Else, texture is currently uncached.
var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
if (graphic == null)
{
FlxG.log.warn('Failed to cache graphic: $key');
}
else
{
trace('Successfully cached graphic: $key');
graphic.persist = true;
permanentCachedTextures.set(key, graphic);
}
currentCachedTextures = permanentCachedTextures;
} }
@:deprecated("Use FunkinMemory.cacheTexture() instead")
public static function cacheSparrow(key:String):Void public static function cacheSparrow(key:String):Void
{ {
FunkinMemory.cacheTexture(Paths.image(key)); cacheTexture(Paths.image(key));
} }
@:deprecated("Use FunkinMemory.cacheTexture() instead")
public static function cachePacker(key:String):Void public static function cachePacker(key:String):Void
{ {
FunkinMemory.cacheTexture(Paths.image(key)); cacheTexture(Paths.image(key));
} }
@:deprecated("Use FunkinMemory.preparePurgeTextureCache() instead") /**
* Call this, then `cacheTexture` to keep the textures we still need, then `purgeCache` to remove the textures that we won't be using anymore.
*/
public static function preparePurgeCache():Void public static function preparePurgeCache():Void
{ {
FunkinMemory.preparePurgeTextureCache(); previousCachedTextures = currentCachedTextures;
for (graphicKey in previousCachedTextures.keys())
{
if (!permanentCachedTextures.exists(graphicKey)) continue;
previousCachedTextures.remove(graphicKey);
}
currentCachedTextures = permanentCachedTextures;
} }
@:deprecated("Use FunkinMemory.purgeCache() instead")
public static function purgeCache():Void public static function purgeCache():Void
{ {
FunkinMemory.purgeCache(); // Everything that is in previousCachedTextures but not in currentCachedTextures should be destroyed.
for (graphicKey in previousCachedTextures.keys())
{
var graphic = previousCachedTextures.get(graphicKey);
if (graphic == null) continue;
FlxG.bitmap.remove(graphic);
graphic.destroy();
previousCachedTextures.remove(graphicKey);
}
@:privateAccess
if (FlxG.bitmap._cache == null)
{
@:privateAccess
FlxG.bitmap._cache = new Map();
System.gc();
return;
}
@:privateAccess
for (key in FlxG.bitmap._cache.keys())
{
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj == null) continue;
if (obj.persist) continue;
if (permanentCachedTextures.exists(key)) continue;
if (!(obj.useCount <= 0 || key.contains("characters") || key.contains("charSelect") || key.contains("results"))) continue;
FlxG.bitmap.removeKey(key);
obj.destroy();
}
openfl.Assets.cache.clear("songs");
openfl.Assets.cache.clear("sounds");
openfl.Assets.cache.clear("music");
System.gc();
} }
static function isGraphicCached(graphic:FlxGraphic):Bool static function isGraphicCached(graphic:FlxGraphic):Bool

View file

@ -1,6 +1,5 @@
package funkin.external.android; package funkin.mobile.external.android;
#if android
import lime.system.JNI; import lime.system.JNI;
import flixel.util.FlxSignal; import flixel.util.FlxSignal;
@ -10,6 +9,7 @@ import flixel.util.FlxSignal;
@:unreflective @:unreflective
class CallbackUtil class CallbackUtil
{ {
#if android
/** /**
* The result code for `DATA_FOLDER_CLOSED` activity. * The result code for `DATA_FOLDER_CLOSED` activity.
*/ */
@ -42,6 +42,7 @@ class CallbackUtil
initCallBackJNI(new CallbackHandler()); initCallBackJNI(new CallbackHandler());
} }
} }
#end
} }
/** /**
@ -49,7 +50,8 @@ class CallbackUtil
*/ */
class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end
{ {
@:allow(funkin.external.android.CallbackUtil) #if android
@:allow(funkin.mobile.external.android.CallbackUtil)
function new():Void {} function new():Void {}
/** /**
@ -66,5 +68,5 @@ class CallbackHandler #if (lime >= "8.0.0") implements JNISafety #end
{ {
if (CallbackUtil.onActivityResult != null) CallbackUtil.onActivityResult.dispatch(requestCode, resultCode); if (CallbackUtil.onActivityResult != null) CallbackUtil.onActivityResult.dispatch(requestCode, resultCode);
} }
#end
} }
#end

View file

@ -1,12 +1,12 @@
package funkin.external.android; package funkin.mobile.external.android;
#if android
/** /**
* A Utility class to manage the Application's Data folder on Android. * A Utility class to manage the Application's Data folder on Android.
*/ */
@:unreflective @:unreflective
class DataFolderUtil class DataFolderUtil
{ {
#if android
/** /**
* Opens the data folder on an Android device using JNI. * Opens the data folder on an Android device using JNI.
*/ */
@ -19,5 +19,5 @@ class DataFolderUtil
openDataFolderJNI(CallbackUtil.DATA_FOLDER_CLOSED); openDataFolderJNI(CallbackUtil.DATA_FOLDER_CLOSED);
} }
} }
#end
} }
#end

View file

@ -1,4 +1,4 @@
package funkin.external.android; package funkin.mobile.external.android;
#if android #if android
import lime.system.JNI; import lime.system.JNI;

View file

@ -1,6 +1,5 @@
package funkin.external.android; package funkin.mobile.external.android;
#if android
import lime.math.Rectangle; import lime.math.Rectangle;
import lime.system.JNI; import lime.system.JNI;
@ -10,6 +9,7 @@ import lime.system.JNI;
@:unreflective @:unreflective
class ScreenUtil class ScreenUtil
{ {
#if android
/** /**
* Retrieves the dimensions of display cutouts (such as notches) on Android devices. * Retrieves the dimensions of display cutouts (such as notches) on Android devices.
* *
@ -48,5 +48,5 @@ class ScreenUtil
return []; return [];
} }
#end
} }
#end

View file

@ -1,10 +1,9 @@
package funkin.external.ios; package funkin.mobile.external.ios;
#if ios
/** /**
* A Utility class to manage iOS audio. * A Utility class to manage iOS audio.
*/ */
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml')) @:build(funkin.mobile.macros.LinkerMacro.xml('project/Build.xml'))
@:include('AudioSession.hpp') @:include('AudioSession.hpp')
@:unreflective @:unreflective
extern class AudioSession extern class AudioSession
@ -14,4 +13,3 @@ extern class AudioSession
@:native('setActive') @:native('setActive')
static function setActive(active:Bool):Void; static function setActive(active:Bool):Void;
} }
#end

View file

@ -1,10 +1,9 @@
package funkin.external.ios; package funkin.mobile.external.ios;
#if ios
/** /**
* A Utility class to get iOS screen related informations. * A Utility class to get iOS screen related informations.
*/ */
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml')) @:build(funkin.mobile.macros.LinkerMacro.xml('project/Build.xml'))
@:include('ScreenUtil.hpp') @:include('ScreenUtil.hpp')
@:unreflective @:unreflective
extern class ScreenUtil extern class ScreenUtil
@ -15,4 +14,3 @@ extern class ScreenUtil
@:native('getScreenSize') @:native('getScreenSize')
static function getScreenSize(width:cpp.RawPointer<Float>, height:cpp.RawPointer<Float>):Void; static function getScreenSize(width:cpp.RawPointer<Float>, height:cpp.RawPointer<Float>):Void;
} }
#end

View file

@ -125,7 +125,7 @@ class ControlsHandler
@:noCompletion @:noCompletion
private static function get_hasExternalInputDevice():Bool private static function get_hasExternalInputDevice():Bool
{ {
return FlxG.gamepads.numActiveGamepads > 0 #if android || extension.androidtools.Tools.isChromebook() #end; return FlxG.gamepads.numActiveGamepads > 0;
} }
@:noCompletion @:noCompletion

View file

@ -0,0 +1,37 @@
package funkin.mobile.macros;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.xml.Printer;
import sys.FileSystem;
using haxe.io.Path;
/**
* This class provides a macro to include an XML build file in the metadata of a Haxe class.
* The file must be located relative to the directory of the Haxe class that uses this macro.
*/
@:nullSafety
class LinkerMacro
{
/**
* Adds an XML `<include>` element to the class's metadata, pointing to a specified build file.
* @param file_name The name of the XML file to include. Defaults to `Build.xml` if not provided.
* @return An array of fields that are processed during the build.
*/
public static macro function xml(?file_name:String = 'Build.xml'):Array<Field>
{
final pos:Position = Context.currentPos();
final sourcePath:String = FileSystem.absolutePath(Context.getPosInfos(pos).file.directory()).removeTrailingSlashes();
final fileToInclude:String = Path.join([sourcePath, file_name?.length > 0 ? file_name : 'Build.xml']);
if (!FileSystem.exists(fileToInclude)) Context.error('The specified file "$fileToInclude" could not be found at "$sourcePath".', pos);
final includeElement:Xml = Xml.createElement('include');
includeElement.set('name', fileToInclude);
Context.getLocalClass().get().meta.add(':buildXml', [
{expr: EConst(CString(Printer.print(includeElement, true))), pos: pos}], pos);
return Context.getBuildFields();
}
}

View file

@ -15,14 +15,7 @@ class FunkinBackButton extends FunkinButton
public var enabled:Bool = true; public var enabled:Bool = true;
public var confirming(get, never):Bool; var confirming:Bool = false;
function get_confirming():Bool
{
return _confirming;
}
var _confirming:Bool = false;
public var restingOpacity:Float; public var restingOpacity:Float;
@ -93,7 +86,7 @@ class FunkinBackButton extends FunkinButton
return; return;
} }
_confirming = true; confirming = true;
FlxTween.cancelTweensOf(this); FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.05, 0.5); HapticUtil.vibrate(0, 0.05, 0.5);
@ -105,7 +98,7 @@ class FunkinBackButton extends FunkinButton
animation.onFinish.addOnce(function(name:String) { animation.onFinish.addOnce(function(name:String) {
if (name != 'confirm') return; if (name != 'confirm') return;
_confirming = false; confirming = false;
held = false; held = false;
onConfirmEnd.dispatch(); onConfirmEnd.dispatch();
}); });
@ -134,7 +127,7 @@ class FunkinBackButton extends FunkinButton
onDown.removeAll(); onDown.removeAll();
onOut.removeAll(); onOut.removeAll();
_confirming = false; confirming = false;
held = false; held = false;
onUp.add(playConfirmAnim); onUp.add(playConfirmAnim);

View file

@ -12,16 +12,7 @@ class FunkinOptionsButton extends FunkinButton
public var onConfirmStart(default, null):FlxSignal = new FlxSignal(); public var onConfirmStart(default, null):FlxSignal = new FlxSignal();
public var onConfirmEnd(default, null):FlxSignal = new FlxSignal(); public var onConfirmEnd(default, null):FlxSignal = new FlxSignal();
public var enabled:Bool = true; var confirming:Bool = false;
public var confirming(get, never):Bool;
function get_confirming():Bool
{
return _confirming;
}
var _confirming:Bool = false;
var instant:Bool = false; var instant:Bool = false;
var held:Bool = false; var held:Bool = false;
@ -58,7 +49,7 @@ class FunkinOptionsButton extends FunkinButton
function playHoldAnim():Void function playHoldAnim():Void
{ {
if (confirming || held || !enabled) return; if (confirming || held) return;
held = true; held = true;
@ -79,7 +70,7 @@ class FunkinOptionsButton extends FunkinButton
return; return;
} }
_confirming = true; confirming = true;
FlxTween.cancelTweensOf(this); FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.05, 0.5); HapticUtil.vibrate(0, 0.05, 0.5);
@ -95,20 +86,17 @@ class FunkinOptionsButton extends FunkinButton
animation.onFinish.addOnce(function(name:String) { animation.onFinish.addOnce(function(name:String) {
if (name != 'confirm') return; if (name != 'confirm') return;
_confirming = false;
held = false;
onConfirmEnd.dispatch(); onConfirmEnd.dispatch();
}); });
} }
function playOutAnim():Void function playOutAnim():Void
{ {
if (confirming || !enabled) return; if (confirming) return;
FlxTween.cancelTweensOf(this); FlxTween.cancelTweensOf(this);
HapticUtil.vibrate(0, 0.01, 0.2); HapticUtil.vibrate(0, 0.01, 0.2);
animation.play('idle'); animation.play('idle');
held = false;
} }
public function resetCallbacks():Void public function resetCallbacks():Void
@ -117,7 +105,7 @@ class FunkinOptionsButton extends FunkinButton
onDown.removeAll(); onDown.removeAll();
onOut.removeAll(); onOut.removeAll();
_confirming = false; confirming = false;
held = false; held = false;
onUp.add(playConfirmAnim); onUp.add(playConfirmAnim);

View file

@ -270,7 +270,7 @@ class ControlsSchemeMenu extends MusicBeatSubState
*/ */
function setSelection(index:Int):Void function setSelection(index:Int):Void
{ {
final newIndex:Int = Math.floor(index.clamp(0, hitboxShowcases.length - 1)); final newIndex:Int = Math.floor(FlxMath.bound(index, 0, hitboxShowcases.length - 1));
if (currentIndex != newIndex) if (currentIndex != newIndex)
{ {
@ -348,7 +348,7 @@ class ControlsSchemeMenu extends MusicBeatSubState
hitboxShowcases.x = MathUtil.smoothLerpPrecision(hitboxShowcases.x, showcasesTargetX, elapsed, 0.5); hitboxShowcases.x = MathUtil.smoothLerpPrecision(hitboxShowcases.x, showcasesTargetX, elapsed, 0.5);
final minShowcasesX:Float = -1500 * availableSchemes.length; final minShowcasesX:Float = -1500 * availableSchemes.length;
hitboxShowcases.x = hitboxShowcases.x.clamp(minShowcasesX, 400); hitboxShowcases.x = FlxMath.bound(hitboxShowcases.x, minShowcasesX, 400);
final targetIndex:Int = Math.round(hitboxShowcases.x / -1500); final targetIndex:Int = Math.round(hitboxShowcases.x / -1500);
@ -356,9 +356,8 @@ class ControlsSchemeMenu extends MusicBeatSubState
} }
else else
{ {
hitboxShowcases.x = MathUtil.smoothLerpPrecision(hitboxShowcases.x, (-1500 * currentIndex) + (-1500 / (availableSchemes.length + 1) * currentIndex), hitboxShowcases.x = MathUtil.smoothLerpPrecision(hitboxShowcases.x, (-1500 * currentIndex) + (-1500 / (availableSchemes.length + 1) * currentIndex), elapsed, 0.5);
elapsed, 0.5);
}
} }
#end #end
}
} }

View file

@ -29,7 +29,7 @@ class AdMobUtil
/** /**
* AdMob publisher ID used for the application. * AdMob publisher ID used for the application.
*/ */
static final ADMOB_PUBLISHER:String = EnvironmentConfigMacro.environmentConfig.get("MOBILE_GLOBAL_ADMOB_PUBLISHER"); static final ADMOB_PUBLISHER:String = EnvironmentConfigMacro.environmentConfig.get("GLOBAL_ADMOB_PUBLISHER");
/** /**
* Test ad unit IDs for development and testing purposes. * Test ad unit IDs for development and testing purposes.

View file

@ -1,9 +1,9 @@
package funkin.mobile.util; package funkin.mobile.util;
#if ios #if ios
import funkin.external.ios.ScreenUtil as NativeScreenUtil; import funkin.mobile.external.ios.ScreenUtil as NativeScreenUtil;
#elseif android #elseif android
import funkin.external.android.ScreenUtil as NativeScreenUtil; import funkin.mobile.external.android.ScreenUtil as NativeScreenUtil;
#end #end
import lime.math.Rectangle; import lime.math.Rectangle;
import lime.system.System; import lime.system.System;

View file

@ -1,54 +0,0 @@
package funkin.modding;
import haxe.ds.StringMap;
/**
* Temporary persistent data storage for mods to use.
*/
@:nullSafety
class ModStore
{
/**
* All registered stores for this session.
*/
public static final stores:StringMap<Dynamic> = new StringMap<Dynamic>();
/**
* Attempts to register a new store with the given ID and return it.
* If a store with the same ID already exists, that store will be returned instead (discards `data`).
*
* @id The unique ID for this store.
* @data Optional initial data, uses an empty object by default.
* @return The store data at the given ID.
*/
public static function register(id:String, ?data:Dynamic):Dynamic
{
if (stores.exists(id)) return stores.get(id);
stores.set(id, data ??= {});
return data;
}
/**
* Helper function to get a store by ID.
*
* @id The target ID of the store.
* @return The store data, or `null` if the store did not exist.
*/
public static function get(id:String):Null<Dynamic>
{
return stores.get(id);
}
/**
* Helper function to remove a store by ID and return it.
*
* @id The target ID of the store.
* @return The store data, or `null` if the store did not exist.
*/
public static function remove(id:String):Null<Dynamic>
{
var data:Null<Dynamic> = stores.get(id);
stores.remove(id);
return data;
}
}

View file

@ -5,6 +5,19 @@ import polymod.Polymod;
@:nullSafety @:nullSafety
class PolymodErrorHandler class PolymodErrorHandler
{ {
/**
* Show a popup with the given text.
* This displays a system popup, it WILL interrupt the game.
* Make sure to only use this when it's important, like when there's a script error.
*
* @param name The name at the top of the popup.
* @param desc The body text of the popup.
*/
public static function showAlert(name:String, desc:String):Void
{
lime.app.Application.current.window.alert(desc, name);
}
public static function onPolymodError(error:PolymodError):Void public static function onPolymodError(error:PolymodError):Void
{ {
// Perform an action based on the error code. // Perform an action based on the error code.
@ -24,12 +37,12 @@ class PolymodErrorHandler
// A syntax error when parsing a script. // A syntax error when parsing a script.
logError(error.message); logError(error.message);
// Notify the user via popup. // Notify the user via popup.
funkin.util.WindowUtil.showError('Polymod Script Parsing Error', error.message); showAlert('Polymod Script Parsing Error', error.message);
case SCRIPT_RUNTIME_EXCEPTION: case SCRIPT_RUNTIME_EXCEPTION:
// A runtime error when running a script. // A runtime error when running a script.
logError(error.message); logError(error.message);
// Notify the user via popup. // Notify the user via popup.
funkin.util.WindowUtil.showError('Polymod Script Exception', error.message); showAlert('Polymod Script Exception', error.message);
case SCRIPT_CLASS_MODULE_NOT_FOUND: case SCRIPT_CLASS_MODULE_NOT_FOUND:
// A scripted class tried to reference an unknown class or module. // A scripted class tried to reference an unknown class or module.
logError(error.message); logError(error.message);
@ -41,12 +54,13 @@ class PolymodErrorHandler
msg += '\nCheck to ensure the class exists and is spelled correctly.'; msg += '\nCheck to ensure the class exists and is spelled correctly.';
// Notify the user via popup. // Notify the user via popup.
funkin.util.WindowUtil.showError('Polymod Script Import Error', msg); showAlert('Polymod Script Import Error', msg);
case SCRIPT_CLASS_MODULE_BLACKLISTED: case SCRIPT_CLASS_MODULE_BLACKLISTED:
// A scripted class tried to reference a blacklisted class or module. // A scripted class tried to reference a blacklisted class or module.
logError(error.message); logError(error.message);
// Notify the user via popup. // Notify the user via popup.
funkin.util.WindowUtil.showError('Polymod Script Blacklist Violation', error.message); showAlert('Polymod Script Blacklist Violation', error.message);
default: default:
// Log the message based on its severity. // Log the message based on its severity.
switch (error.severity) switch (error.severity)

View file

@ -530,10 +530,12 @@ class PolymodHandler
Polymod.clearScripts(); Polymod.clearScripts();
// Forcibly reload Polymod so it finds any new files. // Forcibly reload Polymod so it finds any new files.
// This will also register all scripts.
// TODO: Replace this with loadEnabledMods(). // TODO: Replace this with loadEnabledMods().
funkin.modding.PolymodHandler.loadAllMods(); funkin.modding.PolymodHandler.loadAllMods();
// Reload scripted classes so stages and modules will update.
Polymod.registerAllScriptClasses();
// Reload everything that is cached. // Reload everything that is cached.
// Currently this freezes the game for a second but I guess that's tolerable? // Currently this freezes the game for a second but I guess that's tolerable?

View file

@ -4,18 +4,6 @@ import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.IScriptedClass.IStateChangingScriptedClass; import funkin.modding.IScriptedClass.IStateChangingScriptedClass;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
/**
* Parameters used to initialize a module.
*/
typedef ModuleParams =
{
/**
* The state this module is associated with.
* If set, this module will only receive events when the game is in this state.
*/
?state:Class<Dynamic>
}
/** /**
* A module is a scripted class which receives all events without requiring a specific context. * A module is a scripted class which receives all events without requiring a specific context.
* You may have the module active at all times, or only when another script enables it. * You may have the module active at all times, or only when another script enables it.
@ -51,27 +39,16 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
return value; return value;
} }
/**
* The state this module is associated with.
* If set, this module will only receive events when the game is in this state.
*/
public var state:Null<Class<Dynamic>> = null;
/** /**
* Called when the module is initialized. * Called when the module is initialized.
* It may not be safe to reference other modules here since they may not be loaded yet. * It may not be safe to reference other modules here since they may not be loaded yet.
* *
* NOTE: To make the module start inactive, call `this.active = false` in the constructor. * NOTE: To make the module start inactive, call `this.active = false` in the constructor.
*/ */
public function new(moduleId:String, priority:Int = 1000, ?params:ModuleParams):Void public function new(moduleId:String, priority:Int = 1000):Void
{ {
this.moduleId = moduleId; this.moduleId = moduleId;
this.priority = priority; this.priority = priority;
if (params != null)
{
this.state = params.state ?? null;
}
} }
public function toString() public function toString()

View file

@ -6,7 +6,6 @@ import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.Module; import funkin.modding.module.Module;
import funkin.modding.module.ScriptedModule; import funkin.modding.module.ScriptedModule;
import flixel.FlxG;
/** /**
* Utility functions for loading and manipulating active modules. * Utility functions for loading and manipulating active modules.
@ -146,14 +145,6 @@ class ModuleHandler
// The module needs to be active to receive events. // The module needs to be active to receive events.
if (module != null && module.active) if (module != null && module.active)
{ {
if (module.state != null)
{
// Only call the event if the current state is what the module's state is.
if (!(Type.getClass(FlxG.state) == module.state) && !(Type.getClass(FlxG.state?.subState) == module.state))
{
continue;
}
}
ScriptEventDispatcher.callEvent(module, event); ScriptEventDispatcher.callEvent(module, event);
} }
} }

View file

@ -21,6 +21,7 @@ import funkin.util.MathUtil;
import funkin.effects.RetroCameraFade; import funkin.effects.RetroCameraFade;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import funkin.util.TouchUtil; import funkin.util.TouchUtil;
import openfl.utils.Assets;
#if FEATURE_MOBILE_ADVERTISEMENTS #if FEATURE_MOBILE_ADVERTISEMENTS
import funkin.mobile.util.AdMobUtil; import funkin.mobile.util.AdMobUtil;
#end #end
@ -101,6 +102,18 @@ class GameOverSubState extends MusicBeatSubState
var canInput:Bool = false; var canInput:Bool = false;
var justDied:Bool = true;
var isSpecialAnimation:Bool = false;
var gameOverVibrationPreset:VibrationPreset =
{
period: 0,
duration: Constants.DEFAULT_VIBRATION_DURATION,
amplitude: Constants.MIN_VIBRATION_AMPLITUDE,
sharpness: Constants.DEFAULT_VIBRATION_SHARPNESS
};
public function new(params:GameOverParams) public function new(params:GameOverParams)
{ {
super(); super();
@ -162,7 +175,7 @@ class GameOverSubState extends MusicBeatSubState
if ((parentPlayState?.isMinimalMode ?? true)) {} if ((parentPlayState?.isMinimalMode ?? true)) {}
else else
{ {
boyfriend = parentPlayState?.currentStage?.getBoyfriend(true); boyfriend = parentPlayState?.currentStage.getBoyfriend(true);
if (boyfriend != null) if (boyfriend != null)
{ {
boyfriend.canPlayOtherAnims = true; boyfriend.canPlayOtherAnims = true;
@ -185,17 +198,16 @@ class GameOverSubState extends MusicBeatSubState
addBackButton(FlxG.width - 230, FlxG.height - 200, FlxColor.WHITE, goBack); addBackButton(FlxG.width - 230, FlxG.height - 200, FlxColor.WHITE, goBack);
#end #end
HapticUtil.vibrate(0, Constants.DEFAULT_VIBRATION_DURATION);
// Allow input a second later to prevent accidental gameover skips. // Allow input a second later to prevent accidental gameover skips.
new FlxTimer().start(1, function(tmr:FlxTimer) { new FlxTimer().start(1, function(tmr:FlxTimer) {
canInput = true; canInput = true;
}); });
} }
@:nullSafety(Off)
function setCameraTarget():Void function setCameraTarget():Void
{ {
if (parentPlayState == null || parentPlayState.isMinimalMode || boyfriend == null) return; if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) return;
// Assign a camera follow point to the boyfriend's position. // Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(parentPlayState.cameraFollowPoint.x, parentPlayState.cameraFollowPoint.y, 1, 1); cameraFollowPoint = new FlxObject(parentPlayState.cameraFollowPoint.x, parentPlayState.cameraFollowPoint.y, 1, 1);
@ -206,7 +218,6 @@ class GameOverSubState extends MusicBeatSubState
cameraFollowPoint.y += offsets[1]; cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint); add(cameraFollowPoint);
@:nullSafety(Off)
FlxG.camera.target = null; FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2); FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2);
targetCameraZoom = (parentPlayState?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom(); targetCameraZoom = (parentPlayState?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom();
@ -319,6 +330,9 @@ class GameOverSubState extends MusicBeatSubState
} }
} }
// Handle vibrations on update.
if (HapticUtil.hapticsAvailable) handleAnimationVibrations();
// Start death music before firstDeath gets replaced // Start death music before firstDeath gets replaced
super.update(elapsed); super.update(elapsed);
} }
@ -394,7 +408,7 @@ class GameOverSubState extends MusicBeatSubState
// Readd Boyfriend to the stage. // Readd Boyfriend to the stage.
boyfriend.isDead = false; boyfriend.isDead = false;
remove(boyfriend); remove(boyfriend);
parentPlayState?.currentStage?.addCharacter(boyfriend, BF); parentPlayState?.currentStage.addCharacter(boyfriend, BF);
} }
// Snap reset the camera which may have changed because of the player character data. // Snap reset the camera which may have changed because of the player character data.
@ -440,7 +454,7 @@ class GameOverSubState extends MusicBeatSubState
#else #else
resetPlaying(); resetPlaying();
#end #end
}, true); });
} }
}); });
} }
@ -564,11 +578,11 @@ class GameOverSubState extends MusicBeatSubState
PlayStatePlaylist.reset(); PlayStatePlaylist.reset();
} }
var stickerPackId:Null<String> = parentPlayState?.currentChart?.stickerPack; var stickerPackId:Null<String> = parentPlayState?.currentChart.stickerPack;
if (stickerPackId == null) if (stickerPackId == null)
{ {
var playerCharacterId:Null<String> = PlayerRegistry.instance.getCharacterOwnerId(parentPlayState?.currentChart?.characters.player); var playerCharacterId:Null<String> = PlayerRegistry.instance.getCharacterOwnerId(parentPlayState?.currentChart.characters.player);
var playerCharacter:Null<PlayableCharacter> = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER); var playerCharacter:Null<PlayableCharacter> = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER);
if (playerCharacter != null) if (playerCharacter != null)
@ -601,6 +615,108 @@ class GameOverSubState extends MusicBeatSubState
var hasPlayedDeathQuote:Bool = false; var hasPlayedDeathQuote:Bool = false;
/**
* Used for death haptics.
*/
var startedTimerHaptics:Bool = false;
/**
* Unique vibrations for each death animation.
*/
function handleAnimationVibrations():Void
{
if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) return;
if (justDied)
{
if (isSpecialAnimation)
{
HapticUtil.vibrate(0, Constants.DEFAULT_VIBRATION_DURATION * 5);
trace("It's a special game over animation.");
}
else
{
HapticUtil.vibrate(0, Constants.DEFAULT_VIBRATION_DURATION);
}
justDied = false;
}
if (boyfriend.animation == null) return;
final curFrame:Int = (boyfriend.animation.curAnim != null) ? boyfriend.animation.curAnim.curFrame : -1;
if (boyfriend.characterId.startsWith("bf"))
{
// BF's mic drops.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && curFrame == 27)
{
HapticUtil.vibrateByPreset(gameOverVibrationPreset);
}
// BF's balls pulsating.
if (boyfriend.getCurrentAnimation().startsWith('deathLoop') && (curFrame == 0 || curFrame == 18))
{
HapticUtil.vibrateByPreset(gameOverVibrationPreset);
}
return;
}
// Pico dies because of Darnell beating him up.
if (boyfriend.characterId == "pico-blazin")
{
if (!startedTimerHaptics)
{
startedTimerHaptics = true;
new FlxTimer().start(0.5, function(tmr:FlxTimer) {
// Pico falls on his knees.
HapticUtil.vibrateByPreset(gameOverVibrationPreset);
new FlxTimer().start(0.6, function(tmr:FlxTimer) {
// Pico falls "asleep". :)
HapticUtil.vibrateByPreset(gameOverVibrationPreset);
});
});
return;
}
}
else if (boyfriend.characterId.startsWith("pico") && boyfriend.characterId != "pico-holding-nene")
{
if (isSpecialAnimation)
{
if (startedTimerHaptics) return;
startedTimerHaptics = true;
// Death by Darnell's can.
new FlxTimer().start(1.85, function(tmr:FlxTimer) {
// Pico falls on his knees.
HapticUtil.vibrateByPreset(gameOverVibrationPreset);
});
}
else
{
// Pico falls on his back.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && curFrame == 20)
{
HapticUtil.vibrateByPreset(gameOverVibrationPreset);
}
// Blood firework woohoo!!!!
if (boyfriend.getCurrentAnimation().startsWith('deathLoop') && curFrame % 2 == 0)
{
final randomAmplitude:Float = FlxG.random.float(Constants.MIN_VIBRATION_AMPLITUDE / 100, Constants.MIN_VIBRATION_AMPLITUDE);
final randomDuration:Float = FlxG.random.float(Constants.DEFAULT_VIBRATION_DURATION / 10, Constants.DEFAULT_VIBRATION_DURATION);
HapticUtil.vibrate(0, randomDuration, randomAmplitude);
}
}
return;
}
}
public override function destroy():Void public override function destroy():Void
{ {
super.destroy(); super.destroy();

View file

@ -18,9 +18,9 @@ class GitarooPause extends MusicBeatState
var replaySelect:Bool = false; var replaySelect:Bool = false;
var previousParams:Null<PlayStateParams>; var previousParams:PlayStateParams;
public function new(?previousParams:PlayStateParams):Void public function new(previousParams:PlayStateParams):Void
{ {
super(); super();

View file

@ -213,11 +213,6 @@ class PauseSubState extends MusicBeatSubState
*/ */
var menuEntryText:FlxTypedSpriteGroup<AtlasText>; var menuEntryText:FlxTypedSpriteGroup<AtlasText>;
/**
* Callback that gets called once substate gets open.
*/
var onPause:Void->Void;
// =============== // ===============
// Audio Variables // Audio Variables
// =============== // ===============
@ -227,11 +222,10 @@ class PauseSubState extends MusicBeatSubState
// Constructor // Constructor
// =============== // ===============
public function new(?params:PauseSubStateParams, ?onPause:Void->Void) public function new(?params:PauseSubStateParams)
{ {
super(); super();
this.currentMode = params?.mode ?? Standard; this.currentMode = params?.mode ?? Standard;
this.onPause = onPause;
} }
// =============== // ===============
@ -250,8 +244,6 @@ class PauseSubState extends MusicBeatSubState
AdMobUtil.addBanner(extension.admob.AdmobBannerSize.BANNER, extension.admob.AdmobBannerAlign.TOP_LEFT); AdMobUtil.addBanner(extension.admob.AdmobBannerSize.BANNER, extension.admob.AdmobBannerAlign.TOP_LEFT);
#end #end
if (onPause != null) onPause();
super.create(); super.create();
startPauseMusic(); startPauseMusic();
@ -294,7 +286,6 @@ class PauseSubState extends MusicBeatSubState
hapticTimer.cancel(); hapticTimer.cancel();
hapticTimer = null; hapticTimer = null;
pauseMusic.stop(); pauseMusic.stop();
onPause = null;
} }
// =============== // ===============

File diff suppressed because it is too large Load diff

View file

@ -527,7 +527,8 @@ class ResultState extends MusicBeatSubState
bgFlash.visible = true; bgFlash.visible = true;
FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24); FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24);
// NOTE: Only divide if totalNotes > 0 to prevent divide-by-zero errors. // NOTE: Only divide if totalNotes > 0 to prevent divide-by-zero errors.
var clearPercentFloat = params.scoreData.tallies.totalNotes == 0 ? 0.0 : Scoring.tallyCompletion(params.scoreData.tallies) * 100; var clearPercentFloat = params.scoreData.tallies.totalNotes == 0 ? 0.0 : (params.scoreData.tallies.sick + params.scoreData.tallies.good
- params.scoreData.tallies.missed) / params.scoreData.tallies.totalNotes * 100;
clearPercentTarget = Math.floor(clearPercentFloat); clearPercentTarget = Math.floor(clearPercentFloat);
// Prevent off-by-one errors. // Prevent off-by-one errors.
@ -743,7 +744,6 @@ class ResultState extends MusicBeatSubState
super.draw(); super.draw();
songName.clipRect = FlxRect.get(Math.max(0, 520 - songName.x), 0, FlxG.width, songName.height); songName.clipRect = FlxRect.get(Math.max(0, 520 - songName.x), 0, FlxG.width, songName.height);
clearPercentSmall.forEachAlive(spr -> spr.clipRect = FlxRect.get(Math.max(0, 520 - spr.x), 0, FlxG.width, spr.height));
// PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!! // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!!
@ -751,6 +751,105 @@ class ResultState extends MusicBeatSubState
// maskShaderSongName.frameUV = songName.frame.uv; // maskShaderSongName.frameUV = songName.frame.uv;
} }
private function handleAnimationVibrations()
{
for (atlas in characterAtlasAnimations)
{
if (atlas == null || atlas.sprite == null) continue;
switch (rank)
{
case ScoringRank.PERFECT | ScoringRank.PERFECT_GOLD:
switch (playerCharacterId)
{
// Feel the bed fun :freaky:
case "bf":
if (atlas.sprite.anim.curFrame > 87 && atlas.sprite.anim.curFrame % 5 == 0)
{
HapticUtil.vibrate(0, 0.01, Constants.MAX_VIBRATION_AMPLITUDE);
break;
}
// GF slams into the wall.
if (atlas.sprite.anim.curFrame == 51)
{
HapticUtil.vibrate(0, 0.01, (Constants.MAX_VIBRATION_AMPLITUDE / 3) * 2.5);
break;
}
// Pico drop-kicking Nene.
case "pico":
if (atlas.sprite.anim.curFrame == 52)
{
HapticUtil.vibrate(Constants.DEFAULT_VIBRATION_PERIOD, Constants.DEFAULT_VIBRATION_DURATION * 5, Constants.MAX_VIBRATION_AMPLITUDE);
break;
}
default:
break;
}
case ScoringRank.GREAT | ScoringRank.EXCELLENT:
switch (playerCharacterId)
{
// Pico explodes the targets with a rocket launcher.
case "pico":
// Pico shoots.
if (atlas.sprite.anim.curFrame == 45)
{
HapticUtil.vibrate(0, 0.01, (Constants.MAX_VIBRATION_AMPLITUDE / 3) * 2.5);
break;
}
// The targets explode.
if (atlas.sprite.anim.curFrame == 50)
{
HapticUtil.vibrate(Constants.DEFAULT_VIBRATION_PERIOD, Constants.DEFAULT_VIBRATION_DURATION, Constants.MAX_VIBRATION_AMPLITUDE);
break;
}
default:
break;
}
case ScoringRank.GOOD:
switch (playerCharacterId)
{
// Pico shooting the targets.
case "pico":
if (atlas.sprite.anim.curFrame % 2 != 0) continue;
final frames:Array<Array<Int>> = [[40, 50], [80, 90], [140, 157]];
for (i in 0...frames.length)
{
if (atlas.sprite.anim.curFrame < frames[i][0] || atlas.sprite.anim.curFrame > frames[i][1]) continue;
HapticUtil.vibrate(0, 0.01, Constants.MAX_VIBRATION_AMPLITUDE);
break;
}
default:
break;
}
case ScoringRank.SHIT:
switch (playerCharacterId)
{
// BF falling and GF slams on BF with her ass.
case "bf":
if (atlas.sprite.anim.curFrame == 5 || atlas.sprite.anim.curFrame == 90)
{
HapticUtil.vibrate(Constants.DEFAULT_VIBRATION_PERIOD * 2, Constants.DEFAULT_VIBRATION_DURATION * 2, Constants.MAX_VIBRATION_AMPLITUDE);
break;
}
default:
break;
}
}
}
}
override function update(elapsed:Float):Void override function update(elapsed:Float):Void
{ {
maskShaderDifficulty.swagSprX = difficulty.x; maskShaderDifficulty.swagSprX = difficulty.x;
@ -936,6 +1035,8 @@ class ResultState extends MusicBeatSubState
#end #end
} }
if (HapticUtil.hapticsAvailable) handleAnimationVibrations();
super.update(elapsed); super.update(elapsed);
} }

View file

@ -580,7 +580,7 @@ class AnimateAtlasCharacter extends BaseCharacter
override function set_alpha(value:Float):Float override function set_alpha(value:Float):Float
{ {
value = value.clamp(0, 1); value = FlxMath.bound(value, 0, 1);
if (exists && alpha != value) if (exists && alpha != value)
{ {

View file

@ -288,23 +288,35 @@ class CharacterDataParser
{ {
var charPath:String = "freeplay/icons/"; var charPath:String = "freeplay/icons/";
final charIDParts:Array<String> = char.split("-"); // FunkinCrew please dont skin me alive for copying pixelated icon and changing it a tiny bit
var iconName:String = ""; switch (char)
var lastValidIconName:String = "";
for (i in 0...charIDParts.length)
{ {
iconName += charIDParts[i]; case "bf-christmas" | "bf-car" | "bf-pixel" | "bf-holding-gf" | "bf-dark":
charPath += "bfpixel";
if (Assets.exists(Paths.image(charPath + '${iconName}pixel'))) case "monster-christmas":
{ charPath += "monsterpixel";
lastValidIconName = iconName; case "mom" | "mom-car":
charPath += "mommypixel";
case "pico-blazin" | "pico-playable" | "pico-speaker" | "pico-pixel" | "pico-holding-nene":
charPath += "picopixel";
case "gf-christmas" | "gf-car" | "gf-pixel" | "gf-tankmen" | "gf-dark":
charPath += "gfpixel";
case "dad":
charPath += "dadpixel";
case "darnell-blazin":
charPath += "darnellpixel";
case "senpai-angry":
charPath += "senpaipixel";
case "spooky-dark":
charPath += "spookypixel";
case "tankman-atlas" | "tankman-bloody":
charPath += "tankmanpixel";
case "pico-christmas" | "pico-dark":
charPath += "picopixel";
default:
charPath += '${char}pixel';
} }
if (i < charIDParts.length - 1) iconName += '-';
}
charPath += '${lastValidIconName}pixel';
if (!Assets.exists(Paths.image(charPath))) if (!Assets.exists(Paths.image(charPath)))
{ {
trace('[WARN] Character ${char} has no freeplay icon.'); trace('[WARN] Character ${char} has no freeplay icon.');

View file

@ -94,7 +94,6 @@ class MultiSparrowCharacter extends BaseCharacter
{ {
trace('Concatenating multi-sparrow atlas: ${asset}'); trace('Concatenating multi-sparrow atlas: ${asset}');
subTexture.parent.destroyOnNoUse = false; subTexture.parent.destroyOnNoUse = false;
FunkinMemory.cacheTexture(Paths.image(asset));
} }
texture.addAtlas(subTexture); texture.addAtlas(subTexture);

View file

@ -115,8 +115,6 @@ class HealthIcon extends FunkinSprite
*/ */
static final POSITION_OFFSET:Int = 26; static final POSITION_OFFSET:Int = 26;
public var iconOffset:FlxPoint = FlxPoint.get();
public function new(char:Null<String>, playerId:Int = 0) public function new(char:Null<String>, playerId:Int = 0)
{ {
super(0, 0); super(0, 0);
@ -153,12 +151,10 @@ class HealthIcon extends FunkinSprite
*/ */
public function toggleOldIcon():Void public function toggleOldIcon():Void
{ {
final playState:Null<PlayState> = PlayState.instance;
if (playState == null || playState.currentStage == null) return;
if (characterId == 'bf-old') if (characterId == 'bf-old')
{ {
isPixel = playState.currentStage.getBoyfriend()?.isPixel ?? false; isPixel = PlayState.instance.currentStage.getBoyfriend().isPixel;
playState.currentStage.getBoyfriend()?.initHealthIcon(false); PlayState.instance.currentStage.getBoyfriend().initHealthIcon(false);
} }
else else
{ {
@ -184,7 +180,8 @@ class HealthIcon extends FunkinSprite
loadCharacter(characterId); loadCharacter(characterId);
this.size.set(1.0, 1.0); this.size.set(1.0, 1.0);
this.iconOffset.set(); this.offset.x = 0.0;
this.offset.y = 0.0;
this.flipX = false; this.flipX = false;
} }
else else
@ -195,15 +192,8 @@ class HealthIcon extends FunkinSprite
loadCharacter(characterId); loadCharacter(characterId);
this.size.set(data.scale ?? 1.0, data.scale ?? 1.0); this.size.set(data.scale ?? 1.0, data.scale ?? 1.0);
if (data.offsets != null && data.offsets.length == 2) this.offset.x = (data.offsets != null) ? data.offsets[0] : 0.0;
{ this.offset.y = (data.offsets != null) ? data.offsets[1] : 0.0;
this.iconOffset.set(data.offsets[0], data.offsets[1]);
}
else
{
this.iconOffset.set(0, 0);
}
this.flipX = data.flipX ?? false; // Face the OTHER way by default, since that is more common. this.flipX = data.flipX ?? false; // Face the OTHER way by default, since that is more common.
} }
} }
@ -299,8 +289,6 @@ class HealthIcon extends FunkinSprite
// Keep the icon centered vertically on the health bar. // Keep the icon centered vertically on the health bar.
this.y = PlayState.instance.healthBar.y - (this.height / 2); // - (PlayState.instance.healthBar.height / 2) this.y = PlayState.instance.healthBar.y - (this.height / 2); // - (PlayState.instance.healthBar.height / 2)
offset += iconOffset;
} }
} }

View file

@ -66,7 +66,7 @@ class VideoCutscene
if (!openfl.Assets.exists(filePath)) if (!openfl.Assets.exists(filePath))
{ {
// Display a popup. // Display a popup.
// funkin.util.WindowUtil.showError('Error playing video', 'Video file does not exist: ${filePath}'); // lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
// return; // return;
// TODO: After moving videos to their own library, // TODO: After moving videos to their own library,

View file

@ -186,7 +186,6 @@ class FocusCameraSongEvent extends SongEvent
name: 'duration', name: 'duration',
title: 'Duration', title: 'Duration',
defaultValue: 4.0, defaultValue: 4.0,
min: 0,
step: 0.5, step: 0.5,
type: SongEventFieldType.FLOAT, type: SongEventFieldType.FLOAT,
units: 'steps' units: 'steps'

View file

@ -108,7 +108,6 @@ class ScrollSpeedEvent extends SongEvent
name: 'scroll', name: 'scroll',
title: 'Target Value', title: 'Target Value',
defaultValue: 1.0, defaultValue: 1.0,
min: 0.1,
step: 0.1, step: 0.1,
type: SongEventFieldType.FLOAT, type: SongEventFieldType.FLOAT,
units: 'x' units: 'x'
@ -117,7 +116,6 @@ class ScrollSpeedEvent extends SongEvent
name: 'duration', name: 'duration',
title: 'Duration', title: 'Duration',
defaultValue: 4.0, defaultValue: 4.0,
min: 0,
step: 0.5, step: 0.5,
type: SongEventFieldType.FLOAT, type: SongEventFieldType.FLOAT,
units: 'steps' units: 'steps'

View file

@ -72,7 +72,6 @@ class SetCameraBopSongEvent extends SongEvent
name: 'intensity', name: 'intensity',
title: 'Intensity', title: 'Intensity',
defaultValue: 1.0, defaultValue: 1.0,
min: 0,
step: 0.1, step: 0.1,
type: SongEventFieldType.FLOAT, type: SongEventFieldType.FLOAT,
units: 'x' units: 'x'
@ -81,7 +80,6 @@ class SetCameraBopSongEvent extends SongEvent
name: 'rate', name: 'rate',
title: 'Rate', title: 'Rate',
defaultValue: 4, defaultValue: 4,
min: 0,
step: 1, step: 1,
type: SongEventFieldType.INTEGER, type: SongEventFieldType.INTEGER,
units: 'beats/zoom' units: 'beats/zoom'

View file

@ -90,7 +90,6 @@ class SetHealthIconSongEvent extends SongEvent
name: 'scale', name: 'scale',
title: 'Scale', title: 'Scale',
defaultValue: 1.0, defaultValue: 1.0,
min: 0,
type: SongEventFieldType.FLOAT, type: SongEventFieldType.FLOAT,
}, },
{ {

View file

@ -110,7 +110,6 @@ class ZoomCameraSongEvent extends SongEvent
name: 'zoom', name: 'zoom',
title: 'Zoom Level', title: 'Zoom Level',
defaultValue: 1.0, defaultValue: 1.0,
min: 0,
step: 0.05, step: 0.05,
type: SongEventFieldType.FLOAT, type: SongEventFieldType.FLOAT,
units: 'x' units: 'x'
@ -119,7 +118,6 @@ class ZoomCameraSongEvent extends SongEvent
name: 'duration', name: 'duration',
title: 'Duration', title: 'Duration',
defaultValue: 4.0, defaultValue: 4.0,
min: 0,
step: 0.5, step: 0.5,
type: SongEventFieldType.FLOAT, type: SongEventFieldType.FLOAT,
units: 'steps' units: 'steps'

View file

@ -10,9 +10,6 @@ class NoteSprite extends FunkinSprite
{ {
static final DIRECTION_COLORS:Array<String> = ['purple', 'blue', 'green', 'red']; static final DIRECTION_COLORS:Array<String> = ['purple', 'blue', 'green', 'red'];
/**
* The hold note sprite for this note.
*/
public var holdNoteSprite:SustainTrail; public var holdNoteSprite:SustainTrail;
var hsvShader:HSVShader; var hsvShader:HSVShader;
@ -98,23 +95,8 @@ class NoteSprite extends FunkinSprite
return this.direction; return this.direction;
} }
/**
* The note data associated with this note sprite.
* This is used to store the strum time, length, and other properties.
*/
public var noteData:SongNoteData; public var noteData:SongNoteData;
/**
* If this note kind is scoreable (i.e., counted towards score and accuracy)
* Only accessible in scripts
* Defaults to true
*/
public var scoreable:Bool = true;
/**
* Whether this note is a hold note.
* This is true if the length is greater than 0.
*/
public var isHoldNote(get, never):Bool; public var isHoldNote(get, never):Bool;
function get_isHoldNote():Bool function get_isHoldNote():Bool
@ -185,6 +167,8 @@ class NoteSprite extends FunkinSprite
{ {
noteStyle.buildNoteSprite(this); noteStyle.buildNoteSprite(this);
this.shader = hsvShader;
// `false` disables the update() function for performance. // `false` disables the update() function for performance.
this.active = noteStyle.isNoteAnimated(); this.active = noteStyle.isNoteAnimated();
} }
@ -237,13 +221,11 @@ class NoteSprite extends FunkinSprite
public function desaturate():Void public function desaturate():Void
{ {
this.hsvShader.saturation = 0.2; this.hsvShader.saturation = 0.2;
this.shader = this.hsvShader;
} }
public function setHue(hue:Float):Void public function setHue(hue:Float):Void
{ {
this.hsvShader.hue = hue; this.hsvShader.hue = hue;
if (hue != 1.0) this.shader = this.hsvShader;
} }
public override function revive():Void public override function revive():Void
@ -256,12 +238,7 @@ class NoteSprite extends FunkinSprite
this.hasBeenHit = false; this.hasBeenHit = false;
this.mayHit = false; this.mayHit = false;
this.hasMissed = false; this.hasMissed = false;
this.handledMiss = false;
this.holdNoteSprite = null;
// The hsvShader should only be applied when it's necessary.
// Otherwise, it should be turned off to keep note batching.
this.shader = null;
this.hsvShader.hue = 1.0; this.hsvShader.hue = 1.0;
this.hsvShader.saturation = 1.0; this.hsvShader.saturation = 1.0;
this.hsvShader.value = 1.0; this.hsvShader.value = 1.0;

View file

@ -17,7 +17,6 @@ import funkin.play.notes.NoteVibrationsHandler;
import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import funkin.util.GRhythmUtil; import funkin.util.GRhythmUtil;
import funkin.play.notes.notekind.NoteKind;
import funkin.play.notes.notekind.NoteKindManager; import funkin.play.notes.notekind.NoteKindManager;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
#if mobile #if mobile
@ -97,9 +96,9 @@ class Strumline extends FlxSpriteGroup
/** /**
* Reset the scroll speed to the current chart's scroll speed. * Reset the scroll speed to the current chart's scroll speed.
*/ */
public function resetScrollSpeed(?newScrollSpeed:Float):Void public function resetScrollSpeed():Void
{ {
scrollSpeed = newScrollSpeed ?? PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; scrollSpeed = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
} }
var _conductorInUse:Null<Conductor>; var _conductorInUse:Null<Conductor>;
@ -184,7 +183,7 @@ class Strumline extends FlxSpriteGroup
static final BACKGROUND_PAD:Int = 16; static final BACKGROUND_PAD:Int = 16;
public function new(noteStyle:NoteStyle, isPlayer:Bool, ?scrollSpeed:Float) public function new(noteStyle:NoteStyle, isPlayer:Bool)
{ {
super(); super();
@ -236,13 +235,14 @@ class Strumline extends FlxSpriteGroup
if (inArrowContorlSchemeMode && isPlayer) this.background.x -= 100; if (inArrowContorlSchemeMode && isPlayer) this.background.x -= 100;
#end #end
this.add(this.background); this.add(this.background);
strumlineScale = new FlxCallbackPoint(strumlineScaleCallback);
strumlineScale = new FlxCallbackPoint(strumlineScaleCallback); strumlineScale = new FlxCallbackPoint(strumlineScaleCallback);
this.refresh(); this.refresh();
this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>(); this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
resetScrollSpeed(scrollSpeed); resetScrollSpeed();
for (i in 0...KEY_COUNT) for (i in 0...KEY_COUNT)
{ {
@ -643,6 +643,8 @@ class Strumline extends FlxSpriteGroup
if (holdNote.cover != null && isPlayer) if (holdNote.cover != null && isPlayer)
{ {
holdNote.cover.playEnd(); holdNote.cover.playEnd();
trace("Sustain Note Splash Vibration");
} }
else if (holdNote.cover != null) else if (holdNote.cover != null)
{ {
@ -946,6 +948,7 @@ class Strumline extends FlxSpriteGroup
{ {
if (note == null) return; if (note == null) return;
note.visible = false; note.visible = false;
notes.remove(note, false);
note.kill(); note.kill();
if (note.holdNoteSprite != null) if (note.holdNoteSprite != null)
@ -1100,7 +1103,6 @@ class Strumline extends FlxSpriteGroup
if (noteSprite != null) if (noteSprite != null)
{ {
var noteKind:NoteKind = NoteKindManager.getNoteKind(note.kind);
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle; var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
noteSprite.setupNoteGraphic(noteKindStyle); noteSprite.setupNoteGraphic(noteKindStyle);
@ -1125,7 +1127,6 @@ class Strumline extends FlxSpriteGroup
noteSprite.x -= (noteSprite.width - Strumline.STRUMLINE_SIZE) / 2; // Center it noteSprite.x -= (noteSprite.width - Strumline.STRUMLINE_SIZE) / 2; // Center it
noteSprite.x -= NUDGE; noteSprite.x -= NUDGE;
noteSprite.y = -9999; noteSprite.y = -9999;
if (noteKind != null) noteSprite.scoreable = noteKind.scoreable;
} }
return noteSprite; return noteSprite;

View file

@ -283,7 +283,7 @@ class SustainTrail extends FlxSprite
return; return;
} }
var clipHeight:Float = sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0).clamp(0, graphicHeight); var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight);
if (clipHeight <= 0.1) if (clipHeight <= 0.1)
{ {
visible = false; visible = false;

View file

@ -28,13 +28,6 @@ class NoteKind implements INoteScriptedClass
*/ */
public var params:Array<NoteKindParam>; public var params:Array<NoteKindParam>;
/**
* If this note kind is scoreable (ie, counted towards score and accuracy)
* Only accessible in scripts
* Defaults to true
*/
public var scoreable:Bool = true;
public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array<NoteKindParam>) public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array<NoteKindParam>)
{ {
this.noteKind = noteKind; this.noteKind = noteKind;

View file

@ -11,21 +11,7 @@ import funkin.play.notes.notekind.NoteKind.NoteKindParam;
class NoteKindManager class NoteKindManager
{ {
/** static var noteKinds:Map<String, NoteKind> = [];
* A map of all note kinds, keyed by their name.
* This is used to retrieve note kinds by their name.
*/
public static var noteKinds:Map<String, NoteKind> = [];
/**
* Retrieve a note kind by its name.
* @param noteKind The name of the note kind.
* @return The note kind, or null if it doesn't exist.
*/
public static function getNoteKind(noteKind:String):Null<NoteKind>
{
return noteKinds.get(noteKind);
}
public static function loadScripts():Void public static function loadScripts():Void
{ {

View file

@ -1,7 +1,6 @@
package funkin.play.scoring; package funkin.play.scoring;
import funkin.save.Save.SaveScoreData; import funkin.save.Save.SaveScoreData;
import funkin.save.Save.SaveScoreTallyData;
/** /**
* Which system to use when scoring and judging notes. * Which system to use when scoring and judging notes.
@ -375,7 +374,8 @@ class Scoring
if (scoreData.tallies.totalNotes == 0) return null; if (scoreData.tallies.totalNotes == 0) return null;
// Perfect (Platinum) is a Sick Full Clear // Perfect (Platinum) is a Sick Full Clear
if (scoreData.tallies.sick == scoreData.tallies.totalNotes) var isPerfectGold = scoreData.tallies.sick == scoreData.tallies.totalNotes;
if (isPerfectGold)
{ {
return ScoringRank.PERFECT_GOLD; return ScoringRank.PERFECT_GOLD;
} }
@ -384,21 +384,21 @@ class Scoring
// Final Grade = (Sick + Good - Miss) / (Total Notes) // Final Grade = (Sick + Good - Miss) / (Total Notes)
var completionAmount:Float = Scoring.tallyCompletion(scoreData.tallies); var grade = (scoreData.tallies.sick + scoreData.tallies.good - scoreData.tallies.missed) / scoreData.tallies.totalNotes;
if (completionAmount == Constants.RANK_PERFECT_THRESHOLD) if (grade == Constants.RANK_PERFECT_THRESHOLD)
{ {
return ScoringRank.PERFECT; return ScoringRank.PERFECT;
} }
else if (completionAmount >= Constants.RANK_EXCELLENT_THRESHOLD) else if (grade >= Constants.RANK_EXCELLENT_THRESHOLD)
{ {
return ScoringRank.EXCELLENT; return ScoringRank.EXCELLENT;
} }
else if (completionAmount >= Constants.RANK_GREAT_THRESHOLD) else if (grade >= Constants.RANK_GREAT_THRESHOLD)
{ {
return ScoringRank.GREAT; return ScoringRank.GREAT;
} }
else if (completionAmount >= Constants.RANK_GOOD_THRESHOLD) else if (grade >= Constants.RANK_GOOD_THRESHOLD)
{ {
return ScoringRank.GOOD; return ScoringRank.GOOD;
} }
@ -407,21 +407,6 @@ class Scoring
return ScoringRank.SHIT; return ScoringRank.SHIT;
} }
} }
/**
* Calculates the "completion" of a song, based on how many GOOD and SICK notes were hit, minus how many were missed
* Top secret funkin crew patented algorithm
* TODO: Could possibly move more of the "tallying" related handling here.
* In FreeplayState we make sure it's clamped between 0 and 1, and we probably always want to assume that?
*
* @param tallies
* @return Float Completion, as a float value between 0 and 1. If `tallies` is `null`, we return 0;
*/
public static function tallyCompletion(?tallies:SaveScoreTallyData):Float
{
if (tallies == null) return 0.0;
return (tallies.sick + tallies.good - tallies.missed) / tallies.totalNotes;
}
} }
enum abstract ScoringRank(String) enum abstract ScoringRank(String)

View file

@ -11,7 +11,6 @@ import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme; import funkin.ui.debug.stageeditor.StageEditorState.StageEditorTheme;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.util.macro.ConsoleMacro;
import funkin.util.SerializerUtil; import funkin.util.SerializerUtil;
import funkin.mobile.ui.FunkinHitbox; import funkin.mobile.ui.FunkinHitbox;
import thx.semver.Version; import thx.semver.Version;
@ -21,7 +20,7 @@ import funkin.api.newgrounds.Leaderboards;
#end #end
@:nullSafety @:nullSafety
class Save implements ConsoleClass class Save
{ {
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.1.1"; public static final SAVE_DATA_VERSION:thx.semver.Version = "2.1.1";
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=2.1.0 <2.2.0"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = ">=2.1.0 <2.2.0";
@ -188,7 +187,6 @@ class Save implements ConsoleClass
theme: ChartEditorTheme.Light, theme: ChartEditorTheme.Light,
playtestStartTime: false, playtestStartTime: false,
downscroll: false, downscroll: false,
showNoteKinds: true,
metronomeVolume: 1.0, metronomeVolume: 1.0,
hitsoundVolumePlayer: 1.0, hitsoundVolumePlayer: 1.0,
hitsoundVolumeOpponent: 1.0, hitsoundVolumeOpponent: 1.0,
@ -360,23 +358,6 @@ class Save implements ConsoleClass
return data.optionsChartEditor.downscroll; return data.optionsChartEditor.downscroll;
} }
public var chartEditorShowNoteKinds(get, set):Bool;
function get_chartEditorShowNoteKinds():Bool
{
if (data.optionsChartEditor.showNoteKinds == null) data.optionsChartEditor.showNoteKinds = true;
return data.optionsChartEditor.showNoteKinds;
}
function set_chartEditorShowNoteKinds(value:Bool):Bool
{
// Set and apply.
data.optionsChartEditor.showNoteKinds = value;
flush();
return data.optionsChartEditor.showNoteKinds;
}
public var chartEditorPlaytestStartTime(get, set):Bool; public var chartEditorPlaytestStartTime(get, set):Bool;
function get_chartEditorPlaytestStartTime():Bool function get_chartEditorPlaytestStartTime():Bool
@ -901,12 +882,14 @@ class Save implements ConsoleClass
return; return;
} }
var newCompletion = (newScoreData.tallies.sick + newScoreData.tallies.good) / newScoreData.tallies.totalNotes;
var previousCompletion = (previousScoreData.tallies.sick + previousScoreData.tallies.good) / previousScoreData.tallies.totalNotes;
// Set the high score and the high rank separately. // Set the high score and the high rank separately.
var newScore:SaveScoreData = var newScore:SaveScoreData =
{ {
score: (previousScoreData.score > newScoreData.score) ? previousScoreData.score : newScoreData.score, score: (previousScoreData.score > newScoreData.score) ? previousScoreData.score : newScoreData.score,
tallies: (previousRank > newRank tallies: (previousRank > newRank || previousCompletion > newCompletion) ? previousScoreData.tallies : newScoreData.tallies
|| Scoring.tallyCompletion(previousScoreData.tallies) > Scoring.tallyCompletion(newScoreData.tallies)) ? previousScoreData.tallies : newScoreData.tallies
}; };
song.set(difficultyId, newScore); song.set(difficultyId, newScore);
@ -1221,7 +1204,7 @@ class Save implements ConsoleClass
{ {
var msg = 'There was an error loading your save data in slot ${slot}.'; var msg = 'There was an error loading your save data in slot ${slot}.';
msg += '\nPlease report this issue to the developers.'; msg += '\nPlease report this issue to the developers.';
funkin.util.WindowUtil.showError("Save Data Failure", msg); lime.app.Application.current.window.alert(msg, "Save Data Failure");
// Don't touch that slot anymore. // Don't touch that slot anymore.
// Instead, load the next available slot. // Instead, load the next available slot.
@ -1369,16 +1352,11 @@ class Save implements ConsoleClass
this.data.version = Save.SAVE_DATA_VERSION; this.data.version = Save.SAVE_DATA_VERSION;
} }
public function debug_dumpSaveJsonSave():Void public function debug_dumpSave():Void
{ {
FileUtil.saveFile(haxe.io.Bytes.ofString(this.serialize()), [FileUtil.FILE_FILTER_JSON], null, null, './save.json', 'Write save data as JSON...'); FileUtil.saveFile(haxe.io.Bytes.ofString(this.serialize()), [FileUtil.FILE_FILTER_JSON], null, null, './save.json', 'Write save data as JSON...');
} }
public function debug_dumpSaveJsonPrint():Void
{
trace(this.serialize());
}
#if FEATURE_NEWGROUNDS #if FEATURE_NEWGROUNDS
public static function saveToNewgrounds():Void public static function saveToNewgrounds():Void
{ {
@ -1413,7 +1391,7 @@ class Save implements ConsoleClass
var msg = 'There was an error loading your save data from Newgrounds.'; var msg = 'There was an error loading your save data from Newgrounds.';
msg += '\n${errorMsg}'; msg += '\n${errorMsg}';
msg += '\nAre you sure you are connected to the internet?'; msg += '\nAre you sure you are connected to the internet?';
funkin.util.WindowUtil.showError("Newgrounds Save Slot Failure", msg); lime.app.Application.current.window.alert(msg, "Newgrounds Save Slot Failure");
}); });
} }
#end #end
@ -1866,12 +1844,6 @@ typedef SaveDataChartEditorOptions =
*/ */
var ?downscroll:Bool; var ?downscroll:Bool;
/**
* Show Note Kind Indicator in the Chart Editor.
* @default `true`
*/
var ?showNoteKinds:Bool;
/** /**
* Metronome volume in the Chart Editor. * Metronome volume in the Chart Editor.
* @default `1.0` * @default `1.0`

View file

@ -41,7 +41,7 @@ class SaveDataMigrator
var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.'; var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.';
var slot:Int = Save.archiveBadSaveData(inputData); var slot:Int = Save.archiveBadSaveData(inputData);
var fullMessage:String = 'An error occurred migrating your save data.\n${message}\nInvalid data has been moved to save slot ${slot}.'; var fullMessage:String = 'An error occurred migrating your save data.\n${message}\nInvalid data has been moved to save slot ${slot}.';
funkin.util.WindowUtil.showError("Save Data Failure", fullMessage); lime.app.Application.current.window.alert(fullMessage, "Save Data Failure");
return new Save(Save.getDefault()); return new Save(Save.getDefault());
} }
} }

View file

@ -277,25 +277,25 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
public function accept():Void public function accept():Void
{ {
var menuItem:T = members[selectedIndex]; var selected = members[selectedIndex];
if (!menuItem.available) return; if (!selected.available) return;
onAcceptPress.dispatch(menuItem); onAcceptPress.dispatch(selected);
if (menuItem.fireInstantly) menuItem.callback(); if (selected.fireInstantly) selected.callback();
else else
{ {
busy = true; busy = true;
FunkinSound.playOnce(Paths.sound('confirmMenu')); FunkinSound.playOnce(Paths.sound('confirmMenu'));
FlxFlicker.flicker(menuItem, 1, 0.06, true, false, function(_) { FlxFlicker.flicker(selected, 1, 0.06, true, false, function(_) {
busy = false; busy = false;
menuItem.callback(); selected.callback();
}); });
} }
} }
public function cancelAccept():Void public function cancelAccept()
{ {
FlxFlicker.stopFlickering(members[selectedIndex]); FlxFlicker.stopFlickering(members[selectedIndex]);
busy = false; busy = false;
@ -327,22 +327,22 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
selectedIndex = index; selectedIndex = index;
var selectedMenuItem:T = members[selectedIndex]; var selected = members[selectedIndex];
selectedMenuItem.select(); selected.select();
onChange.dispatch(selectedMenuItem); onChange.dispatch(selected);
} }
public function has(name:String):Bool public function has(name:String)
{ {
return byName.exists(name); return byName.exists(name);
} }
public function getItem(name:String):Null<T> public function getItem(name:String)
{ {
return byName[name]; return byName[name];
} }
override function destroy():Void override function destroy()
{ {
super.destroy(); super.destroy();
byName.clear(); byName.clear();
@ -366,7 +366,7 @@ class MenuListItem extends FlxSprite
/** /**
* Set to true for things like opening URLs otherwise, it may it get blocked. * Set to true for things like opening URLs otherwise, it may it get blocked.
*/ */
public var fireInstantly:Bool = false; public var fireInstantly = false;
public var selected(get, never):Bool; public var selected(get, never):Bool;
@ -386,7 +386,7 @@ class MenuListItem extends FlxSprite
idle(); idle();
} }
function setData(name:String, ?callback:Void->Void, available:Bool):Void function setData(name:String, ?callback:Void->Void, available:Bool)
{ {
this.name = name; this.name = name;
@ -400,7 +400,7 @@ class MenuListItem extends FlxSprite
* @param name the label. * @param name the label.
* @param callback Unchanged if null. * @param callback Unchanged if null.
*/ */
public function setItem(name:String, ?callback:Void->Void):Void public function setItem(name:String, ?callback:Void->Void)
{ {
setData(name, callback, available); setData(name, callback, available);
@ -409,12 +409,12 @@ class MenuListItem extends FlxSprite
idle(); idle();
} }
public function idle():Void public function idle()
{ {
alpha = 0.6; alpha = 0.6;
} }
public function select():Void public function select()
{ {
alpha = 1.0; alpha = 1.0;
} }

View file

@ -12,7 +12,6 @@ import funkin.modding.PolymodHandler;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import funkin.util.WindowUtil;
import funkin.input.Controls; import funkin.input.Controls;
#if mobile #if mobile
import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinCamera;
@ -157,11 +156,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
function handleFunctionControls():Void function handleFunctionControls():Void
{ {
// Emergency exit button. // Emergency exit button.
if (FlxG.keys.justPressed.F4) if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
{
FlxG.switchState(() -> new MainMenuState());
WindowUtil.setWindowTitle('Friday Night Funkin\'');
}
} }
override function update(elapsed:Float) override function update(elapsed:Float)
@ -189,7 +184,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
{ {
// Both have an xPos of 0, but a width equal to the full screen. // Both have an xPos of 0, but a width equal to the full screen.
// The rightWatermarkText is right aligned, which puts the text in the correct spot. // The rightWatermarkText is right aligned, which puts the text in the correct spot.
// Their xPos is only changed when there's a notch on the device so it doesn't get covered by it. // Their xPos is only changed when there's a notch on the device so it doesn't get covered byt it.
leftWatermarkText = new FlxText(funkin.ui.FullScreenScaleMode.gameNotchSize.x, FlxG.height - 18, FlxG.width, '', 12); leftWatermarkText = new FlxText(funkin.ui.FullScreenScaleMode.gameNotchSize.x, FlxG.height - 18, FlxG.width, '', 12);
rightWatermarkText = new FlxText(-(funkin.ui.FullScreenScaleMode.gameNotchSize.x), FlxG.height - 18, FlxG.width, '', 12); rightWatermarkText = new FlxText(-(funkin.ui.FullScreenScaleMode.gameNotchSize.x), FlxG.height - 18, FlxG.width, '', 12);

View file

@ -10,7 +10,6 @@ import funkin.modding.IScriptedClass.IEventHandler;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
import funkin.modding.PolymodHandler; import funkin.modding.PolymodHandler;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import funkin.util.WindowUtil;
import flixel.util.FlxSort; import flixel.util.FlxSort;
import funkin.input.Controls; import funkin.input.Controls;
#if mobile #if mobile
@ -141,11 +140,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
super.update(elapsed); super.update(elapsed);
// Emergency exit button. // Emergency exit button.
if (FlxG.keys.justPressed.F4) if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
{
FlxG.switchState(() -> new MainMenuState());
WindowUtil.setWindowTitle('Friday Night Funkin\'');
}
// Display Conductor info in the watch window. // Display Conductor info in the watch window.
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);

View file

@ -21,24 +21,33 @@ class PixelatedIcon extends FlxFilteredSprite
{ {
var charPath:String = "freeplay/icons/"; var charPath:String = "freeplay/icons/";
final charIDParts:Array<String> = char.split("-"); switch (char)
var iconName:String = "";
var lastValidIconName:String = "";
for (i in 0...charIDParts.length)
{ {
iconName += charIDParts[i]; case "bf-christmas" | "bf-car" | "bf-pixel" | "bf-holding-gf":
charPath += "bfpixel";
if (Assets.exists(Paths.image(charPath + '${iconName}pixel'))) case "monster-christmas":
{ charPath += "monsterpixel";
lastValidIconName = iconName; case "mom" | "mom-car":
charPath += "mommypixel";
case "pico-blazin" | "pico-playable" | "pico-speaker" | "pico-pixel" | "pico-holding-nene":
charPath += "picopixel";
case "gf-christmas" | "gf-car" | "gf-pixel" | "gf-tankmen":
charPath += "gfpixel";
case "dad":
charPath += "dadpixel";
case "darnell-blazin":
charPath += "darnellpixel";
case "senpai-angry":
charPath += "senpaipixel";
case "spooky-dark":
charPath += "spookypixel";
case "tankman-atlas" | "tankman-bloody":
charPath += "tankmanpixel";
default:
charPath += '${char}pixel';
} }
if (i < charIDParts.length - 1) iconName += '-'; if (!openfl.utils.Assets.exists(Paths.image(charPath)))
}
charPath += '${lastValidIconName}pixel';
if (!Assets.exists(Paths.image(charPath)))
{ {
trace('[WARN] Character ${char} has no freeplay icon.'); trace('[WARN] Character ${char} has no freeplay icon.');
this.visible = false; this.visible = false;
@ -49,7 +58,7 @@ class PixelatedIcon extends FlxFilteredSprite
this.visible = true; this.visible = true;
} }
var isAnimated = Assets.exists(Paths.file('images/$charPath.xml')); var isAnimated = openfl.utils.Assets.exists(Paths.file('images/$charPath.xml'));
if (isAnimated) if (isAnimated)
{ {

View file

@ -1,100 +0,0 @@
package funkin.ui;
/**
* Simple state machine for UI components
* Replaces scattered boolean flags with clean state management
*/
enum UIState
{
Idle;
Interacting;
Entering;
Exiting;
Disabled;
}
/**
* Note: Not to be confust with FlxState or FlxSubState!
* State as in the design pattern!
* https://refactoring.guru/design-patterns/state
*/
@:nullSafety
class UIStateMachine
{
public var currentState(default, null):UIState = Idle;
public var previousState(default, null):UIState = Idle;
var validTransitions:Map<UIState, Array<UIState>>;
var onStateChange:Array<(UIState, UIState) -> Void> = [];
public function new(?transitions:Map<UIState, Array<UIState>>)
{
// Default valid transitions if none provided
validTransitions = transitions != null ? transitions : [
Idle => [Interacting, Entering, Exiting, Disabled],
Entering => [Idle, Exiting, Disabled, Interacting],
Interacting => [Idle, Entering, Exiting, Disabled],
Exiting => [Idle],
Disabled => [Idle]
];
}
public function canTransition(from:UIState, to:UIState):Bool
{
if (from != currentState) return false;
var allowedStates = validTransitions.get(from);
return allowedStates != null && allowedStates.contains(to);
}
public function transition(newState:UIState):Bool
{
// Allow same-state transitions (idempotent)
if (currentState == newState)
{
trace('State transition: ${currentState} -> ${newState} (no change)');
return true;
}
if (!canTransition(currentState, newState))
{
trace('Invalid state transition: ${currentState} -> ${newState}');
return false;
}
previousState = currentState;
currentState = newState;
trace('State transition: ${previousState} -> ${currentState}');
// Notify listeners
for (callback in onStateChange)
{
callback(previousState, currentState);
}
return true;
}
public function onStateChanged(callback:(UIState, UIState) -> Void):Void
{
onStateChange.push(callback);
}
public function reset():Void
{
previousState = currentState;
currentState = Idle;
}
public function is(state:UIState):Bool
{
return currentState == state;
}
public function canInteract():Bool
{
// Entering is an enabled state since we want to be able to interact even during the screen fade wipe thing
return currentState == Idle || currentState == Entering;
}
}

View file

@ -140,7 +140,7 @@ class CharSelectGF extends FlxAtlasSprite implements IBPMSyncedScriptedClass
alphaDiff /= 100; // flash exports alpha as a whole number alphaDiff /= 100; // flash exports alpha as a whole number
alpha += alphaDiff; alpha += alphaDiff;
alpha = alpha.clamp(0, 1); alpha = FlxMath.bound(alpha, 0, 1);
x += xDiff; x += xDiff;
y += yDiff; y += yDiff;

View file

@ -271,12 +271,6 @@ class CharSelectSubState extends MusicBeatSubState
nametag.midpointX += cutoutSize; nametag.midpointX += cutoutSize;
add(nametag); add(nametag);
@:privateAccess
{
nametag.midpointY += 200;
FlxTween.tween(nametag, {midpointY: nametag.midpointY - 200}, 1, {ease: FlxEase.expoOut});
}
nametag.scrollFactor.set(); nametag.scrollFactor.set();
FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSprite, ["x", "y", "alpha", "scale", "blend"])); FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSprite, ["x", "y", "alpha", "scale", "blend"]));
@ -745,7 +739,6 @@ class CharSelectSubState extends MusicBeatSubState
FlxTween.tween(cursorConfirmed, {alpha: 0}, 0.8, {ease: FlxEase.expoOut}); FlxTween.tween(cursorConfirmed, {alpha: 0}, 0.8, {ease: FlxEase.expoOut});
FlxTween.tween(barthing, {y: barthing.y + 80}, 0.8, {ease: FlxEase.backIn}); FlxTween.tween(barthing, {y: barthing.y + 80}, 0.8, {ease: FlxEase.backIn});
FlxTween.tween(nametag, {y: nametag.y + 80}, 0.8, {ease: FlxEase.backIn});
FlxTween.tween(dipshitBacking, {y: dipshitBacking.y + 210}, 0.8, {ease: FlxEase.backIn}); FlxTween.tween(dipshitBacking, {y: dipshitBacking.y + 210}, 0.8, {ease: FlxEase.backIn});
FlxTween.tween(chooseDipshit, {y: chooseDipshit.y + 200}, 0.8, {ease: FlxEase.backIn}); FlxTween.tween(chooseDipshit, {y: chooseDipshit.y + 200}, 0.8, {ease: FlxEase.backIn});
FlxTween.tween(dipshitBlur, {y: dipshitBlur.y + 220}, 0.8, {ease: FlxEase.backIn}); FlxTween.tween(dipshitBlur, {y: dipshitBlur.y + 220}, 0.8, {ease: FlxEase.backIn});

View file

@ -15,18 +15,17 @@ class CreditsDataHandler
static final CREDITS_DATA_PATH:String = "assets/data/credits.json"; static final CREDITS_DATA_PATH:String = "assets/data/credits.json";
#end #end
#if macro
public static function debugPrint(data:Null<CreditsData>):Void public static function debugPrint(data:Null<CreditsData>):Void
{ {
if (data == null) if (data == null)
{ {
Sys.println('[INFO] CreditsData(NULL)'); trace('CreditsData(NULL)');
return; return;
} }
if (data.entries == null || data.entries.length == 0) if (data.entries == null || data.entries.length == 0)
{ {
Sys.println('[INFO] CreditsData(EMPTY)'); trace('CreditsData(EMPTY)');
return; return;
} }
@ -37,9 +36,8 @@ class CreditsDataHandler
lineCount += entry?.body?.length ?? 0; lineCount += entry?.body?.length ?? 0;
} }
Sys.println('[INFO] CreditsData($entryCount entries containing $lineCount lines)'); trace('CreditsData($entryCount entries containing $lineCount lines)');
} }
#end
/** /**
* If for some reason the full credits won't load, * If for some reason the full credits won't load,

View file

@ -10,7 +10,7 @@ class CreditsDataMacro
public static macro function loadCreditsData():haxe.macro.Expr.ExprOf<CreditsData> public static macro function loadCreditsData():haxe.macro.Expr.ExprOf<CreditsData>
{ {
#if !display #if !display
Sys.println('[INFO] Hardcoding credits data...'); trace('Hardcoding credits data...');
var json = CreditsDataMacro.fetchJSON(); var json = CreditsDataMacro.fetchJSON();
if (json == null) if (json == null)

View file

@ -293,7 +293,6 @@ class CreditsState extends MusicBeatState
function exit():Void function exit():Void
{ {
FlxG.keys.enabled = false;
FlxG.switchState(() -> new MainMenuState()); FlxG.switchState(() -> new MainMenuState());
} }

View file

@ -4,7 +4,6 @@ import flixel.math.FlxPoint;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.ui.MusicBeatSubState; import funkin.ui.MusicBeatSubState;
import funkin.ui.FullScreenScaleMode;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import funkin.ui.TextMenuList; import funkin.ui.TextMenuList;
import funkin.ui.debug.charting.ChartEditorState; import funkin.ui.debug.charting.ChartEditorState;
@ -38,7 +37,7 @@ class DebugMenuSubState extends MusicBeatSubState
// Create the green background. // Create the green background.
var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat')); var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat'));
menuBG.color = 0xFF4CAF50; menuBG.color = 0xFF4CAF50;
menuBG.setGraphicSize(Std.int(menuBG.width * 1.1 * FullScreenScaleMode.wideScale.x)); menuBG.setGraphicSize(Std.int(menuBG.width * 1.1));
menuBG.updateHitbox(); menuBG.updateHitbox();
menuBG.screenCenter(); menuBG.screenCenter();
menuBG.scrollFactor.set(0, 0); menuBG.scrollFactor.set(0, 0);

View file

@ -100,7 +100,6 @@ class DebugBoundingState extends FlxState
offsetAnimationDropdown = offsetEditorDialog.findComponent("animationDropdown", DropDown); offsetAnimationDropdown = offsetEditorDialog.findComponent("animationDropdown", DropDown);
offsetEditorDialog.cameras = [hudCam]; offsetEditorDialog.cameras = [hudCam];
offsetEditorDialog.closable = false;
add(offsetEditorDialog); add(offsetEditorDialog);
offsetEditorDialog.showDialog(false); offsetEditorDialog.showDialog(false);

View file

@ -32,7 +32,6 @@ import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongData.NoteParamData; import funkin.data.song.SongData.NoteParamData;
import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils;
import funkin.data.song.SongNoteDataUtils; import funkin.data.song.SongNoteDataUtils;
import funkin.data.song.importer.ChartManifestData;
import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.input.Cursor; import funkin.input.Cursor;
@ -95,9 +94,6 @@ import haxe.ui.components.Button;
import haxe.ui.components.DropDown; import haxe.ui.components.DropDown;
import haxe.ui.components.Label; import haxe.ui.components.Label;
import haxe.ui.components.Slider; import haxe.ui.components.Slider;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.dialogs.CollapsibleDialog;
import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuBar;
@ -635,11 +631,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return isViewDownscroll; return isViewDownscroll;
} }
/**
* Whether to show an indicator if a note is of a non-default kind.
*/
var showNoteKindIndicators:Bool = false;
/** /**
* The current theme used by the editor. * The current theme used by the editor.
* Dictates the appearance of many UI elements. * Dictates the appearance of many UI elements.
@ -731,12 +722,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var hitsoundVolumeOpponent:Float = 1.0; var hitsoundVolumeOpponent:Float = 1.0;
/**
* The audio volume before it was toggled to zero.
* Metronome, hitsounds (player and enemy), instrumental, vocals (player and enemy)
*/
var previousAudioVolumes:Array<Float> = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
/** /**
* Whether hitsounds are enabled for at least one character. * Whether hitsounds are enabled for at least one character.
*/ */
@ -1256,24 +1241,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
// ============================== // ==============================
/**
* The song manifest data.
* If none already exists, it's intialized with the current song name in lower-kebab-case.
*/
var _songManifestData:Null<ChartManifestData> = null;
var songManifestData(get, set):ChartManifestData;
function get_songManifestData():ChartManifestData
{
if (_songManifestData != null) return _songManifestData;
return _songManifestData = new ChartManifestData(getDefaultSongId());
}
function set_songManifestData(value:ChartManifestData):ChartManifestData
{
return _songManifestData = value;
}
/** /**
* The song metadata. * The song metadata.
* - Keys are the variation IDs. At least one (`default`) must exist. * - Keys are the variation IDs. At least one (`default`) must exist.
@ -1555,14 +1522,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_currentSongId():String function get_currentSongId():String
{ {
return songManifestData.songId; return currentSongName.toLowerKebabCase().replace(' ', '-').sanitize();
}
function getDefaultSongId():String
{
var defaultSongId:String = currentSongName.trim().toLowerKebabCase().sanitize();
if (defaultSongId == '') defaultSongId = 'new-song';
return defaultSongId;
} }
var currentSongArtist(get, set):String; var currentSongArtist(get, set):String;
@ -1895,11 +1855,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/ */
var menubarItemDownscroll:MenuCheckBox; var menubarItemDownscroll:MenuCheckBox;
/**
* The `View -> Note Kind Indicator` menu item.
*/
var menubarItemViewIndicators:MenuCheckBox;
/** /**
* The `View -> Increase Difficulty` menu item. * The `View -> Increase Difficulty` menu item.
*/ */
@ -2403,7 +2358,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
noteSnapQuantIndex = save.chartEditorNoteQuant; noteSnapQuantIndex = save.chartEditorNoteQuant;
currentLiveInputStyle = save.chartEditorLiveInputStyle; currentLiveInputStyle = save.chartEditorLiveInputStyle;
isViewDownscroll = save.chartEditorDownscroll; isViewDownscroll = save.chartEditorDownscroll;
showNoteKindIndicators = save.chartEditorShowNoteKinds;
playtestStartTime = save.chartEditorPlaytestStartTime; playtestStartTime = save.chartEditorPlaytestStartTime;
currentTheme = save.chartEditorTheme; currentTheme = save.chartEditorTheme;
metronomeVolume = save.chartEditorMetronomeVolume; metronomeVolume = save.chartEditorMetronomeVolume;
@ -2414,7 +2368,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemVolumeInstrumental.value = Std.int(save.chartEditorInstVolume * 100); menubarItemVolumeInstrumental.value = Std.int(save.chartEditorInstVolume * 100);
menubarItemVolumeVocalsPlayer.value = Std.int(save.chartEditorPlayerVoiceVolume * 100); menubarItemVolumeVocalsPlayer.value = Std.int(save.chartEditorPlayerVoiceVolume * 100);
menubarItemVolumeVocalsOpponent.value = Std.int(save.chartEditorOpponentVoiceVolume * 100); menubarItemVolumeVocalsOpponent.value = Std.int(save.chartEditorOpponentVoiceVolume * 100);
menubarItemPlaybackSpeed.value = Std.int(save.chartEditorPlaybackSpeed * 100.0); menubarItemPlaybackSpeed.value = Std.int(save.chartEditorPlaybackSpeed * 100);
} }
public function writePreferences(hasBackup:Bool):Void public function writePreferences(hasBackup:Bool):Void
@ -2433,7 +2387,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
save.chartEditorNoteQuant = noteSnapQuantIndex; save.chartEditorNoteQuant = noteSnapQuantIndex;
save.chartEditorLiveInputStyle = currentLiveInputStyle; save.chartEditorLiveInputStyle = currentLiveInputStyle;
save.chartEditorDownscroll = isViewDownscroll; save.chartEditorDownscroll = isViewDownscroll;
save.chartEditorShowNoteKinds = showNoteKindIndicators;
save.chartEditorPlaytestStartTime = playtestStartTime; save.chartEditorPlaytestStartTime = playtestStartTime;
save.chartEditorTheme = currentTheme; save.chartEditorTheme = currentTheme;
save.chartEditorMetronomeVolume = metronomeVolume; save.chartEditorMetronomeVolume = metronomeVolume;
@ -2566,7 +2519,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(gridTiledSprite); add(gridTiledSprite);
gridTiledSprite.zIndex = 10; gridTiledSprite.zIndex = 10;
gridGhostNote = new ChartEditorNoteSprite(this, true); gridGhostNote = new ChartEditorNoteSprite(this);
gridGhostNote.alpha = 0.6; gridGhostNote.alpha = 0.6;
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "", []); gridGhostNote.noteData = new SongNoteData(0, 0, 0, "", []);
gridGhostNote.visible = false; gridGhostNote.visible = false;
@ -2853,14 +2806,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Setup character dropdowns. // Setup character dropdowns.
FlxMouseEvent.add(healthIconDad, function(_) { FlxMouseEvent.add(healthIconDad, function(_) {
if (!isCursorOverHaxeUI && this.subState == null) if (!isCursorOverHaxeUI)
{ {
this.openCharacterDropdown(CharacterType.DAD, true); this.openCharacterDropdown(CharacterType.DAD, true);
} }
}); });
FlxMouseEvent.add(healthIconBF, function(_) { FlxMouseEvent.add(healthIconBF, function(_) {
if (!isCursorOverHaxeUI && this.subState == null) if (!isCursorOverHaxeUI)
{ {
this.openCharacterDropdown(CharacterType.BF, true); this.openCharacterDropdown(CharacterType.BF, true);
} }
@ -3136,9 +3089,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemDownscroll.onClick = event -> isViewDownscroll = event.value; menubarItemDownscroll.onClick = event -> isViewDownscroll = event.value;
menubarItemDownscroll.selected = isViewDownscroll; menubarItemDownscroll.selected = isViewDownscroll;
menubarItemViewIndicators.onClick = event -> showNoteKindIndicators = menubarItemViewIndicators.selected;
menubarItemViewIndicators.selected = showNoteKindIndicators;
menubarItemDifficultyUp.onClick = _ -> incrementDifficulty(1); menubarItemDifficultyUp.onClick = _ -> incrementDifficulty(1);
menubarItemDifficultyDown.onClick = _ -> incrementDifficulty(-1); menubarItemDifficultyDown.onClick = _ -> incrementDifficulty(-1);
@ -3180,7 +3130,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarLabelVolumeMetronome.text = 'Metronome - ${Std.int(event.value)}%'; menubarLabelVolumeMetronome.text = 'Metronome - ${Std.int(event.value)}%';
}; };
menubarItemVolumeMetronome.value = Std.int(metronomeVolume * 100); menubarItemVolumeMetronome.value = Std.int(metronomeVolume * 100);
previousAudioVolumes[0] = Std.int(metronomeVolume * 100);
menubarItemThemeMusic.onChange = event -> { menubarItemThemeMusic.onChange = event -> {
this.welcomeMusic.active = event.value; this.welcomeMusic.active = event.value;
@ -3194,7 +3143,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarLabelVolumeHitsoundPlayer.text = 'Player - ${Std.int(event.value)}%'; menubarLabelVolumeHitsoundPlayer.text = 'Player - ${Std.int(event.value)}%';
}; };
menubarItemVolumeHitsoundPlayer.value = Std.int(hitsoundVolumePlayer * 100); menubarItemVolumeHitsoundPlayer.value = Std.int(hitsoundVolumePlayer * 100);
previousAudioVolumes[1] = Std.int(hitsoundVolumePlayer * 100);
menubarItemVolumeHitsoundOpponent.onChange = event -> { menubarItemVolumeHitsoundOpponent.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0; var volume:Float = event.value.toFloat() / 100.0;
@ -3202,33 +3150,29 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarLabelVolumeHitsoundOpponent.text = 'Enemy - ${Std.int(event.value)}%'; menubarLabelVolumeHitsoundOpponent.text = 'Enemy - ${Std.int(event.value)}%';
}; };
menubarItemVolumeHitsoundOpponent.value = Std.int(hitsoundVolumeOpponent * 100); menubarItemVolumeHitsoundOpponent.value = Std.int(hitsoundVolumeOpponent * 100);
previousAudioVolumes[2] = Std.int(hitsoundVolumeOpponent * 100);
menubarItemVolumeInstrumental.onChange = event -> { menubarItemVolumeInstrumental.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0; var volume:Float = event.value.toFloat() / 100.0;
if (audioInstTrack != null) audioInstTrack.volume = volume; if (audioInstTrack != null) audioInstTrack.volume = volume;
menubarLabelVolumeInstrumental.text = 'Instrumental - ${Std.int(event.value)}%'; menubarLabelVolumeInstrumental.text = 'Instrumental - ${Std.int(event.value)}%';
}; };
previousAudioVolumes[3] = menubarLabelVolumeInstrumental.value;
menubarItemVolumeVocalsPlayer.onChange = event -> { menubarItemVolumeVocalsPlayer.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0; var volume:Float = event.value.toFloat() / 100.0;
audioVocalTrackGroup.playerVolume = volume; audioVocalTrackGroup.playerVolume = volume;
menubarLabelVolumeVocalsPlayer.text = 'Player - ${Std.int(event.value)}%'; menubarLabelVolumeVocalsPlayer.text = 'Player - ${Std.int(event.value)}%';
}; };
previousAudioVolumes[4] = menubarLabelVolumeVocalsPlayer.value;
menubarItemVolumeVocalsOpponent.onChange = event -> { menubarItemVolumeVocalsOpponent.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0; var volume:Float = event.value.toFloat() / 100.0;
audioVocalTrackGroup.opponentVolume = volume; audioVocalTrackGroup.opponentVolume = volume;
menubarLabelVolumeVocalsOpponent.text = 'Enemy - ${Std.int(event.value)}%'; menubarLabelVolumeVocalsOpponent.text = 'Enemy - ${Std.int(event.value)}%';
}; };
previousAudioVolumes[5] = menubarLabelVolumeVocalsOpponent.value;
menubarItemPlaybackSpeed.onChange = event -> { menubarItemPlaybackSpeed.onChange = event -> {
var pitch:Float = (event.value.toFloat() * 2.0) / 100.0; var pitch:Float = (event.value.toFloat() * 2.0) / 100.0;
pitch = Math.round(pitch / 0.05) * 0.05; // Round to nearest 5% pitch = Math.floor(pitch / 0.05) * 0.05; // Round to nearest 5%
pitch = pitch.clamp(0.05, 2.0); // Clamp to 5% to 200% pitch = Math.max(0.05, Math.min(2.0, pitch)); // Clamp to 5% to 200%
#if FLX_PITCH #if FLX_PITCH
if (audioInstTrack != null) audioInstTrack.pitch = pitch; if (audioInstTrack != null) audioInstTrack.pitch = pitch;
audioVocalTrackGroup.pitch = pitch; audioVocalTrackGroup.pitch = pitch;
@ -3474,7 +3418,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
handleViewKeybinds(); handleViewKeybinds();
handleTestKeybinds(); handleTestKeybinds();
handleHelpKeybinds(); handleHelpKeybinds();
handleAudioKeybinds();
#if FEATURE_DEBUG_FUNCTIONS #if FEATURE_DEBUG_FUNCTIONS
handleQuickWatch(); handleQuickWatch();
@ -3978,9 +3921,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
selectionSquare.width = selectionSquare.height = GRID_SIZE; selectionSquare.width = selectionSquare.height = GRID_SIZE;
selectionSquare.color = FlxColor.RED; selectionSquare.color = FlxColor.RED;
} }
// Additional cleanup on notes.
if (noteTooltipsDirty) noteSprite.updateTooltipText();
} }
for (eventSprite in renderedEvents.members) for (eventSprite in renderedEvents.members)
@ -4684,17 +4624,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
var dragDistanceColumns:Int = dragTargetCurrentColumn; var dragDistanceColumns:Int = dragTargetCurrentColumn;
if (dragDistanceMs == 0 && dragDistanceColumns == 0)
{
// There's no need to move anything.
// Also prevents the selection boxes on notes from disappearing when they're 'moved' like this.
dragTargetNote = null;
dragTargetEvent = null;
dragTargetCurrentStep = 0;
dragTargetCurrentColumn = 0;
return;
}
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0) if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
{ {
// Both notes and events are selected. // Both notes and events are selected.
@ -4797,15 +4726,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
dragLengthCurrent = dragLengthSteps; dragLengthCurrent = dragLengthSteps;
} }
var sameHold:Bool = (gridGhostHoldNote.noteData == currentPlaceNoteData);
gridGhostHoldNote.visible = true; gridGhostHoldNote.visible = true;
gridGhostHoldNote.noteData = currentPlaceNoteData; gridGhostHoldNote.noteData = currentPlaceNoteData;
gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, sameHold); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle; gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
gridGhostHoldNote.updateHoldNoteGraphic();
} }
else else
{ {
@ -5672,24 +5598,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
@:nullSafety(Off) @:nullSafety(Off)
function quitChartEditor():Void function quitChartEditor():Void
{
if (saveDataDirty)
{
this.isHaxeUIDialogOpen = true;
var dialog = Dialogs.messageBox("You are about to leave the editor without saving.\n\nAre you sure?", "Leave Editor", MessageBoxType.TYPE_YESNO, true,
function(button:DialogButton) {
if (button == DialogButton.YES)
{ {
autoSave(); autoSave();
quitChartEditor();
}
});
return;
dialog.onDialogClosed = (_) -> this.isHaxeUIDialogOpen = false;
}
stopWelcomeMusic(); stopWelcomeMusic();
// TODO: PR Flixel to make onComplete nullable. // TODO: PR Flixel to make onComplete nullable.
if (audioInstTrack != null) audioInstTrack.onComplete = null; if (audioInstTrack != null) audioInstTrack.onComplete = null;
@ -5908,82 +5818,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
} }
/**
* Handle keybinds for audio playback.
*/
function handleAudioKeybinds():Void
{
// Metronome volume toggle
if (!isHaxeUIFocused && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.M)
{
// Changing values of the audio slider directly because it'll update the audio anyway and makes this code much cleaner, though more verbose.
var oldValue = previousAudioVolumes[0];
previousAudioVolumes[0] = menubarItemVolumeMetronome.value;
menubarItemVolumeMetronome.value = (menubarItemVolumeMetronome.value == 0) ? (oldValue > menubarItemVolumeMetronome.value) ? oldValue : 100 : 0;
}
// Hitsounds volume toggle
if (!isHaxeUIFocused && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.H)
{
var oldBFValue = previousAudioVolumes[1];
previousAudioVolumes[1] = menubarItemVolumeHitsoundPlayer.value;
var oldDadValue = previousAudioVolumes[2];
previousAudioVolumes[2] = menubarItemVolumeHitsoundOpponent.value;
if (oldBFValue <= menubarItemVolumeHitsoundPlayer.value || oldDadValue <= menubarItemVolumeHitsoundOpponent.value)
{
if (menubarItemVolumeHitsoundPlayer.value == 0 && menubarItemVolumeHitsoundOpponent.value == 0)
{
menubarItemVolumeHitsoundPlayer.value = (oldBFValue > menubarItemVolumeHitsoundPlayer.value) ? oldBFValue : 100;
menubarItemVolumeHitsoundOpponent.value = (oldDadValue > menubarItemVolumeHitsoundOpponent.value) ? oldDadValue : 100;
}
else
{
if (oldBFValue > menubarItemVolumeHitsoundPlayer.value) previousAudioVolumes[1] = oldBFValue;
if (oldDadValue > menubarItemVolumeHitsoundOpponent.value) previousAudioVolumes[2] = oldDadValue;
menubarItemVolumeHitsoundPlayer.value = 0;
menubarItemVolumeHitsoundOpponent.value = 0;
}
}
else
{
menubarItemVolumeHitsoundPlayer.value = (oldBFValue > menubarItemVolumeHitsoundPlayer.value) ? oldBFValue : 100;
menubarItemVolumeHitsoundOpponent.value = (oldDadValue > menubarItemVolumeHitsoundOpponent.value) ? oldDadValue : 100;
}
// menubarItemVolumeHitsoundPlayer.value = (menubarItemVolumeHitsoundPlayer.value == 0) ? (oldBFValue > menubarItemVolumeHitsoundPlayer.value) ? oldBFValue : 100 : 0;
// menubarItemVolumeHitsoundOpponent.value = (menubarItemVolumeHitsoundOpponent.value == 0) ? (oldDadValue > menubarItemVolumeHitsoundOpponent.value) ? oldDadValue : 100 : 0;
}
// Instrumental volume toggle
if (!isHaxeUIFocused && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.I)
{
var oldValue = previousAudioVolumes[3];
previousAudioVolumes[3] = menubarItemVolumeInstrumental.value;
menubarItemVolumeInstrumental.value = (menubarItemVolumeInstrumental.value == 0) ? (oldValue > menubarItemVolumeInstrumental.value) ? oldValue : 100 : 0;
}
// Vocals volume toggle
if (!isHaxeUIFocused && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.V)
{
var oldValue = previousAudioVolumes[4];
previousAudioVolumes[4] = menubarItemVolumeVocalsPlayer.value;
menubarItemVolumeVocalsPlayer.value = (menubarItemVolumeVocalsPlayer.value == 0) ? (oldValue > menubarItemVolumeVocalsPlayer.value) ? oldValue : 100 : 0;
oldValue = previousAudioVolumes[5];
previousAudioVolumes[5] = menubarItemVolumeVocalsOpponent.value;
menubarItemVolumeVocalsOpponent.value = (menubarItemVolumeVocalsOpponent.value == 0) ? (oldValue > menubarItemVolumeVocalsOpponent.value) ? oldValue : 100 : 0;
}
// Boyfriend vocals volume toggle
if (!isHaxeUIFocused && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.B)
{
var oldValue = previousAudioVolumes[4];
previousAudioVolumes[4] = menubarItemVolumeVocalsPlayer.value;
menubarItemVolumeVocalsPlayer.value = (menubarItemVolumeVocalsPlayer.value == 0) ? (oldValue > menubarItemVolumeVocalsPlayer.value) ? oldValue : 100 : 0;
}
// Dad vocals volume toggle
if (!isHaxeUIFocused && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.D)
{
var oldValue = previousAudioVolumes[5];
previousAudioVolumes[5] = menubarItemVolumeVocalsOpponent.value;
menubarItemVolumeVocalsOpponent.value = (menubarItemVolumeVocalsOpponent.value == 0) ? (oldValue > menubarItemVolumeVocalsOpponent.value) ? oldValue : 100 : 0;
}
}
function handleQuickWatch():Void function handleQuickWatch():Void
{ {
FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0); FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
@ -6025,9 +5859,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var startTimestamp:Float = 0; var startTimestamp:Float = 0;
if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0; var playbackRate:Float = ((menubarItemPlaybackSpeed.value ?? 1.0) * 2.0) / 100.0;
playbackRate = Math.round(playbackRate / 0.05) * 0.05; // Round to nearest 5% playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = playbackRate.clamp(0.05, 2.0); // Clamp to 5% to 200% playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
var targetSong:Song; var targetSong:Song;
try try
@ -6462,8 +6296,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
currentScrollEase = Math.max(0, targetScrollPosition); currentScrollEase = Math.max(0, targetScrollPosition);
currentScrollEase = Math.min(currentScrollEase, songLengthInPixels); currentScrollEase = Math.min(currentScrollEase, songLengthInPixels);
scrollPositionInPixels = MathUtil.snap(MathUtil.smoothLerpPrecision(scrollPositionInPixels, currentScrollEase, FlxG.elapsed, SCROLL_EASE_DURATION, scrollPositionInPixels = MathUtil.snap(MathUtil.smoothLerpPrecision(scrollPositionInPixels, currentScrollEase, FlxG.elapsed, SCROLL_EASE_DURATION, 1 / 1000), currentScrollEase, 1 / 1000);
1 / 1000), currentScrollEase, 1 / 1000);
moveSongToScrollPosition(); moveSongToScrollPosition();
} }
@ -6498,30 +6331,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION); fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION);
// Reapply the volume and playback rate. // Reapply the volume.
var instTargetVolume:Float = (menubarItemVolumeInstrumental.value / 100.0) ?? 1.0; var instTargetVolume:Float = menubarItemVolumeInstrumental.value / 100.0 ?? 1.0;
var vocalPlayerTargetVolume:Float = (menubarItemVolumeVocalsPlayer.value / 100.0) ?? 1.0; var vocalPlayerTargetVolume:Float = menubarItemVolumeVocalsPlayer.value / 100.0 ?? 1.0;
var vocalOpponentTargetVolume:Float = (menubarItemVolumeVocalsOpponent.value / 100.0) ?? 1.0; var vocalOpponentTargetVolume:Float = menubarItemVolumeVocalsOpponent.value / 100.0 ?? 1.0;
var playbackRate = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
playbackRate = Math.round(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = playbackRate.clamp(0.05, 2.0); // Clamp to 5% to 200%
if (audioInstTrack != null) if (audioInstTrack != null)
{ {
audioInstTrack.volume = instTargetVolume; audioInstTrack.volume = instTargetVolume;
#if FLX_PITCH
audioInstTrack.pitch = playbackRate;
#end
audioInstTrack.onComplete = null; audioInstTrack.onComplete = null;
} }
if (audioVocalTrackGroup != null) if (audioVocalTrackGroup != null)
{ {
audioVocalTrackGroup.playerVolume = vocalPlayerTargetVolume; audioVocalTrackGroup.playerVolume = vocalPlayerTargetVolume;
audioVocalTrackGroup.opponentVolume = vocalOpponentTargetVolume; audioVocalTrackGroup.opponentVolume = vocalOpponentTargetVolume;
#if FLX_PITCH
audioVocalTrackGroup.pitch = playbackRate;
#end
} }
} }
@ -6676,11 +6499,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
public function postLoadInstrumental():Void public function postLoadInstrumental():Void
{ {
// Reapply the volume and playback rate.
var instTargetVolume:Float = ((menubarItemVolumeInstrumental.value / 100) ?? 1.0);
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
playbackRate = Math.round(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = playbackRate.clamp(0.05, 2.0); // Clamp to 5% to 200%
if (audioInstTrack != null) if (audioInstTrack != null)
{ {
// Prevent the time from skipping back to 0 when the song ends. // Prevent the time from skipping back to 0 when the song ends.
@ -6693,10 +6511,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
audioVocalTrackGroup.pause(); audioVocalTrackGroup.pause();
}; };
audioInstTrack.volume = instTargetVolume;
#if FLX_PITCH
audioInstTrack.pitch = playbackRate;
#end
} }
else else
{ {
@ -6713,25 +6527,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
healthIconsDirty = true; healthIconsDirty = true;
} }
public function postLoadVocals():Void
{
// Reapply the volume and playback rate.
var vocalPlayerTargetVolume:Float = (menubarItemVolumeVocalsPlayer.value / 100.0) ?? 1.0;
var vocalOpponentTargetVolume:Float = (menubarItemVolumeVocalsOpponent.value / 100.0) ?? 1.0;
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
playbackRate = Math.round(playbackRate / 0.05) * 0.05; // Round to nearest 5%
playbackRate = playbackRate.clamp(0.05, 2.0); // Clamp to 5% to 200%
if (audioVocalTrackGroup != null)
{
audioVocalTrackGroup.playerVolume = vocalPlayerTargetVolume;
audioVocalTrackGroup.opponentVolume = vocalOpponentTargetVolume;
#if FLX_PITCH
audioVocalTrackGroup.pitch = playbackRate;
#end
}
}
function hardRefreshOffsetsToolbox():Void function hardRefreshOffsetsToolbox():Void
{ {
var offsetsToolbox:ChartEditorOffsetsToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT); var offsetsToolbox:ChartEditorOffsetsToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT);
@ -6776,7 +6571,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
cleanupAutoSave(); cleanupAutoSave();
this.closeExistingMenu(); this.closeAllMenus();
// Hide the mouse cursor on other states. // Hide the mouse cursor on other states.
Cursor.hide(); Cursor.hide();

View file

@ -1,7 +1,5 @@
package funkin.ui.debug.charting.components; package funkin.ui.debug.charting.components;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection; import flixel.graphics.frames.FlxFramesCollection;
@ -12,9 +10,6 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.NoteDirection; import funkin.play.notes.NoteDirection;
import haxe.ui.tooltips.ToolTipRegionOptions;
import funkin.util.HaxeUIUtil;
import haxe.ui.tooltips.ToolTipManager;
/** /**
* A sprite that can be used to display a note in a chart. * A sprite that can be used to display a note in a chart.
@ -68,21 +63,11 @@ class ChartEditorNoteSprite extends FlxSprite
return overrideData; return overrideData;
} }
public var isGhost:Bool = false; public function new(parent:ChartEditorState)
public var tooltip:ToolTipRegionOptions;
/**
* An indicator if the note is a note kind different than Default ("").
*/
public var kindIndicator:FlxText = new FlxText(5, 5, 100, '*', 16);
public function new(parent:ChartEditorState, isGhost:Bool = false)
{ {
super(); super();
this.parentState = parent; this.parentState = parent;
this.isGhost = isGhost;
this.tooltip = HaxeUIUtil.buildTooltip('N/A');
var entries:Array<String> = NoteStyleRegistry.instance.listEntryIds(); var entries:Array<String> = NoteStyleRegistry.instance.listEntryIds();
@ -104,8 +89,6 @@ class ChartEditorNoteSprite extends FlxSprite
{ {
addNoteStyleAnimations(fetchNoteStyle(entry)); addNoteStyleAnimations(fetchNoteStyle(entry));
} }
kindIndicator.setFormat("VCR OSD Mono", 24, FlxColor.YELLOW, LEFT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
} }
static var noteFrameCollection:Null<FlxFramesCollection> = null; static var noteFrameCollection:Null<FlxFramesCollection> = null;
@ -173,7 +156,6 @@ class ChartEditorNoteSprite extends FlxSprite
if (this.noteData == null) if (this.noteData == null)
{ {
this.kill(); this.kill();
updateTooltipPosition();
return this.noteData; return this.noteData;
} }
@ -185,7 +167,7 @@ class ChartEditorNoteSprite extends FlxSprite
// Update the position to match the note data. // Update the position to match the note data.
updateNotePosition(); updateNotePosition();
updateTooltipText();
return this.noteData; return this.noteData;
} }
@ -212,50 +194,6 @@ class ChartEditorNoteSprite extends FlxSprite
this.x += origin.x; this.x += origin.x;
this.y += origin.y; this.y += origin.y;
} }
this.updateTooltipPosition();
}
public function updateTooltipText():Void
{
if (this.noteData == null) return;
if (this.isGhost) return;
this.tooltip.tipData = {text: this.noteData.buildTooltip()};
}
public function updateTooltipPosition():Void
{
// No tooltip for ghost sprites.
if (this.isGhost) return;
if (this.noteData == null || (this.tooltip.tipData?.text ?? "").length == 0)
{
// Disable the tooltip.
ToolTipManager.instance.unregisterTooltipRegion(this.tooltip);
}
else
{
// Update the position.
this.tooltip.left = this.x;
this.tooltip.top = this.y;
this.tooltip.width = this.width;
this.tooltip.height = this.height;
// Enable the tooltip.
ToolTipManager.instance.registerTooltipRegion(this.tooltip);
}
}
override public function draw()
{
super.draw();
if (!parentState.showNoteKindIndicators) return;
if ((this.noteData?.kind ?? "").length == 0) return; // Do not render the note kind indicator if the note kind is default.
kindIndicator.x = this.x;
kindIndicator.y = this.y;
kindIndicator.draw();
} }
function get_noteStyle():Null<String> function get_noteStyle():Null<String>

View file

@ -11,7 +11,7 @@ class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu
var contextmenuEdit:MenuItem; var contextmenuEdit:MenuItem;
var contextmenuDelete:MenuItem; var contextmenuDelete:MenuItem;
public var data:SongEventData; var data:SongEventData;
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongEventData) public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongEventData)
{ {
@ -21,7 +21,7 @@ class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu
initialize(); initialize();
} }
public function initialize() function initialize()
{ {
contextmenuEdit.onClick = function(_) { contextmenuEdit.onClick = function(_) {
chartEditorState.showToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT); chartEditorState.showToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);

View file

@ -13,7 +13,7 @@ class ChartEditorHoldNoteContextMenu extends ChartEditorBaseContextMenu
var contextmenuFlip:MenuItem; var contextmenuFlip:MenuItem;
var contextmenuDelete:MenuItem; var contextmenuDelete:MenuItem;
public var data:SongNoteData; var data:SongNoteData;
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData) public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
{ {
@ -23,7 +23,7 @@ class ChartEditorHoldNoteContextMenu extends ChartEditorBaseContextMenu
initialize(); initialize();
} }
public function initialize():Void function initialize():Void
{ {
// NOTE: Remember to use commands here to ensure undo/redo works properly // NOTE: Remember to use commands here to ensure undo/redo works properly
contextmenuFlip.onClick = function(_) { contextmenuFlip.onClick = function(_) {

View file

@ -13,7 +13,7 @@ class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu
var contextmenuFlip:MenuItem; var contextmenuFlip:MenuItem;
var contextmenuDelete:MenuItem; var contextmenuDelete:MenuItem;
public var data:SongNoteData; var data:SongNoteData;
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData) public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
{ {
@ -23,7 +23,7 @@ class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu
initialize(); initialize();
} }
public function initialize():Void function initialize():Void
{ {
// NOTE: Remember to use commands here to ensure undo/redo works properly // NOTE: Remember to use commands here to ensure undo/redo works properly
contextmenuFlip.onClick = function(_) { contextmenuFlip.onClick = function(_) {

View file

@ -30,7 +30,7 @@ class ChartEditorSelectionContextMenu extends ChartEditorBaseContextMenu
initialize(); initialize();
} }
public function initialize():Void function initialize():Void
{ {
contextmenuCut.onClick = (_) -> { contextmenuCut.onClick = (_) -> {
chartEditorState.performCommand(new CutItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection)); chartEditorState.performCommand(new CutItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection));

View file

@ -137,8 +137,6 @@ class ChartEditorAudioHandler
result = playVocals(state, DAD, opponentId, instId); result = playVocals(state, DAD, opponentId, instId);
// if (!result) return false; // if (!result) return false;
state.postLoadVocals();
state.hardRefreshOffsetsToolbox(); state.hardRefreshOffsetsToolbox();
state.hardRefreshFreeplayToolbox(); state.hardRefreshFreeplayToolbox();

View file

@ -9,7 +9,6 @@ import haxe.ui.containers.menus.Menu;
import haxe.ui.core.Screen; import haxe.ui.core.Screen;
import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import haxe.ui.events.UIEvent;
/** /**
* Handles context menus (the little menus that appear when you right click on stuff) for the new Chart Editor. * Handles context menus (the little menus that appear when you right click on stuff) for the new Chart Editor.
@ -18,28 +17,11 @@ import haxe.ui.events.UIEvent;
@:access(funkin.ui.debug.charting.ChartEditorState) @:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorContextMenuHandler class ChartEditorContextMenuHandler
{ {
static var existingMenu:Null<Menu>; static var existingMenus:Array<Menu> = [];
static var existingDefaultContextMenu:Null<ChartEditorDefaultContextMenu>;
static var existingSelectionContextMenu:Null<ChartEditorSelectionContextMenu>;
static var existingNoteContextMenu:Null<ChartEditorNoteContextMenu>;
static var existingHoldNoteContextMenu:Null<ChartEditorHoldNoteContextMenu>;
static var existingEventContextMenu:Null<ChartEditorEventContextMenu>;
public static function openDefaultContextMenu(state:ChartEditorState, xPos:Float, yPos:Float) public static function openDefaultContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
{ {
if (existingDefaultContextMenu != null) displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos));
{
existingDefaultContextMenu.left = xPos;
existingDefaultContextMenu.top = yPos;
Screen.instance.addComponent(existingDefaultContextMenu);
}
else
{
var targetMenu = new ChartEditorDefaultContextMenu(state, xPos, yPos);
displayMenu(state, targetMenu);
existingDefaultContextMenu = targetMenu;
}
} }
/** /**
@ -47,19 +29,7 @@ class ChartEditorContextMenuHandler
*/ */
public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float) public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
{ {
if (existingSelectionContextMenu != null) displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos));
{
existingSelectionContextMenu.left = xPos;
existingSelectionContextMenu.top = yPos;
existingSelectionContextMenu.initialize();
Screen.instance.addComponent(existingSelectionContextMenu);
}
else
{
var targetMenu = new ChartEditorSelectionContextMenu(state, xPos, yPos);
displayMenu(state, targetMenu);
existingSelectionContextMenu = targetMenu;
}
} }
/** /**
@ -67,20 +37,7 @@ class ChartEditorContextMenuHandler
*/ */
public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData) public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
{ {
if (existingNoteContextMenu != null) displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data));
{
existingNoteContextMenu.left = xPos;
existingNoteContextMenu.top = yPos;
existingNoteContextMenu.data = data;
existingNoteContextMenu.initialize();
Screen.instance.addComponent(existingNoteContextMenu);
}
else
{
var targetMenu = new ChartEditorNoteContextMenu(state, xPos, yPos, data);
displayMenu(state, targetMenu);
existingNoteContextMenu = targetMenu;
}
} }
/** /**
@ -88,20 +45,7 @@ class ChartEditorContextMenuHandler
*/ */
public static function openHoldNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData) public static function openHoldNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
{ {
if (existingHoldNoteContextMenu != null) displayMenu(state, new ChartEditorHoldNoteContextMenu(state, xPos, yPos, data));
{
existingHoldNoteContextMenu.left = xPos;
existingHoldNoteContextMenu.top = yPos;
existingHoldNoteContextMenu.data = data;
existingHoldNoteContextMenu.initialize();
Screen.instance.addComponent(existingHoldNoteContextMenu);
}
else
{
var targetMenu = new ChartEditorHoldNoteContextMenu(state, xPos, yPos, data);
displayMenu(state, targetMenu);
existingHoldNoteContextMenu = targetMenu;
}
} }
/** /**
@ -109,44 +53,30 @@ class ChartEditorContextMenuHandler
*/ */
public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData) public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData)
{ {
if (existingEventContextMenu != null) displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data));
{
existingEventContextMenu.left = xPos;
existingEventContextMenu.top = yPos;
existingEventContextMenu.data = data;
existingEventContextMenu.initialize();
Screen.instance.addComponent(existingEventContextMenu);
}
else
{
var targetMenu = new ChartEditorEventContextMenu(state, xPos, yPos, data);
displayMenu(state, targetMenu);
existingEventContextMenu = targetMenu;
}
} }
static function displayMenu(state:ChartEditorState, targetMenu:Menu) static function displayMenu(state:ChartEditorState, targetMenu:Menu)
{ {
// Close the existing menu because it's of a different type // Close any existing menus
closeExistingMenu(state); closeAllMenus(state);
// Show the new menu // Show the new menu
Screen.instance.addComponent(targetMenu); Screen.instance.addComponent(targetMenu);
existingMenu = targetMenu; existingMenus.push(targetMenu);
} }
public static function closeExistingMenu(state:ChartEditorState) public static function closeMenu(state:ChartEditorState, targetMenu:Menu)
{ {
if (existingMenu != null) // targetMenu.close();
{ existingMenus.remove(targetMenu);
Screen.instance.removeComponent(existingMenu); }
existingDefaultContextMenu = null; public static function closeAllMenus(state:ChartEditorState)
existingSelectionContextMenu = null; {
existingNoteContextMenu = null; for (existingMenu in existingMenus)
existingHoldNoteContextMenu = null; {
existingEventContextMenu = null; closeMenu(state, existingMenu);
existingMenu = null;
} }
} }
} }

View file

@ -619,23 +619,6 @@ class ChartEditorDialogHandler
}; };
inputSongArtist.text = ""; inputSongArtist.text = "";
var inputSongCharter:Null<TextField> = dialog.findComponent('inputSongCharter', TextField);
if (inputSongCharter == null) throw 'Could not locate inputSongCharter TextField in Song Metadata dialog';
inputSongCharter.onChange = function(event:UIEvent) {
var valid:Bool = event.target.text != null && event.target.text != '';
if (valid)
{
inputSongCharter.removeClass('invalid-value');
newSongMetadata.charter = event.target.text;
}
else
{
newSongMetadata.charter = "";
}
};
inputSongCharter.text = "";
var inputStage:Null<DropDown> = dialog.findComponent('inputStage', DropDown); var inputStage:Null<DropDown> = dialog.findComponent('inputStage', DropDown);
if (inputStage == null) throw 'Could not locate inputStage DropDown in Song Metadata dialog'; if (inputStage == null) throw 'Could not locate inputStage DropDown in Song Metadata dialog';
inputStage.onChange = function(event:UIEvent) { inputStage.onChange = function(event:UIEvent) {
@ -709,7 +692,6 @@ class ChartEditorDialogHandler
{ {
state.songMetadata.clear(); state.songMetadata.clear();
state.songChartData.clear(); state.songChartData.clear();
state._songManifestData = null;
} }
state.songMetadata.set(targetVariation, newSongMetadata); state.songMetadata.set(targetVariation, newSongMetadata);

View file

@ -54,7 +54,7 @@ class ChartEditorImportExportHandler
if (chartData != null) songChartData.set(variation, chartData); if (chartData != null) songChartData.set(variation, chartData);
} }
loadSong(state, songMetadata, songChartData, new ChartManifestData(songId)); loadSong(state, songMetadata, songChartData);
state.sortChartData(); state.sortChartData();
@ -126,14 +126,10 @@ class ChartEditorImportExportHandler
* @param newSongMetadata The song metadata to load. * @param newSongMetadata The song metadata to load.
* @param newSongChartData The song chart data to load. * @param newSongChartData The song chart data to load.
*/ */
public static function loadSong(state:ChartEditorState, newSongMetadata:Map<String, SongMetadata>, newSongChartData:Map<String, SongChartData>, ?newSongManifestData:ChartManifestData):Void public static function loadSong(state:ChartEditorState, newSongMetadata:Map<String, SongMetadata>, newSongChartData:Map<String, SongChartData>):Void
{ {
state.songMetadata = newSongMetadata; state.songMetadata = newSongMetadata;
state.songChartData = newSongChartData; state.songChartData = newSongChartData;
if (newSongManifestData != null)
{
state.songManifestData = newSongManifestData;
}
if (!state.songMetadata.exists(state.selectedVariation)) if (!state.songMetadata.exists(state.selectedVariation))
{ {
@ -281,7 +277,7 @@ class ChartEditorImportExportHandler
songChartDatas.set(variation, variChartData); songChartDatas.set(variation, variChartData);
} }
loadSong(state, songMetadatas, songChartDatas, manifest); loadSong(state, songMetadatas, songChartDatas);
state.sortChartData(); state.sortChartData();
@ -458,7 +454,8 @@ class ChartEditorImportExportHandler
if (state.audioInstTrackData != null) zipEntries = zipEntries.concat(state.makeZIPEntriesFromInstrumentals()); if (state.audioInstTrackData != null) zipEntries = zipEntries.concat(state.makeZIPEntriesFromInstrumentals());
if (state.audioVocalTrackData != null) zipEntries = zipEntries.concat(state.makeZIPEntriesFromVocals()); if (state.audioVocalTrackData != null) zipEntries = zipEntries.concat(state.makeZIPEntriesFromVocals());
zipEntries.push(FileUtil.makeZIPEntry('manifest.json', state.songManifestData.serialize())); var manifest:ChartManifestData = new ChartManifestData(state.currentSongId);
zipEntries.push(FileUtil.makeZIPEntry('manifest.json', manifest.serialize()));
trace('Exporting ${zipEntries.length} files to ZIP...'); trace('Exporting ${zipEntries.length} files to ZIP...');

View file

@ -194,7 +194,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
numberStepper.id = field.name; numberStepper.id = field.name;
numberStepper.step = field.step ?? 1.0; numberStepper.step = field.step ?? 1.0;
if (field.min != null) numberStepper.min = field.min; if (field.min != null) numberStepper.min = field.min;
if (field.max != null) numberStepper.max = field.max; if (field.min != null) numberStepper.max = field.max;
if (field.defaultValue != null) numberStepper.value = field.defaultValue; if (field.defaultValue != null) numberStepper.value = field.defaultValue;
input = numberStepper; input = numberStepper;
case FLOAT: case FLOAT:

View file

@ -207,7 +207,7 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox
{ {
// Move the playhead if it would go out of view. // Move the playhead if it would go out of view.
var prevPlayheadRelativePos = playheadRelativePos; var prevPlayheadRelativePos = playheadRelativePos;
playheadRelativePos = playheadRelativePos.clamp(0, waveformScrollview.width - PLAYHEAD_RIGHT_PAD); playheadRelativePos = FlxMath.bound(playheadRelativePos, 0, waveformScrollview.width - PLAYHEAD_RIGHT_PAD);
trace('newPos: ${playheadRelativePos}'); trace('newPos: ${playheadRelativePos}');
var diff = playheadRelativePos - prevPlayheadRelativePos; var diff = playheadRelativePos - prevPlayheadRelativePos;

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