Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdedc0aad2 | ||
|
|
b23b008861 | ||
|
|
acbfa69d48 | ||
|
|
648ca8fd13 | ||
|
|
90eabf0b38 | ||
|
|
b3959a5935 | ||
|
|
cdca3bf307 | ||
|
|
e491c5e841 | ||
|
|
f9b125e44a | ||
|
|
1ceb4fad1a | ||
|
|
21fc59c997 | ||
|
|
0cbde53f86 | ||
|
|
5119cf1b72 | ||
|
|
927f3fc300 | ||
|
|
d1e4a17f46 | ||
|
|
ec868f8ef5 | ||
|
|
1e0a238463 | ||
|
|
e07e59a288 | ||
|
|
d6410d5e42 | ||
|
|
5974c1a482 | ||
|
|
ffdcbdbf21 | ||
|
|
3b27ec5b66 | ||
|
|
56a56dbfaa | ||
|
|
537bd5dd83 | ||
|
|
b0fd471f8a | ||
|
|
3633df3fdc | ||
|
|
e6234397f5 | ||
|
|
68ce5ce90b | ||
|
|
a6066f9905 | ||
|
|
75506f71a9 | ||
|
|
c4b5462a5a | ||
|
|
47d4d77c86 | ||
|
|
acf17f6165 | ||
|
|
2db26070c6 | ||
|
|
e0f9c7336b | ||
|
|
a28a28ae4c | ||
|
|
7160f748eb | ||
|
|
c8e60bed34 | ||
|
|
11c826daa6 | ||
|
|
6c887b4632 | ||
|
|
ecfb897dae | ||
|
|
56d5767004 | ||
|
|
7d3bfa32ca | ||
|
|
348647f486 | ||
|
|
48ed659800 | ||
|
|
8aafe62f9c | ||
|
|
0f7859bfb6 | ||
|
|
c3d3d2e547 | ||
|
|
37dc3461af | ||
|
|
c11ac1f658 | ||
|
|
47bbf67ae6 | ||
|
|
528e9e4f03 | ||
|
|
7626700495 | ||
|
|
0d2a663346 | ||
|
|
22736b27da | ||
|
|
dcfad7b7e8 | ||
|
|
ef625b3d9d | ||
|
|
6ce3a2e93e | ||
|
|
b8c2854007 | ||
|
|
04fffe8ade | ||
|
|
c37862445f | ||
|
|
0613e9d295 | ||
|
|
bed249dca0 | ||
|
|
139940f9ec | ||
|
|
36e2f47577 | ||
|
|
ce493a03c9 | ||
|
|
aabd1aa9f7 | ||
|
|
89029220e4 | ||
|
|
dca54de087 | ||
|
|
15bef89d71 | ||
|
|
6c1f3800db | ||
|
|
8507f799a1 | ||
|
|
dba2794780 | ||
|
|
48752e444c | ||
|
|
8a1d74b82c | ||
|
|
0d1db6ae9e | ||
|
|
1f1851990f | ||
|
|
67d2a9811b | ||
|
|
a782b3f9e8 | ||
|
|
74e28a18ff | ||
|
|
220636e01e | ||
|
|
4f316003af | ||
|
|
1a04e35db7 | ||
|
|
d3e83dd72d | ||
|
|
c4e2b900c1 | ||
|
|
ac3178118c | ||
|
|
681f7a6d07 | ||
|
|
aa24c94782 | ||
|
|
7075c8af4b | ||
|
|
efc33403b6 | ||
|
|
25a42b8182 | ||
|
|
0389af2202 | ||
|
|
b694ec08d6 | ||
|
|
423ccfab98 | ||
|
|
edc1f7113b | ||
|
|
f0b50de9fa | ||
|
|
0ad2d507ef | ||
|
|
6c13444211 | ||
|
|
5a0b4310a2 | ||
|
|
6b1fc0372b | ||
|
|
cf69125d25 | ||
|
|
84b2d5b48d | ||
|
|
dcb5b3a935 | ||
|
|
011f1541b1 | ||
|
|
13c1bf8ac0 | ||
|
|
1893f2b2f6 | ||
|
|
a0caa41edc | ||
|
|
456b2ff6c4 | ||
|
|
a1d1b1b2d1 | ||
|
|
0db4ce11b7 | ||
|
|
340fd5dcf1 | ||
|
|
d569690a51 | ||
|
|
4a5afa2fc7 | ||
|
|
59c593b812 | ||
|
|
6ba9e00a9f | ||
|
|
ec7bdd32e2 | ||
|
|
939528a9bd | ||
|
|
08b3af959d | ||
|
|
cea54e394c | ||
|
|
e710198afe | ||
|
|
bf3601ea8d | ||
|
|
c5566c8644 | ||
|
|
5944f5b55d | ||
|
|
077fbf6978 | ||
|
|
f02bdc4225 | ||
|
|
8da9ed4ea0 | ||
|
|
5ed4a2ddd0 | ||
|
|
6293e2808b | ||
|
|
c6dc562769 | ||
|
|
a607396d04 | ||
|
|
365f8dcfea | ||
|
|
b3eb709f4e | ||
|
|
fcbce0a66b | ||
|
|
05032e3126 | ||
|
|
b8a7189e24 | ||
|
|
881aba3b03 | ||
|
|
5480774afe | ||
|
|
d62c8af20c | ||
|
|
7d0644f3c8 | ||
|
|
fe21c0addd | ||
|
|
219143c02d | ||
|
|
03930a29c3 | ||
|
|
5d378cd9f2 | ||
|
|
e5db402025 | ||
|
|
d348ef3127 | ||
|
|
9301c0c7dd | ||
|
|
02ee8e64c8 | ||
|
|
40eab2e461 | ||
|
|
d6a65ca681 | ||
|
|
f8841bab35 | ||
|
|
dc23a116dd | ||
|
|
024994e85a | ||
|
|
9be1633320 | ||
|
|
3b15873fc7 | ||
|
|
786f19c57d | ||
|
|
ccb874e97a | ||
|
|
f7fd7a57c3 | ||
|
|
23f79aef58 | ||
|
|
2c394cc636 | ||
|
|
d7a0139e4f | ||
|
|
562d23005d | ||
|
|
671c152463 | ||
|
|
3ce7c7939f | ||
|
|
28450ccfc6 | ||
|
|
31dbd68194 | ||
|
|
29d776061c | ||
|
|
0805ec914e | ||
|
|
98ac6a6ce1 | ||
|
|
823120cdef |
@@ -48,19 +48,19 @@ body:
|
||||
attributes:
|
||||
label: Mobile Device Model
|
||||
description: (Mobile users only) What mobile device are you playing on?
|
||||
placeholder: ex. iPhone 17, Galaxy S25, iPad 11th Gen
|
||||
placeholder: ex. iPhone 17, Galaxy S26, iPad 11th Gen
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Mobile OS Version
|
||||
description: (Mobile users only) What version is your Operating System?
|
||||
placeholder: ex. iOS 26.2, Android 16, iPadOS 26.2
|
||||
placeholder: ex. iOS 26.4, Android 16, iPadOS 26.4
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.8.1
|
||||
placeholder: ex. 0.8.4 or 0.8.5
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ body:
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.8.1
|
||||
placeholder: ex. 0.8.4 or 0.8.5
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -52,7 +52,7 @@ body:
|
||||
attributes:
|
||||
label: Location
|
||||
description: Where did you find the issue(s)? Include the name of the song, the variation, the difficulty, the time/section of the song, and any images or videos.
|
||||
placeholder: ex. Cocoa Erect on Erect/Nightmare difficulties at Section 30
|
||||
placeholder: ex. Cocoa Erect [Nightmare] at Section 30, step 5
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ body:
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you compiling? The game version is in the bottom left corner of the main menu or in the project.hxp file.
|
||||
placeholder: ex. 0.8.1
|
||||
placeholder: ex. 0.8.4 or 0.8.5
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -49,19 +49,19 @@ body:
|
||||
attributes:
|
||||
label: Mobile Device Model
|
||||
description: (Mobile users only) What mobile device are you playing on?
|
||||
placeholder: ex. iPhone 17, Galaxy S25, iPad 11th Gen
|
||||
placeholder: ex. iPhone 17, Galaxy S26, iPad 11th Gen
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Mobile OS Version
|
||||
description: (Mobile users only) What version is your Operating System?
|
||||
placeholder: ex. iOS 26.2, Android 16, iPadOS 26.2
|
||||
placeholder: ex. iOS 26.4, Android 16, iPadOS 26.4
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version are you playing on? The game version is in the bottom left corner of the main menu.
|
||||
placeholder: ex. 0.8.1
|
||||
placeholder: ex. 0.8.4 or 0.8.5
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
Vendored
+1
-2
@@ -72,7 +72,6 @@
|
||||
"haxecheckstyle.externalSourceRoots": [],
|
||||
"haxecheckstyle.configurationFile": "checkstyle.json",
|
||||
"haxecheckstyle.codeSimilarityBufferSize": 100,
|
||||
"lime.projectFile": "project.hxp",
|
||||
"lime.targets": [
|
||||
{ "name": "windows", "enabled": true, "label": "Windows" },
|
||||
{ "name": "mac", "enabled": true, "label": "macOS" },
|
||||
@@ -80,8 +79,8 @@
|
||||
{ "name": "html5", "enabled": true, "label": "HTML5" },
|
||||
{ "name": "android", "enabled": true, "label": "Android" },
|
||||
{ "name": "ios", "enabled": true, "label": "iOS" },
|
||||
{ "name": "hl", "enabled": true, "label": "HashLink" },
|
||||
// Disabled targets
|
||||
{ "name": "hl", "enabled": false, "label": "HashLink" },
|
||||
{ "name": "air", "enabled": false },
|
||||
{ "name": "electron", "enabled": false },
|
||||
{ "name": "flash", "enabled": false },
|
||||
|
||||
+144
-2
@@ -4,6 +4,149 @@ All notable changes will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.8.5] - 2026-04-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- [ANDROID] Fixed various issues with visuals, controls, and gamepad functionality.
|
||||
|
||||
|
||||
|
||||
## [0.8.4] - 2026-03-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added several new features to Polymod:
|
||||
- Added FULL support for abstracts! (Previously, only `FlxColor` was supported)
|
||||
- Added support for getting the code of a single character string (i.e `’h’.code`) (Thanks NotHyper-474!)
|
||||
- Added support for import defaults (`import.hx`/`import.hxc`), allowing scripted classes with the same package to share imports. (Thanks KoloInDaCrib!)
|
||||
- Added `scriptInit` and `listScriptClasses` functions to scripted classes. (Thanks KoloInDaCrib!)
|
||||
- `.hx` files can now be parsed. (Thanks Starexify!)
|
||||
- Added support for preprocessing (`#if`/`#else` syntax), allowing for platform-specific code to be implemented in an easier way. (Thanks KoloInDaCrib!)
|
||||
- Added support for mod IDs. (Thanks Starexify!)
|
||||
- Implemented `_merge` support for scripts. (Thanks KoloInDaCrib!)
|
||||
- Please refer to the Cookbook to learn how to use this.
|
||||
- Added widescreen scale properties to the Zoom Camera event in the Chart Editor to account for widescreen resolutions.
|
||||
- Added a flipY field for Story Mode level props. ([48ed659](https://github.com/FunkinCrew/Funkin/commit/48ed65980005366371fda7a66c04f73bb5b86200)) - by @ExtraCode75 in [#6340](https://github.com/FunkinCrew/Funkin/pull/6340)
|
||||
- Implemented working start and end times for Freeplay song previews. ([40eab2e](https://github.com/FunkinCrew/Funkin/commit/40eab2e4618ebb6dc67a06ef67df817920773c56)) - by @ADA-Funni in [#6725](https://github.com/FunkinCrew/Funkin/pull/6725)
|
||||
- Added a new albumOSTName property to Freeplay album data to make the "OFFICIAL OST" text moddable. ([67d2a98](https://github.com/FunkinCrew/Funkin/commit/67d2a9811b5c92409bdc76e287c984d5777d8621)) - by @NotHyper-474 in [#6578](https://github.com/FunkinCrew/Funkin/pull/6578)
|
||||
- Added an optional 'capsule' field to level data to display any week label on Freeplay capsules. ([528e9e4](https://github.com/FunkinCrew/Funkin/commit/528e9e4f03c40e59f3932d8b4c522b6eeb66783b)) - by @Trayfellow in [#7149](https://github.com/FunkinCrew/Funkin/pull/7149)
|
||||
- Implemented ScriptedBaseFreeplayDJ, allowing modders to customize how custom Freeplay DJs are rendered. ([423ccfa](https://github.com/FunkinCrew/Funkin/commit/423ccfab98a29646e4998dacedaf403319d956ff)) - by @PurSnake in [#6806](https://github.com/FunkinCrew/Funkin/pull/6806)
|
||||
- Added a new FunkinGroup class to make groups of sprites easier to modify. ([4f31600](https://github.com/FunkinCrew/Funkin/commit/4f316003af9a6ca46c77bf4971059e20543cbd7b)) - by @MightyTheArmiddilo in [#6758](https://github.com/FunkinCrew/Funkin/pull/6758)
|
||||
- Added `Reflect.makeVarArgs` to allow modders to create functions with a variable number of arguments. ([3633df3](https://github.com/FunkinCrew/Funkin/commit/3633df3fdc6dc0af4f9886fcb1e1d4a8d2b8d5aa)) - by @Starexify in [#6738](https://github.com/FunkinCrew/Funkin/pull/6738)
|
||||
- Added script event support for song events. ([15bef89](https://github.com/FunkinCrew/Funkin/commit/15bef89d71982c5685f4ce6f6d7e6e4d8321508c)) - by @KoloInDaCrib in [#4130](https://github.com/FunkinCrew/Funkin/pull/4130)
|
||||
- Added a new Chart Editor event type: Set Target Bop Speed. ([1893f2b](https://github.com/FunkinCrew/Funkin/commit/1893f2b2f679d8a4d262052793dde24e94719ee6)) - by @roma-perec-bp in [#6769](https://github.com/FunkinCrew/Funkin/pull/6769)
|
||||
- Added shortcuts to select all notes above or below the playhead in the Chart Editor. ([08b3af9](https://github.com/FunkinCrew/Funkin/commit/08b3af959d6657511bfe8418da545731050cb24a)) - by @Lasercar in [#4306](https://github.com/FunkinCrew/Funkin/pull/4306)
|
||||
- Added the ability to view and edit the timing of a note or event through the Chart Editor context menu. ([a782b3f](https://github.com/FunkinCrew/Funkin/commit/a782b3f9e8498634babb864341fd1437bb2c01f6)) - by @Lasercar in [#4200](https://github.com/FunkinCrew/Funkin/pull/4200)
|
||||
- Added an "Edit Note" option to the Chart Editor context menu that opens the Notes window. ([e710198](https://github.com/FunkinCrew/Funkin/commit/e710198afec9a0fe19effe4cd196ab3a247f4784)) - by @Honton129 in [#7010](https://github.com/FunkinCrew/Funkin/pull/7010)
|
||||
- Added the ability to Ctrl-click on health icons in the Chart Editor to open character previews. ([011f154](https://github.com/FunkinCrew/Funkin/commit/011f1541b197fd4f962ab8a910cbd417f78c2d09)) - by @Trofem in [#7089](https://github.com/FunkinCrew/Funkin/pull/7089)
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded the input handling system from SDL2 to SDL3!
|
||||
- Added an “Unlocked Framerate” preference to Desktop platforms.
|
||||
- The input system is now much more responsive and accurate down to the nanosecond.
|
||||
- The game now properly supports more controllers and input devices.
|
||||
- The game now shows popups when installed mods have issues, such as when the API version is incorrect.
|
||||
- Unfocusing the game now reduces the game’s volume to 25% (down from 50%).
|
||||
- The notekinds dropdown are no longer hardcoded in the Chart Editor.
|
||||
- The Character Select key is now hidden in the Controls menu until a new character is unlocked. ([6c13444](https://github.com/FunkinCrew/Funkin/commit/6c134442118d3f5f61b9e3faa631512d210422fb)) - by @Lasercar in [#4882](https://github.com/FunkinCrew/Funkin/pull/4882)
|
||||
- The Story Mode level score now displays with commas. ([04fffe8](https://github.com/FunkinCrew/Funkin/commit/04fffe8ade4c4b236d59a236f08534e8eede179f)) - by @JVNpixels in [#4965](https://github.com/FunkinCrew/Funkin/pull/4965)
|
||||
- Freeplay capsule week labels are now softcoded and rendered using a font. ([48752e4](https://github.com/FunkinCrew/Funkin/commit/48752e444cc5f1b8cd0a3b579f843e9ab6580c46)) - by @KoloInDaCrib in [#5249](https://github.com/FunkinCrew/Funkin/pull/5249)
|
||||
- The Freeplay change character hint now changes when a gamepad is connected. ([0f7859b](https://github.com/FunkinCrew/Funkin/commit/0f7859bfb6b829290804efab9d5c67fbdbec0903)) - by @MrMadera in [#6609](https://github.com/FunkinCrew/Funkin/pull/6609)
|
||||
- Moved the Winter Horrorland intro cutscene logic into a script. ([9bbe73b](https://github.com/FunkinCrew/funkin.assets/commit/9bbe73b21ccbea29a5d5187709f91b4822bf7e31)) - by @mario64thane in [funkin.assets#63](https://github.com/FunkinCrew/funkin.assets/pull/63)
|
||||
- Merged FlxFilteredSprite functionality into FunkinSprite. ([22736b2](https://github.com/FunkinCrew/Funkin/commit/22736b27da7d8de8b11ab6cc6c2a622e5579dccd)) - by @TechnikTil in [#7114](https://github.com/FunkinCrew/Funkin/pull/7114)
|
||||
- Adjusted some wording in the Chart/Stage Editor backup window to be clearer. ([a607396](https://github.com/FunkinCrew/Funkin/commit/a607396d04f24388691ee23bd04022681ca54756)) - by @Marcosayfi in [#7071](https://github.com/FunkinCrew/Funkin/pull/7071)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Conjoined string interpolation `$a$b` now parses properly in Polymod.
|
||||
- Video cutscenes are no longer muted at volumes of 3 or lower.
|
||||
- Video cutscenes no longer occasionally show glitches at the beginning.
|
||||
- [MOBILE] Loading a cloud save from Newgrounds no longer carries over the volume level from Desktop.
|
||||
- [HTML5] Scrolling the game out of view now pauses the game instead of allowing gameplay to continue processing.
|
||||
- The Boyfriend Freeplay DJ new rank animation now transitions smoothly back to its idle animation.
|
||||
- Hold notes now score much more consistently across different framerate values.
|
||||
- The bricks in Week 7 stages are now better quality.
|
||||
- Trimmed out the silent beat at the beginning of Ugh Erect.
|
||||
- Camera bopping no longer stops at zoom values of 1.35x or higher.
|
||||
- Charting many hold notes in a row no longer breaks audio handling.
|
||||
- Adding collapsible frames in event schemas no longer crashes the game.
|
||||
- Some event properties have been grouped under Advanced frames.
|
||||
- Chart Editor dropdowns no longer refocus when a tooltip appears.
|
||||
- Focus and Zoom Camera event durations now account for playback speed.
|
||||
- [WINDOWS] Exiting the game no longer leaves it running as a background process. ([881aba3](https://github.com/FunkinCrew/Funkin/commit/881aba3b03a26764374441cab244cd2d360a30b9)) - by @AnimatingLegend in [#6885](https://github.com/FunkinCrew/Funkin/pull/6885)
|
||||
- Story Mode now only shows difficulties that all of a level's songs have. ([365f8dc](https://github.com/FunkinCrew/Funkin/commit/365f8dcfea9569f2edba1ebfceb3d49819ad2b57)) - by @Starexify in [#7046](https://github.com/FunkinCrew/Funkin/pull/7046)
|
||||
- The Girlfriend Story Mode prop now bops continuously when scrolling through levels. ([84b2d5b](https://github.com/FunkinCrew/Funkin/commit/84b2d5b48d9eb76c918fdba6987ccc1f5da76133)) - by @Redar13 in [#6830](https://github.com/FunkinCrew/Funkin/pull/6830)
|
||||
- The Freeplay new rank slam animation now properly plays its glow effect. ([fcbce0a](https://github.com/FunkinCrew/Funkin/commit/fcbce0a66b75b0c1224c6a0e2907c170b71998ad)) - by @FuroYT in [#7015](https://github.com/FunkinCrew/Funkin/pull/7015)
|
||||
- The Freeplay Random capsule no longer displays BPM and difficulty numbers. ([0ad2d50](https://github.com/FunkinCrew/Funkin/commit/0ad2d507efa9dcb577184e28bfd04091b3424bfb)) - by @Lasercar in [#4927](https://github.com/FunkinCrew/Funkin/pull/4927)
|
||||
- Favoriting a song in Freeplay no longer breaks capsule text cycling. ([ce493a0](https://github.com/FunkinCrew/Funkin/commit/ce493a03c9ffb4c43c2c27885ed0fab0c1c3531d)) - by @VirtuGuy in [#7134](https://github.com/FunkinCrew/Funkin/pull/7134)
|
||||
- Freeplay songs with invalid pixel icons no longer duplicate the icon from the previous song in the list. ([b3eb709](https://github.com/FunkinCrew/Funkin/commit/b3eb709f4ed535ed2544f3bb530ff3724e07be3f)) - by @FuroYT in [#7006](https://github.com/FunkinCrew/Funkin/pull/7006)
|
||||
- A Freeplay clear percent of 100% now properly exits to the right when returning to the Main Menu. ([5974c1a](https://github.com/FunkinCrew/Funkin/commit/5974c1a48293f3185c5f249acb9130ebd6b35ae4)) - by @Trayfellow in [#7183](https://github.com/FunkinCrew/Funkin/pull/7183)
|
||||
- Nudged the Freeplay backing image down by one pixel to align with the bottom of the screen. ([b8c2854](https://github.com/FunkinCrew/Funkin/commit/b8c2854007f36ac7a2b6897eead3f979e11f0ad7)) - by @SuperEric1 in [#6893](https://github.com/FunkinCrew/Funkin/pull/6893)
|
||||
- Improved performance for songs that use a lot of events. ([3b27ec5](https://github.com/FunkinCrew/Funkin/commit/3b27ec5b66b2fa00d59a77f6ec07aff68e440860)) - by @KoloInDaCrib in [#7171](https://github.com/FunkinCrew/Funkin/pull/7171)
|
||||
- Adjusted hit window logic to accurately reflect threshold values for all judgments. ([dcb5b3a](https://github.com/FunkinCrew/Funkin/commit/dcb5b3a9358a9bb126153b57aa2d38dc0684c39d)) - by @Rudyrue in [#7104](https://github.com/FunkinCrew/Funkin/pull/7104)
|
||||
- Adjusted offsets for all health icons to be more consistent. ([83d8134](https://github.com/FunkinCrew/funkin.assets/commit/83d8134f9d0b3cf3b59e9e1d972fa1e200a98ba1)) - by @LeGooeyy in [funkin.assets#278](https://github.com/FunkinCrew/funkin.assets/pull/278)
|
||||
- Hold notes no longer shift upward when hit early. ([d62c8af](https://github.com/FunkinCrew/Funkin/commit/d62c8af20c90ec3d6e4274aba8ddbfb378ab0256)) - by @VirtuGuy in [#7060](https://github.com/FunkinCrew/Funkin/pull/7060)
|
||||
- The opponent no longer stops singing upon missing if Boyfriend has no vocals file. ([cea54e3](https://github.com/FunkinCrew/Funkin/commit/cea54e394c78544dd0ec0339721faaac140caa65)) - by @FuroYT in [#7007](https://github.com/FunkinCrew/Funkin/pull/7007)
|
||||
- onPause script functions now properly trigger when pausing during a cutscene or dialogue. ([5a0b431](https://github.com/FunkinCrew/Funkin/commit/5a0b4310a2c39b033989e0e36b8f456bdac65a5b)) - by @MightyTheArmiddilo in [#6804](https://github.com/FunkinCrew/Funkin/pull/6804)
|
||||
- Camera events now consistently pause when the Pause Menu is opened. ([e623439](https://github.com/FunkinCrew/Funkin/commit/e6234397f57d2af37035a0ff2504b8241e23949d)) - by @VirtuGuy in [#7145](https://github.com/FunkinCrew/Funkin/pull/7145)
|
||||
- The Pause Menu global offset value now adjusts at the same rate across all framerates. ([348647f](https://github.com/FunkinCrew/Funkin/commit/348647f48629a24ee3bd2b759bbfc1bff168fb81)) - by @KolzeYT in [#7147](https://github.com/FunkinCrew/Funkin/pull/7147)
|
||||
- Spirit's trail effect now renders consistently across all framerates. ([75506f7](https://github.com/FunkinCrew/Funkin/commit/75506f71a97d468f516a0232c8be5560afb0682e)) - by @KolzeYT in [#7179](https://github.com/FunkinCrew/Funkin/pull/7179)
|
||||
- The sound tray now appears consistently and plays sound effects at the right volume. ([2b9906d](https://github.com/FunkinCrew/Funkin/commit/2b9906df29660057410d3054e897012896896d6a)) - by @Raltyro in [#7068](https://github.com/FunkinCrew/Funkin/pull/7068)
|
||||
- The sticker transition now properly pauses when losing focus with Pause on Unfocus enabled. ([8a1d74b](https://github.com/FunkinCrew/Funkin/commit/8a1d74b82cceab97e777dc49e7a2117b8d27e59b)) - by @ComedyLost in [#7129](https://github.com/FunkinCrew/Funkin/pull/7129)
|
||||
- [HTML5] Rimlight alt masks now render properly. ([0613e9d](https://github.com/FunkinCrew/Funkin/commit/0613e9d295179e66d30db3d2f6805870eac847d6)) - by @VirtuGuy in [#6900](https://github.com/FunkinCrew/Funkin/pull/6900)
|
||||
- The Week 2 Erect stage lightning strike fade now pauses and cancels appropriately. ([cd05267](https://github.com/FunkinCrew/funkin.assets/commit/cd05267023859d04cc8920c787b8280682e5f0f5)) - by @ComedyLost in [funkin.assets#390](https://github.com/FunkinCrew/funkin.assets/pull/390)
|
||||
- A-Bot (Dark)'s frame no longer renders as completely dark in Week 2 Erect. ([a142530](https://github.com/FunkinCrew/funkin.assets/commit/a142530cae6198ca17b560ff73e36d50f1582768)) - by @ComedyLost in [funkin.assets#368](https://github.com/FunkinCrew/funkin.assets/pull/368)
|
||||
- Dying on the same frame as a lightning strike in Week 2 Erect no longer throws a script error. ([ef89bde](https://github.com/FunkinCrew/funkin.assets/commit/ef89bde19c397f15a53b0fda2f64ba93ff96f56f)) - by @KoloInDaCrib in [funkin.assets#363](https://github.com/FunkinCrew/funkin.assets/pull/363)
|
||||
- Pico (Dark) and Nene (Dark) no longer throw script errors in debug editors. ([b16ce44](https://github.com/FunkinCrew/funkin.assets/commit/b16ce44beb43d4511c61418c824929fb4dfabb63)) - by @ComedyLost in [funkin.assets#367](https://github.com/FunkinCrew/funkin.assets/pull/367)
|
||||
- Girlfriend's hair no longer falls when restarting the song in a Week 3 stage. ([ade80ab](https://github.com/FunkinCrew/funkin.assets/commit/ade80ab72915ddd47724c380bb41d6df4c1b79bd)) - by @AnimatingLegend in [funkin.assets#370](https://github.com/FunkinCrew/funkin.assets/pull/370)
|
||||
- The camera transition to Results no longer reveals the edges of stage art in the Week 6 Erect stage. ([ecf4c54](https://github.com/FunkinCrew/funkin.assets/commit/ecf4c5458a2e1fadba7db31c3272cea485d22b2f)) - by @ComedyLost in [funkin.assets#383](https://github.com/FunkinCrew/funkin.assets/pull/383)
|
||||
- Fine-tuned offsets for pixel characters. ([8b3c268](https://github.com/FunkinCrew/funkin.assets/commit/8b3c2680a6a966e81b62598e783c60f46355c759)) - by @CEliuxJV in [funkin.assets#376](https://github.com/FunkinCrew/funkin.assets/pull/376)
|
||||
- Tankman's game over lines now properly play during game overs in Ugh (Pico Mix) and Guns (Pico Mix). ([b900208](https://github.com/FunkinCrew/funkin.assets/commit/b900208143f980adee25d70db247e0b176dd659c)) - by @hucks5 in [funkin.assets#373](https://github.com/FunkinCrew/funkin.assets/pull/373)
|
||||
- Restarting during the mid-song animation in Stress (Pico Mix) now properly resets the characters' animations. ([2ca92de](https://github.com/FunkinCrew/funkin.assets/commit/2ca92de46eced3f53d305579704fcb7e8c051f3a)) - by @requazar in [funkin.assets#364](https://github.com/FunkinCrew/funkin.assets/pull/364)
|
||||
- The rain shader now properly fades out with the transition to the Results Screen. ([a0caa41](https://github.com/FunkinCrew/Funkin/commit/a0caa41edcb26298e4f8c243a5eed0b4fe93fcf7)) - by @VirtuGuy in [#6776](https://github.com/FunkinCrew/Funkin/pull/6776)
|
||||
- Exiting a Chart Editor playtest while using the pixel notestyle no longer crashes the game. ([6c1f380](https://github.com/FunkinCrew/Funkin/commit/6c1f3800dbc9b13fb4b409abce53c1dafc9bbe8f)) - by @Starexify and @KoloInDaCrib in [#7108](https://github.com/FunkinCrew/Funkin/pull/7108)
|
||||
- Chart Editor Edit buttons are now appropriately disabled when nothing is selected. ([6b1fc03](https://github.com/FunkinCrew/Funkin/commit/6b1fc0372bca202d80f4ccf316580b13a5a80879)) - by @Lasercar in [#4386](https://github.com/FunkinCrew/Funkin/pull/4386)
|
||||
- Undoing a Chart Editor event now plays the undo sound. ([f7fd7a5](https://github.com/FunkinCrew/Funkin/commit/f7fd7a57c36f0461f56c2ab627a17d752cb82320)) - by @VirtuGuy in [#6963](https://github.com/FunkinCrew/Funkin/pull/6963)
|
||||
- The playback speed audio setting no longer rounds incorrectly upon reopening the Chart Editor. ([3b15873](https://github.com/FunkinCrew/Funkin/commit/3b15873fc7d0c20840c313f1d2cfed91014c664f)) - by @AnimatingLegend in [#7023](https://github.com/FunkinCrew/Funkin/pull/7023)
|
||||
- The Chart Editor easing preview graph now properly displays tweens that exceed start/end values. ([7626700](https://github.com/FunkinCrew/Funkin/commit/7626700495743862caddbf50a7dafef51529a2a6)) - by @echolotl in [#7152](https://github.com/FunkinCrew/Funkin/pull/7152)
|
||||
- The Chart Editor playbar head is no longer draggable during a playtest. ([ccb874e](https://github.com/FunkinCrew/Funkin/commit/ccb874e97a121859c26a8a71b394743d27f6ed37)) - by @ComedyLost in [#6987](https://github.com/FunkinCrew/Funkin/pull/6987)
|
||||
- The Results Screen no longer shows up after a Chart Editor playtest with Bot Play enabled. ([786f19c](https://github.com/FunkinCrew/Funkin/commit/786f19c57d999dba9309cae28d912b080968670d)) - by @ComedyLost in [#7022](https://github.com/FunkinCrew/Funkin/pull/7022)
|
||||
- The Osu!Mania Chart Editor importer no longer accepts non-Osu!Mania beatmaps. ([456b2ff](https://github.com/FunkinCrew/Funkin/commit/456b2ff6c4a127733120cef3d03d6102bdd7522f)) - by @FuroYT in [#6624](https://github.com/FunkinCrew/Funkin/pull/6624)
|
||||
- Characters in the Stage Editor no longer fly away when clicking and dragging them. ([0389af2](https://github.com/FunkinCrew/Funkin/commit/0389af2202aa4dc6a21ce9b7eea95944bfaa6946)) - by @KoloInDaCrib in [#7002](https://github.com/FunkinCrew/Funkin/pull/7002)
|
||||
- Characters with alternating idle animations now properly play danceLeft when pressing Space in the Animation Editor. ([05032e3](https://github.com/FunkinCrew/Funkin/commit/05032e3126eda78a2bbf70e8c975bcb04eed201b)) - by @ComedyLost in [#7056](https://github.com/FunkinCrew/Funkin/pull/7056)
|
||||
- [LINUX] Paths are now properly sanitized as they are on other platforms. ([219143c](https://github.com/FunkinCrew/Funkin/commit/219143c02dcf65e68aec80701a82023ca0d70db6)) - by @NotHyper-474 in [#6395](https://github.com/FunkinCrew/Funkin/pull/6395)
|
||||
- OpenFL and some Lime classes are now accessible to modders. ([56a56db](https://github.com/FunkinCrew/Funkin/commit/56a56dbfaa3f62a3f684df49c682f69a496e45f4)) - by @FuroYT in [#7115](https://github.com/FunkinCrew/Funkin/pull/7115)
|
||||
- Sandboxed API classes can now be accessed by mods. ([03930a2](https://github.com/FunkinCrew/Funkin/commit/03930a29c3d8aa6a310147a7131c6b7cddefd475)) - by @MightyTheArmiddilo in [#6952](https://github.com/FunkinCrew/Funkin/pull/6952)
|
||||
- Blacklisted some more classes for security.
|
||||
- And even more fixes!
|
||||
|
||||
## New Contributors for 0.8.4
|
||||
|
||||
* @roma-perec-bp made their first contribution in [#6769](https://github.com/FunkinCrew/Funkin/pull/6769)
|
||||
* @Redar13 made their first contribution in [#6830](https://github.com/FunkinCrew/Funkin/pull/6830)
|
||||
* @SuperEric1 made their first contribution in [#6893](https://github.com/FunkinCrew/Funkin/pull/6893)
|
||||
* @AnimatingLegend made their first contribution in [#7023](https://github.com/FunkinCrew/Funkin/pull/7023)
|
||||
* @Marcosayfi made their first contribution in [#7071](https://github.com/FunkinCrew/Funkin/pull/7071)
|
||||
* @Rudyrue made their first contribution in [#7104](https://github.com/FunkinCrew/Funkin/pull/7104)
|
||||
* @KolzeYT made their first contribution in [#7147](https://github.com/FunkinCrew/Funkin/pull/7147)
|
||||
* @echolotl made their first contribution in [#7152](https://github.com/FunkinCrew/Funkin/pull/7152)
|
||||
* @mario64thane made their first contribution in [funkin.assets#63](https://github.com/FunkinCrew/funkin.assets/pull/63)
|
||||
* @LeGooeyy made their first contribution in [funkin.assets#278](https://github.com/FunkinCrew/funkin.assets/pull/278)
|
||||
* @requazar made their first contribution in [funkin.assets#364](https://github.com/FunkinCrew/funkin.assets/pull/364)
|
||||
|
||||
|
||||
|
||||
## [0.8.3] - 2026-02-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- [HTML5] Hitting a can in 2hot no longer creates a script error. (Thanks NotHyper-474!)
|
||||
- The game no longer consistently lag spikes during gameplay. ([823120c](https://github.com/FunkinCrew/Funkin/commit/823120cdef7bfbb2d3ec37c7085878fcbeebcb30)) - by @ACrazyTown in [#6970](https://github.com/FunkinCrew/Funkin/pull/6970)
|
||||
|
||||
|
||||
|
||||
## [0.8.2] - 2026-02-23
|
||||
|
||||
### Added
|
||||
@@ -22,7 +165,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Scripted classes can now extend other scripted classes. (Thanks KoloInDaCrib!)
|
||||
- Tweaked some error messages to be more verbose.
|
||||
- Added `scriptHas` and `staticScriptHas` functions to check if a script has a specific variable/function.
|
||||
- The `is` operator is now more accurate to regular Haxe. (Thanks NotHyper-474!)
|
||||
- The `is` operator is now more accurate to regular Haxe. (Thanks Starexify!)
|
||||
- Static functions can now use static extensions (`using`s). (Thanks NotHyper-474!)
|
||||
- `interface`s can now be properly parsed. (Thanks Starexify!)
|
||||
- Optional fields and extensions can now be used in `typedef`s. (Thanks Starexify!)
|
||||
@@ -88,7 +231,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- [DESKTOP] The game display no longer sometimes becomes offset from the window.
|
||||
- [MOBILE] Selecting a song using touch controls when a keyboard is connected no longer disables the keyboard strumline layout.
|
||||
- [ANDROID] Improved Freeplay scrolling behavior on high framerate displays.
|
||||
- Ctrl-clicking on a hold note no longer crashes the Chart Editor.
|
||||
- [WINDOWS] Chart Editor save dialog windows no longer freeze the game.
|
||||
- The Chart Editor Results Screen playtest option now consistently shows the Results Screen after each playtest.
|
||||
- Scrolling really quickly in Freeplay no longer crashes the game. ([76699b2](https://github.com/FunkinCrew/Funkin/commit/76699b235cd5b359bc34e6950cd05bbbb13918f8)) - by @ComedyLost in [#6962](https://github.com/FunkinCrew/Funkin/pull/6962)
|
||||
|
||||
+1
-1
Submodule assets updated: 5be03b9cd8...d1d027d474
@@ -147,6 +147,7 @@
|
||||
}
|
||||
],
|
||||
"excludes": [
|
||||
"assets/fonts/vcr-bmp.png",
|
||||
"assets/preload/images/cursor/",
|
||||
"assets/preload/images/fonts/",
|
||||
"assets/preload/images/freeplay/",
|
||||
@@ -197,6 +198,8 @@
|
||||
"assets/week1/images/erect/orangeLight.png",
|
||||
"assets/week2/images/erect/stairsDark.png",
|
||||
"assets/week2/images/erect/stairsLight.png",
|
||||
"assets/week6/"
|
||||
"assets/week6/",
|
||||
"assets/week7/images/bricksGround.png",
|
||||
"assets/week7/images/erect/bricksGround.png"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ This section provides guidelines to follow when [opening an issue](https://githu
|
||||
|
||||
## Requirements
|
||||
Make sure you're playing:
|
||||
- the latest version of the game (currently v0.8.1)
|
||||
- the latest version of the game (currently v0.8.5 for Android, v0.8.4 for all other platforms)
|
||||
- without any mods
|
||||
- on [Newgrounds](https://www.newgrounds.com/portal/view/770371) or downloaded from [itch.io](https://ninja-muffin24.itch.io/funkin)
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ Most of this functionality is only available on debug builds of the game!
|
||||
- `F3`: ***SCREENSHOT***: Takes a screenshot of the game and saves it to the local `screenshots` directory. Works outside of debug builds too!
|
||||
- `F4`: ***EJECT***: Forcibly switch state to the Main Menu (with no extra transition). Useful if you're stuck in a level and you need to get out!
|
||||
- `F5`: ***HOT RELOAD***: Forcibly reload the game's scripts and data files, then restart the current state. If any files in the `assets` folder have been modified, the game should process the changes for you! NOTE: Known bug, this does not reset song charts or song scripts, but it should reset everything else (such as stage layout data and character animation data).
|
||||
- `CTRL-ALT-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace. (Only works in the Main Menu on debug builds).
|
||||
|
||||
- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
|
||||
- `CTRL-ALT-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
|
||||
|
||||
## **Play State**
|
||||
- `H`: ***HIDE UI***: Makes the user interface invisible. Works in Pause Menu, great for screenshots.
|
||||
@@ -27,6 +25,7 @@ Most of this functionality is only available on debug builds of the game!
|
||||
- `P`: ***SWITCH TO PICO/BF***: Switch to Pico or BF (reopens Freeplay as the other character).
|
||||
- `T`: ***TEST RANK ANIMATION***: Play the Gold Perfect rank animation on the currently selected song.
|
||||
- `SHIFT-ACCEPT`: ***BOTPLAY MODE***: Start a song with Botplay mode enabled.
|
||||
- `CTRL-ACCEPT`: ***MIRRORED MODE***: Start a song with mirrored notes enabled.
|
||||
- `CTRL-Switch Song`: Play the first unlocked alternate instrumental as the song preview.
|
||||
|
||||
## **Title State**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "FlxPartialSound",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "26ed20b9c181a576f4cbe18ccdea38912c01476e",
|
||||
"ref": "2f984e244f1544ca98c0e03b9b21ae570f07ac55",
|
||||
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
|
||||
},
|
||||
{
|
||||
@@ -18,7 +18,7 @@
|
||||
"name": "extension-admob",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "4c69bb43f1fb89fd6003dc98d2f3a9d75c5364b6",
|
||||
"ref": "5c908b3f7c74115a8b52eddf1e05525f4873f26e",
|
||||
"url": "https://github.com/FunkinCrew/extension-admob"
|
||||
},
|
||||
{
|
||||
@@ -52,14 +52,14 @@
|
||||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "c8e79d2a42f9c11c639519ef2a6fd7ef2379a8aa",
|
||||
"ref": "f7b94eebf7dbb452a929d0c67ab31a9cbd71d3a0",
|
||||
"url": "https://github.com/FunkinCrew/flixel"
|
||||
},
|
||||
{
|
||||
"name": "flixel-addons",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "6fa30b3f5209146c852c25f3d1003e08898083e2",
|
||||
"ref": "187f93b34f93c6a405d634a42913c745e443463a",
|
||||
"url": "https://github.com/FunkinCrew/flixel-addons"
|
||||
},
|
||||
{
|
||||
@@ -97,7 +97,7 @@
|
||||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "088b153b35985f8e4419ef710057ebd5b3749513",
|
||||
"ref": "09f2512564b45e92229e21433e6dd482391f7b7d",
|
||||
"url": "https://github.com/FunkinCrew/haxeui-core"
|
||||
},
|
||||
{
|
||||
@@ -111,7 +111,7 @@
|
||||
"name": "hxcpp",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "5932340d095a7eea8635fe4d1355f1c0efd0b3c2",
|
||||
"ref": "450d112e50acff57b1bc9d584dcf1374c9e33995",
|
||||
"url": "https://github.com/FunkinCrew/hxcpp"
|
||||
},
|
||||
{
|
||||
@@ -146,7 +146,7 @@
|
||||
{
|
||||
"name": "hxvlc",
|
||||
"type": "haxelib",
|
||||
"version": "2.2.5"
|
||||
"version": "2.2.6"
|
||||
},
|
||||
{
|
||||
"name": "json2object",
|
||||
@@ -169,7 +169,7 @@
|
||||
"name": "lime",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "cae0c0bc10915f6281625d07e5527f3cecb4347f",
|
||||
"ref": "826d25199c17329b730ae09838f3df7a2903c471",
|
||||
"url": "https://github.com/FunkinCrew/lime"
|
||||
},
|
||||
{
|
||||
@@ -183,14 +183,14 @@
|
||||
"name": "openfl",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "5399946b68fd98f345d6b3e52b95ed6b78bf427c",
|
||||
"ref": "88534506595a32c3f02b21b3987e789a24074ae7",
|
||||
"url": "https://github.com/FunkinCrew/openfl"
|
||||
},
|
||||
{
|
||||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d8f036378c4ff472f1922ad623b8800b6a061c1a",
|
||||
"ref": "fb123758b90127f5647727727f273d27e93cabdb",
|
||||
"url": "https://github.com/FunkinCrew/polymod"
|
||||
},
|
||||
{
|
||||
|
||||
+26
-36
@@ -32,13 +32,13 @@ class Project extends HXProject
|
||||
* REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES!
|
||||
* You only have to change it here, the rest of the game will query this value.
|
||||
*/
|
||||
static final VERSION:String = "0.8.2";
|
||||
static final VERSION:String = "0.8.5";
|
||||
|
||||
/**
|
||||
* The build's number and version code.
|
||||
* Used when publishing the game to mobile app stores. Should increment with each patch.
|
||||
*/
|
||||
static final BUILD_NUMBER:Int = 76;
|
||||
static final BUILD_NUMBER:Int = 87;
|
||||
|
||||
/**
|
||||
* The game's name. Used as the default window title.
|
||||
@@ -114,11 +114,7 @@ class Project extends HXProject
|
||||
*/
|
||||
static final ANDROID_TARGET_SDK_VERSION:Int = 35;
|
||||
|
||||
static final ANDROID_EXTENSIONS:Array<String> = [
|
||||
"funkin.extensions.CallbackUtil",
|
||||
"funkin.extensions.FNFCExtension",
|
||||
"funkin.extensions.AudioSession"
|
||||
];
|
||||
static final ANDROID_EXTENSIONS:Array<String> = ["funkin.extensions.CallbackUtil", "funkin.extensions.FNFCExtension", "funkin.extensions.AudioSession"];
|
||||
|
||||
/**
|
||||
* The team ID to use for the iOS app. Configured in XCode.
|
||||
@@ -171,11 +167,7 @@ class Project extends HXProject
|
||||
/**
|
||||
* Asset path globs to exclude on platforms with `CENSOR_EXPLETIVES` disabled.
|
||||
*/
|
||||
static final EXCLUDE_ASSETS_UNCENSORED:Array<String> = [
|
||||
"stressCutscene-censored.mp4",
|
||||
"stressPicoCutscene-censored.mp4",
|
||||
"darnellCutscene-censored.mp4"
|
||||
];
|
||||
static final EXCLUDE_ASSETS_UNCENSORED:Array<String> = ["stressCutscene-censored.mp4", "stressPicoCutscene-censored.mp4", "darnellCutscene-censored.mp4"];
|
||||
|
||||
//
|
||||
// ASSET EMBEDDING
|
||||
@@ -869,10 +861,6 @@ class Project extends HXProject
|
||||
// FEATURE FLAGS
|
||||
//
|
||||
|
||||
// Should be true on debug builds or if GITHUB_BUILD is enabled.
|
||||
FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this));
|
||||
FEATURE_RESULTS_DEBUG.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this));
|
||||
|
||||
FEATURE_FUNKVIS.apply(this, true);
|
||||
FEATURE_LOG_TRACE.apply(this, true);
|
||||
FEATURE_DEBUG_FILE_LOGGING.apply(this, FEATURE_DEBUG_FUNCTIONS.isEnabled(this));
|
||||
@@ -894,7 +882,7 @@ class Project extends HXProject
|
||||
|
||||
// Should be true on desktop, non-tester builds.
|
||||
// We don't want testers to accidentally leak songs to their Discord friends!
|
||||
FEATURE_DISCORD_RPC.apply(this, isDesktop() && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this));
|
||||
FEATURE_DISCORD_RPC.apply(this, isDesktop() && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this) && !isHashLink());
|
||||
|
||||
FEATURE_LOST_FOCUS_VOLUME.apply(this, isDesktop());
|
||||
|
||||
@@ -1133,6 +1121,13 @@ class Project extends HXProject
|
||||
// Ensure all flixel-animate classes are available at runtime.
|
||||
addHaxeMacro("include('animate')");
|
||||
|
||||
// Ensure all openfl classes are available at runtime.
|
||||
addHaxeMacro("include('openfl')");
|
||||
|
||||
// Ensure all lime classes are available at runtime.
|
||||
// Explicitly ignore tools and internal packages.
|
||||
addHaxeMacro("include('lime', true, ['lime.tools.*', 'lime._internal.*'])");
|
||||
|
||||
// Ensure all Flixel classes are available at runtime.
|
||||
// Explicitly ignore packages which require additional dependencies.
|
||||
addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*', 'flixel.addons.tile.FlxRayCastTilemap' ])");
|
||||
@@ -1143,8 +1138,9 @@ class Project extends HXProject
|
||||
*/
|
||||
function configureCustomMacros()
|
||||
{
|
||||
// This macro allows addition of new functionality to existing Flixel. -->
|
||||
// These macros allow additions of new functionality to existing Flixel. -->
|
||||
addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')");
|
||||
addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxSprite())', 'flixel.FlxSprite')");
|
||||
}
|
||||
|
||||
function configureOutputDir()
|
||||
@@ -1228,21 +1224,12 @@ class Project extends HXProject
|
||||
intentFilter.addChild(category);
|
||||
}
|
||||
|
||||
final intentFilterDatas:Map<String, Array<String>> = [
|
||||
"android:mimeType" => ["application/zip"],
|
||||
// When this is added, it makes the game show up only when it's a FNFC file.
|
||||
final intentFilterDatas:Map<String,
|
||||
Array<String>> = ["android:mimeType" => ["application/zip"], // When this is added, it makes the game show up only when it's a FNFC file.
|
||||
// But it also for some reason make it not show up at all in ZArchiver no matter what the file is...
|
||||
// "android:host" => ["*"],
|
||||
"android:scheme" => ["file", "content"],
|
||||
// I think having all these makes it so the extension check would work even when the files has dots like "epic-song-v2.0.fnfc"
|
||||
"android:pathPattern" => [
|
||||
".*\\.fnfc",
|
||||
".*\\..*\\.fnfc",
|
||||
".*\\..*\\..*\\.fnfc",
|
||||
".*\\..*\\..*\\..*\\.fnfc",
|
||||
".*\\..*\\..*\\..*\\..*\\.fnfc"
|
||||
]
|
||||
];
|
||||
"android:scheme" => ["file", "content"], // I think having all these makes it so the extension check would work even when the files has dots like "epic-song-v2.0.fnfc"
|
||||
"android:pathPattern" => [".*\\.fnfc", ".*\\..*\\.fnfc", ".*\\..*\\..*\\.fnfc", ".*\\..*\\..*\\..*\\.fnfc", ".*\\..*\\..*\\..*\\..*\\.fnfc"]];
|
||||
|
||||
for (key => values in intentFilterDatas)
|
||||
{
|
||||
@@ -1351,12 +1338,12 @@ class Project extends HXProject
|
||||
addHaxelib('thx.core'); // General utility library, "the lodash of Haxe"
|
||||
addHaxelib('thx.semver'); // Version string handling
|
||||
|
||||
if (isDebug())
|
||||
if (isDebug() && isCPP())
|
||||
{
|
||||
addHaxelib('hxcpp-debug-server'); // VSCode debug support
|
||||
}
|
||||
|
||||
if (isDesktop() || isMobile() && !isHashLink() && FEATURE_VIDEO_PLAYBACK.isEnabled(this))
|
||||
if ((isDesktop() || isMobile()) && FEATURE_VIDEO_PLAYBACK.isEnabled(this))
|
||||
{
|
||||
// hxvlc doesn't function on HashLink or non-desktop platforms
|
||||
// It's also unnecessary if video playback is disabled
|
||||
@@ -1422,8 +1409,11 @@ class Project extends HXProject
|
||||
|
||||
function configureAssets()
|
||||
{
|
||||
// Delete the assets folder from the export folder before adding anything.
|
||||
clearAssets();
|
||||
if (!isDisplay())
|
||||
{
|
||||
// Delete the assets folder from the export folder before adding anything.
|
||||
clearAssets();
|
||||
}
|
||||
|
||||
var exclude = EXCLUDE_ASSETS.concat(isWeb() ? EXCLUDE_ASSETS_WEB : EXCLUDE_ASSETS_NATIVE);
|
||||
var shouldPreload = PRELOAD_ALL.isEnabled(this);
|
||||
@@ -1535,7 +1525,7 @@ class Project extends HXProject
|
||||
var exportPath:Null<String> = app.path ?? "";
|
||||
|
||||
// mac target uses `macos` for the export folder instead of `mac`
|
||||
var platform:String = (!isMac() ? Std.string(this.target) : 'macos');
|
||||
var platform:String = isHashLink() ? 'hl' : (!isMac() ? Std.string(this.target) : 'macos');
|
||||
var basePath:String = Path.join([exportPath, platform, "bin"]);
|
||||
var assetsPath:String = "";
|
||||
|
||||
|
||||
+23
-18
@@ -87,6 +87,21 @@ class Main extends Sprite
|
||||
removeEventListener(Event.ADDED_TO_STAGE, init);
|
||||
}
|
||||
|
||||
#if (sys && !mobile)
|
||||
// Force-kill the game to prevent background processing.
|
||||
Lib.current.stage.window.onClose.add(function()
|
||||
{
|
||||
trace(' EXITING '.bold().bg_red() + ' Game is exiting, cleaning up resources...');
|
||||
|
||||
#if hxvlc
|
||||
// Clean up VLC threads to prevent memory leaks.
|
||||
hxvlc.util.Handle.dispose();
|
||||
#end
|
||||
|
||||
Sys.exit(0);
|
||||
});
|
||||
#end
|
||||
|
||||
// Manually crash the game when using a software renderer in order to give a nicer error message.
|
||||
var context = stage.window.context.type;
|
||||
if (context != WEBGL && context != OPENGL && context != OPENGLES)
|
||||
@@ -149,20 +164,15 @@ class Main extends Sprite
|
||||
});
|
||||
#end
|
||||
|
||||
// Don't call anything from the preferences until the save is loaded!
|
||||
#if web
|
||||
// set this variable (which is a function) from the lime version at lime/_internal/backend/html5/HTML5Application.hx
|
||||
// The framerate cap will more thoroughly initialize via Preferences in InitState.hx
|
||||
funkin.Preferences.lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
|
||||
#end
|
||||
|
||||
WindowUtil.setVSyncMode(funkin.Preferences.vsyncMode);
|
||||
|
||||
// Force a `FunkinCamera` to be the default camera.
|
||||
// This allows the blend mode shader to work everywhere.
|
||||
untyped FlxG.cameras = new funkin.graphics.FunkinCameraFrontEnd();
|
||||
|
||||
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash,
|
||||
var framerate:Int = Preferences.unlockedFramerate ? 0 : Preferences.framerate;
|
||||
|
||||
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash,
|
||||
(FlxG.stage.window.fullscreen || Preferences.autoFullscreen));
|
||||
|
||||
// FlxG.game._customSoundTray wants just the class, it calls new from
|
||||
@@ -174,7 +184,7 @@ class Main extends Sprite
|
||||
addChild(game);
|
||||
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
|
||||
#if !FLX_NO_DEBUG game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil()); #end
|
||||
funkin.util.macro.ConsoleMacro.init();
|
||||
#end
|
||||
|
||||
@@ -236,14 +246,7 @@ class Main extends Sprite
|
||||
function repositionCounters(lerp:Bool):Void
|
||||
{
|
||||
// Calling this so it gets scaled based on the resolution of the game and device's resolution.
|
||||
var scale:Float = Math.min(FlxG.stage.stageWidth / FlxG.width, FlxG.stage.stageHeight / FlxG.height);
|
||||
|
||||
#if android
|
||||
scale = Math.max(scale, 1);
|
||||
#else
|
||||
scale = Math.min(scale, 1);
|
||||
#end
|
||||
final thypos:Float = Math.max(FullScreenScaleMode.notchSize.x, 10);
|
||||
var scale:Float = Math.max(Math.min(FlxG.stage.stageWidth / FlxG.width, FlxG.stage.stageHeight / FlxG.height), 1);
|
||||
|
||||
if (debugDisplay != null)
|
||||
{
|
||||
@@ -251,13 +254,15 @@ class Main extends Sprite
|
||||
|
||||
if (FlxG.game != null)
|
||||
{
|
||||
final thypos:Float = Math.max(FullScreenScaleMode.notchSize.x, 10);
|
||||
|
||||
if (lerp)
|
||||
{
|
||||
debugDisplay.x = flixel.math.FlxMath.lerp(debugDisplay.x, FlxG.game.x + thypos, FlxG.elapsed * 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
debugDisplay.x = FlxG.game.x + FullScreenScaleMode.notchSize.x + 10;
|
||||
debugDisplay.x = FlxG.game.x + thypos;
|
||||
}
|
||||
|
||||
debugDisplay.y = FlxG.game.y + (3 * scale);
|
||||
|
||||
+4
-2
@@ -2,6 +2,8 @@ package source; // Yeah, I know...
|
||||
|
||||
import sys.io.File;
|
||||
|
||||
using tools.AnsiUtil;
|
||||
|
||||
/**
|
||||
* A script which executes before the game is built.
|
||||
*/
|
||||
@@ -12,13 +14,13 @@ class Prebuild
|
||||
static function main():Void
|
||||
{
|
||||
var start:Float = Sys.time();
|
||||
// Sys.println('[INFO] Performing pre-build tasks...');
|
||||
// Sys.println(' INFO '.bold().bg_blue() + " Performing pre-build tasks...");
|
||||
|
||||
saveBuildTime();
|
||||
|
||||
var end:Float = Sys.time();
|
||||
var duration:Float = end - start;
|
||||
// Sys.println('[INFO] Finished pre-build tasks in $duration seconds.');
|
||||
// Sys.println(' INFO '.bold().bg_blue() + ' Finished pre-build tasks in $duration seconds.');
|
||||
}
|
||||
|
||||
static function saveBuildTime():Void
|
||||
|
||||
@@ -137,7 +137,7 @@ class InitState extends FlxState
|
||||
|
||||
#if mobile
|
||||
// Setup Mobile FNFC launcher.
|
||||
funkin.mobile.util.FNFCUtil.init();
|
||||
funkin.mobile.util.FNFCProvider.init();
|
||||
#end
|
||||
|
||||
SongEventHelper.generateEaseGraphsBitmaps();
|
||||
@@ -311,19 +311,34 @@ class InitState extends FlxState
|
||||
#if FEATURE_LOST_FOCUS_VOLUME
|
||||
@:noCompletion var _lastFocusVolume:Null<Float>;
|
||||
|
||||
function onLostFocus()
|
||||
function onLostFocus():Void
|
||||
{
|
||||
if (FlxG.sound.muted || FlxG.sound.volume == 0 || FlxG.autoPause) return;
|
||||
_lastFocusVolume = FlxG.sound.volume;
|
||||
FlxG.sound.volume *= Constants.LOST_FOCUS_VOLUME_MULTIPLIER;
|
||||
}
|
||||
#end
|
||||
|
||||
function onGainFocus()
|
||||
function onGainFocus():Void
|
||||
{
|
||||
#if !mobile
|
||||
if (Preferences.unlockedFramerate)
|
||||
{
|
||||
FlxG.updateFramerate = 0;
|
||||
FlxG.drawFramerate = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.updateFramerate = Preferences.framerate;
|
||||
FlxG.drawFramerate = Preferences.framerate;
|
||||
}
|
||||
#end
|
||||
|
||||
#if FEATURE_LOST_FOCUS_VOLUME
|
||||
if (FlxG.sound.muted || FlxG.autoPause) return;
|
||||
if (_lastFocusVolume != null) FlxG.sound.volume = _lastFocusVolume;
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
/**
|
||||
* Start the game.
|
||||
@@ -411,7 +426,6 @@ class InitState extends FlxState
|
||||
function startGameNormally():Void
|
||||
{
|
||||
var params:CLIParams = CLIUtil.processArgs();
|
||||
trace('Command line args: ${params}');
|
||||
|
||||
if (params.chart.shouldLoadChart)
|
||||
{
|
||||
@@ -445,7 +459,7 @@ class InitState extends FlxState
|
||||
{
|
||||
// FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
|
||||
#if mobile
|
||||
funkin.mobile.util.FNFCUtil.onFNFCOpen.add(function(fnfcFile:String)
|
||||
funkin.mobile.util.FNFCProvider.onFNFCOpen.add(function(fnfcFile:String)
|
||||
{
|
||||
flixel.tweens.FlxTween.globalManager.clear();
|
||||
flixel.util.FlxTimer.globalManager.clear();
|
||||
@@ -459,7 +473,7 @@ class InitState extends FlxState
|
||||
FlxG.switchState(() -> new ChartPlaytestMenu(fnfcFile));
|
||||
});
|
||||
|
||||
final fnfcFile = funkin.mobile.util.FNFCUtil.queryFNFC();
|
||||
final fnfcFile = funkin.mobile.util.FNFCProvider.queryFNFC();
|
||||
if (fnfcFile != null)
|
||||
{
|
||||
trace('launching FNFC from $fnfcFile');
|
||||
@@ -574,7 +588,7 @@ class InitState extends FlxState
|
||||
//
|
||||
// FLIXEL DEBUG SETUP
|
||||
//
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
#if (FEATURE_DEBUG_FUNCTIONS && !FLX_NO_DEBUG)
|
||||
trace('Initializing Flixel debugger...');
|
||||
|
||||
#if !debug
|
||||
|
||||
@@ -98,7 +98,7 @@ class PlayerSettings
|
||||
*/
|
||||
function new(id:Int)
|
||||
{
|
||||
trace('loading player settings for id: $id');
|
||||
trace('Loading player settings for id: $id');
|
||||
|
||||
this.id = id;
|
||||
this.controls = new Controls('player$id', None);
|
||||
|
||||
@@ -41,12 +41,23 @@ class Preferences
|
||||
{
|
||||
#if web
|
||||
return 60;
|
||||
#elseif mobile
|
||||
var refreshRate:Int = FlxG.stage.window.displayMode.refreshRate;
|
||||
|
||||
if (refreshRate < 60) refreshRate = 60;
|
||||
|
||||
return refreshRate;
|
||||
#else
|
||||
var save:Save = Save.instance;
|
||||
save.options.framerate = value;
|
||||
Save.system.flush();
|
||||
FlxG.updateFramerate = value;
|
||||
FlxG.drawFramerate = value;
|
||||
|
||||
if (!unlockedFramerate)
|
||||
{
|
||||
FlxG.updateFramerate = value;
|
||||
FlxG.drawFramerate = value;
|
||||
}
|
||||
|
||||
return value;
|
||||
#end
|
||||
}
|
||||
@@ -69,13 +80,13 @@ class Preferences
|
||||
static function set_naughtyness(value:Bool):Bool
|
||||
{
|
||||
#if NO_FEATURE_NAUGHTYNESS
|
||||
value = false;
|
||||
#end
|
||||
|
||||
return false;
|
||||
#else
|
||||
var save:Save = Save.instance;
|
||||
save.options.naughtyness = value;
|
||||
Save.system.flush();
|
||||
return value;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,19 +157,33 @@ class Preferences
|
||||
{
|
||||
#if NO_FEATURE_DEBUG_DISPLAY
|
||||
return DebugDisplayMode.Off;
|
||||
#else
|
||||
#if hl
|
||||
// Account for when debugDisplay used to be a boolean
|
||||
var options:Null<SaveDataOptions> = Save.instance?.options;
|
||||
if (options != null && Std.isOfType(options.debugDisplay, Bool))
|
||||
{
|
||||
var convertedDebugDisplay = cast(options.debugDisplay, Bool) ? DebugDisplayMode.Simple : DebugDisplayMode.Off;
|
||||
options.debugDisplay = convertedDebugDisplay;
|
||||
Save.system.flush();
|
||||
}
|
||||
#end
|
||||
|
||||
return Save?.instance?.options?.debugDisplay ?? 'Off';
|
||||
#end
|
||||
}
|
||||
|
||||
static function set_debugDisplay(value:DebugDisplayMode):DebugDisplayMode
|
||||
{
|
||||
#if NO_FEATURE_DEBUG_DISPLAY
|
||||
return DebugDisplayMode.Off;
|
||||
#else
|
||||
if (value != Save.instance.options.debugDisplay) setDebugDisplayMode(value);
|
||||
|
||||
var save = Save.instance;
|
||||
save.options.debugDisplay = value;
|
||||
Save.system.flush();
|
||||
return value;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,18 +278,23 @@ class Preferences
|
||||
{
|
||||
#if mobile
|
||||
return false;
|
||||
#end
|
||||
#else
|
||||
return Save?.instance?.options?.autoPause ?? true;
|
||||
#end
|
||||
}
|
||||
|
||||
static function set_autoPause(value:Bool):Bool
|
||||
{
|
||||
#if mobile
|
||||
return false;
|
||||
#else
|
||||
if (value != Save.instance.options.autoPause) FlxG.autoPause = value;
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.autoPause = value;
|
||||
Save.system.flush();
|
||||
return value;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,6 +344,9 @@ class Preferences
|
||||
|
||||
static function get_vsyncMode():lime.ui.WindowVSyncMode
|
||||
{
|
||||
#if (mobile || web)
|
||||
return lime.ui.WindowVSyncMode.OFF;
|
||||
#else
|
||||
var value = Save?.instance?.options?.vsyncMode ?? "Off";
|
||||
|
||||
return switch (value)
|
||||
@@ -327,10 +360,14 @@ class Preferences
|
||||
default:
|
||||
lime.ui.WindowVSyncMode.OFF;
|
||||
};
|
||||
#end
|
||||
}
|
||||
|
||||
static function set_vsyncMode(value:lime.ui.WindowVSyncMode):lime.ui.WindowVSyncMode
|
||||
{
|
||||
#if (mobile || web)
|
||||
return lime.ui.WindowVSyncMode.OFF;
|
||||
#else
|
||||
var string;
|
||||
|
||||
switch (value)
|
||||
@@ -351,51 +388,37 @@ class Preferences
|
||||
save.options.vsyncMode = string;
|
||||
Save.system.flush();
|
||||
return value;
|
||||
#end
|
||||
}
|
||||
|
||||
public static var unlockedFramerate(get, set):Bool;
|
||||
|
||||
static function get_unlockedFramerate():Bool
|
||||
{
|
||||
#if (mobile || web)
|
||||
return false;
|
||||
#else
|
||||
return Save?.instance?.options?.unlockedFramerate ?? false;
|
||||
#end
|
||||
}
|
||||
|
||||
static function set_unlockedFramerate(value:Bool):Bool
|
||||
{
|
||||
#if (mobile || web)
|
||||
return false;
|
||||
#else
|
||||
if (value != Save.instance.options.unlockedFramerate)
|
||||
{
|
||||
#if web
|
||||
toggleFramerateCap(value);
|
||||
#end
|
||||
}
|
||||
|
||||
var save:Save = Save.instance;
|
||||
save.options.unlockedFramerate = value;
|
||||
Save.system.flush();
|
||||
return value;
|
||||
#end
|
||||
}
|
||||
|
||||
#if web
|
||||
// We create a haxe version of this just for readability.
|
||||
// We use these to override `window.requestAnimationFrame` in Javascript to uncap the framerate / "animation" request rate
|
||||
// Javascript is crazy since u can just do stuff like that lol
|
||||
|
||||
public static function unlockedFramerateFunction(callback, element)
|
||||
{
|
||||
var currTime = Date.now().getTime();
|
||||
var timeToCall = 0;
|
||||
var id = js.Browser.window.setTimeout(function()
|
||||
{
|
||||
callback(currTime + timeToCall);
|
||||
}, timeToCall);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Lime already implements their own little framerate cap, so we can just use that
|
||||
// This also gets set in the init function in Main.hx, since we need to definitely override it
|
||||
public static var lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
|
||||
#end
|
||||
|
||||
/**
|
||||
* If >0, the game will display a semi-opaque background under the notes.
|
||||
* `0` for no background, `100` for solid black if you're freaky like that
|
||||
@@ -485,9 +508,7 @@ class Preferences
|
||||
setDebugDisplayMode(Preferences.debugDisplay);
|
||||
setDebugDisplayBGOpacity(Preferences.debugDisplayBGOpacity / 100);
|
||||
|
||||
#if web
|
||||
toggleFramerateCap(Preferences.unlockedFramerate);
|
||||
#end
|
||||
|
||||
#if mobile
|
||||
// Apply the allowScreenTimeout setting.
|
||||
@@ -497,9 +518,9 @@ class Preferences
|
||||
|
||||
static function toggleFramerateCap(unlocked:Bool):Void
|
||||
{
|
||||
#if web
|
||||
var framerateFunction = unlocked ? unlockedFramerateFunction : lockedFramerateFunction;
|
||||
untyped js.Syntax.code("window.requestAnimationFrame = framerateFunction;");
|
||||
#if !(mobile || web)
|
||||
FlxG.drawFramerate = unlocked ? 0 : framerate;
|
||||
FlxG.updateFramerate = unlocked ? 0 : framerate;
|
||||
#end
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@ import funkin.audio.waveform.WaveformData;
|
||||
@:nullSafety
|
||||
class VoicesGroup extends SoundGroup
|
||||
{
|
||||
/**
|
||||
* Whenever or not the game is using the legacy vocals system (shared Voices.ogg)
|
||||
*/
|
||||
public var legacyVoiceSystem:Bool = false;
|
||||
|
||||
public var legacyVoiceUsesPlayer:Bool = false;
|
||||
|
||||
var playerVoices:Null<FlxTypedGroup<FunkinSound>>;
|
||||
var opponentVoices:Null<FlxTypedGroup<FunkinSound>>;
|
||||
|
||||
|
||||
@@ -3,10 +3,14 @@ package funkin.audio.waveform;
|
||||
@:nullSafety
|
||||
class WaveformDataParser
|
||||
{
|
||||
@:native("int16_max")
|
||||
static final INT16_MAX:Int = 32767;
|
||||
@:native("int16_min")
|
||||
static final INT16_MIN:Int = -32768;
|
||||
|
||||
@:native("int8_max")
|
||||
static final INT8_MAX:Int = 127;
|
||||
@:native("int8_min")
|
||||
static final INT8_MIN:Int = -128;
|
||||
|
||||
public static function interpretFlxSound(sound:Null<flixel.sound.FlxSound>):Null<WaveformData>
|
||||
|
||||
@@ -53,7 +53,7 @@ class DataError
|
||||
case TEnum(c):
|
||||
trace(' ERROR '.error() + '(${Type.getEnumName(c)}) ${e.toString()}');
|
||||
default:
|
||||
trace(' ERROR '.error() + '(${Type.typeof(e)}) ${e.toString()}');
|
||||
trace(' ERROR '.error() + '(${Type.typeof(e)}) ${Std.string(e)}');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package funkin.data.event;
|
||||
|
||||
import flixel.util.FlxSort;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.event.ScriptedSongEvent;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.util.macro.ClassMacro;
|
||||
|
||||
/**
|
||||
* This class statically handles the parsing of internal and scripted song event handlers.
|
||||
@@ -133,24 +135,38 @@ class SongEventRegistry
|
||||
}
|
||||
|
||||
/**
|
||||
* Caching the index for the next event to query greatly reduces lag.
|
||||
* Kinda nasty that it's tied to a static class though.
|
||||
*/
|
||||
static var nextEventIndex:Int = 0;
|
||||
|
||||
/**
|
||||
* Retrieve the list of events to activate this frame.
|
||||
*
|
||||
* @param events The list of available song events.
|
||||
* @param currentTime The current time in milliseconds.
|
||||
* @param startIndex The index to start querying from.
|
||||
* Defaults to the index of the last event handled.
|
||||
* @return The list of events which haven't been handled yet.
|
||||
*/
|
||||
public static function queryEvents(events:Array<SongEventData>, currentTime:Float):Array<SongEventData>
|
||||
public static function queryEvents(events:Array<SongEventData>, currentTime:Float, ?startIndex:Int):Array<SongEventData>
|
||||
{
|
||||
var result:Array<SongEventData> = events.filter(function(event:SongEventData):Bool
|
||||
startIndex ??= nextEventIndex;
|
||||
|
||||
var result:Array<SongEventData> = [];
|
||||
|
||||
for (i in startIndex...events.length)
|
||||
{
|
||||
// If the event is already activated, don't activate it again.
|
||||
if (event.activated) return false;
|
||||
if (events[i].activated) continue;
|
||||
|
||||
// If the event is in the future, don't activate it.
|
||||
if (event.time > currentTime) return false;
|
||||
if (events[i].time > currentTime)
|
||||
{
|
||||
nextEventIndex = i;
|
||||
return result;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
result.sort(SortUtil.eventDataByTime.bind(FlxSort.ASCENDING));
|
||||
result.push(events[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -183,10 +199,26 @@ class SongEventRegistry
|
||||
*/
|
||||
public static function resetEvents(events:Array<SongEventData>):Void
|
||||
{
|
||||
events.sort(SortUtil.eventDataByTime.bind(FlxSort.ASCENDING));
|
||||
nextEventIndex = 0;
|
||||
allEventHandlers.resize(0);
|
||||
|
||||
for (event in events)
|
||||
{
|
||||
event.activated = false;
|
||||
// TODO: Add an onReset() method to SongEvent?
|
||||
|
||||
var handler:Null<SongEvent> = getEvent(event.eventKind);
|
||||
if (handler != null && !allEventHandlers.contains(handler)) allEventHandlers.push(handler);
|
||||
}
|
||||
}
|
||||
|
||||
static var allEventHandlers:Array<SongEvent> = [];
|
||||
|
||||
public static inline function callEvent(scriptEvent:ScriptEvent):Void
|
||||
{
|
||||
for (event in allEventHandlers)
|
||||
{
|
||||
ScriptEventDispatcher.callEvent(event, scriptEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,4 +49,10 @@ typedef AlbumData =
|
||||
@:optional
|
||||
@:default([])
|
||||
public var albumTitleAnimations:Array<AnimationData>;
|
||||
|
||||
/**
|
||||
* An optional override for the Freeplay OST text.
|
||||
*/
|
||||
@:optional
|
||||
public var albumOSTName:String;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class AlbumRegistry extends BaseRegistry<Album, AlbumData, AlbumEntryParams> imp
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateAlbumData()` function.
|
||||
*/
|
||||
public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.0';
|
||||
public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.3';
|
||||
|
||||
public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x';
|
||||
|
||||
|
||||
@@ -5,5 +5,17 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.3]
|
||||
### Added
|
||||
- Added `albumOSTName` to change the OST at the top right of the Freeplay menu.
|
||||
|
||||
## [1.0.2]
|
||||
### Added
|
||||
- Added `albumTitleOffsets` to change the horizontal and vertical position of the album title.
|
||||
|
||||
## [1.0.1]
|
||||
### Added
|
||||
- Added `albumTitleAnimations` to modify animation data for the album art.
|
||||
|
||||
## [1.0.0]
|
||||
Initial release.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package funkin.data.song;
|
||||
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import thx.semver.Version;
|
||||
import funkin.util.tools.ICloneable;
|
||||
import funkin.play.notes.notekind.NoteKind;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
|
||||
/**
|
||||
* Data containing information about a song.
|
||||
@@ -119,6 +122,18 @@ class SongMetadata implements ICloneable<SongMetadata>
|
||||
// Update generatedBy and version before writing.
|
||||
updateVersionToLatest();
|
||||
|
||||
// This might not be a very wise hack, but it works.
|
||||
#if FEATURE_CHART_EDITOR
|
||||
if (Std.isOfType(FlxG.state, ChartEditorState))
|
||||
{
|
||||
var state:ChartEditorState = cast(FlxG.state, ChartEditorState);
|
||||
|
||||
final songLength:Float = @:privateAccess state.songLengthInMs;
|
||||
if (playData.previewStart > 1) playData.previewStart /= songLength;
|
||||
if (playData.previewEnd > 1) playData.previewEnd /= songLength;
|
||||
}
|
||||
#end
|
||||
|
||||
var ignoreNullOptionals = true;
|
||||
var writer = new json2object.JsonWriter<SongMetadata>(ignoreNullOptionals);
|
||||
// I believe @:jignored should be ignored by the writer?
|
||||
@@ -483,17 +498,17 @@ class SongPlayData implements ICloneable<SongPlayData>
|
||||
* @since `2.2.2`
|
||||
*/
|
||||
@:optional
|
||||
@:default(0)
|
||||
public var previewStart:Int;
|
||||
@:default(funkin.util.Constants.DEFAULT_PREVIEW_START_TIME)
|
||||
public var previewStart:Float;
|
||||
|
||||
/**
|
||||
* The end time for the audio preview in Freeplay.
|
||||
* Defaults to 15 seconds in.
|
||||
* Defaults to 20% seconds in.
|
||||
* @since `2.2.2`
|
||||
*/
|
||||
@:optional
|
||||
@:default(15000)
|
||||
public var previewEnd:Int;
|
||||
@:default(funkin.util.Constants.DEFAULT_PREVIEW_END_TIME)
|
||||
public var previewEnd:Float;
|
||||
|
||||
public function new()
|
||||
{
|
||||
@@ -848,7 +863,7 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
public function getInt(key:String):Null<Int>
|
||||
{
|
||||
if (this.value == null) return null;
|
||||
var result = Reflect.field(this.value, key);
|
||||
var result:Any = Reflect.field(this.value, key);
|
||||
if (result == null) return null;
|
||||
if (Std.isOfType(result, Int)) return result;
|
||||
if (Std.isOfType(result, String)) return Std.parseInt(cast result);
|
||||
@@ -863,7 +878,7 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
public function getFloat(key:String):Null<Float>
|
||||
{
|
||||
if (this.value == null) return null;
|
||||
var result = Reflect.field(this.value, key);
|
||||
var result:Any = Reflect.field(this.value, key);
|
||||
if (result == null) return null;
|
||||
if (Std.isOfType(result, Float)) return result;
|
||||
if (Std.isOfType(result, String)) return Std.parseFloat(cast result);
|
||||
@@ -1303,12 +1318,15 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
*/
|
||||
public function buildTooltip():String
|
||||
{
|
||||
if ((this.kind?.length ?? 0) == 0) return "";
|
||||
if ((this.kind?.length ?? 0) == 0) return '';
|
||||
|
||||
var result:String = 'Kind: ${this.kind}';
|
||||
var noteKind:Null<NoteKind> = NoteKindManager.getNoteKind(this.kind);
|
||||
var noteKindDesc:String = noteKind?.description ?? this.kind;
|
||||
|
||||
var result:String = 'Kind: $noteKindDesc';
|
||||
if (this.params.length == 0) return result;
|
||||
|
||||
result += "\nParams:";
|
||||
result += '\nParams:';
|
||||
|
||||
for (param in params)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ typedef OsuManiaData =
|
||||
var General:
|
||||
{
|
||||
var PreviewTime:Int;
|
||||
var Mode:Int; // Should only be 3 if you are sure this is a mania beatmap
|
||||
};
|
||||
var Editor:
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ class OsuManiaImporter
|
||||
{
|
||||
var parts:Array<String> = line.split(":");
|
||||
var key:String = StringTools.trim(parts.shift());
|
||||
var value:String = StringTools.trim(parts.join(":"));
|
||||
var value:Any = parseValue(StringTools.trim(parts.join(":")));
|
||||
if (Reflect.field(result, currentSection) == null) Reflect.setField(result, currentSection, {
|
||||
});
|
||||
Reflect.setField(Reflect.field(result, currentSection), key, value);
|
||||
@@ -200,4 +200,22 @@ class OsuManiaImporter
|
||||
return new ManiaHitObject(time, noteD, holdDuration);
|
||||
});
|
||||
}
|
||||
|
||||
static function parseValue(v:String):Any
|
||||
{
|
||||
var result:Null<Any>;
|
||||
|
||||
if (v.contains('.'))
|
||||
{
|
||||
result = Std.parseFloat(v);
|
||||
if (!Math.isNaN(result)) return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Std.parseInt(v);
|
||||
if (result != null) return result;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.2]
|
||||
### Added
|
||||
- Added `capsule` field to change the Level's capsule name on the Freeplay menu.
|
||||
|
||||
## [1.0.1]
|
||||
### Added
|
||||
- Added `visible` attribute.
|
||||
|
||||
## [1.0.0]
|
||||
|
||||
@@ -21,6 +21,13 @@ typedef LevelData =
|
||||
*/
|
||||
var name:String;
|
||||
|
||||
/**
|
||||
* Optional data for how a level title is labeled on a capsule.
|
||||
* Used when displaying songs in Freeplay.
|
||||
*/
|
||||
@:optional
|
||||
var capsule:Null<LevelCapsuleData>;
|
||||
|
||||
/**
|
||||
* The graphic for the level, as seen in the scrolling list.
|
||||
*/
|
||||
@@ -56,6 +63,24 @@ typedef LevelData =
|
||||
var background:String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for a level capsule text.
|
||||
*/
|
||||
typedef LevelCapsuleData =
|
||||
{
|
||||
/**
|
||||
* The label for the capsule text.
|
||||
*/
|
||||
@:optional
|
||||
var name:String;
|
||||
|
||||
/**
|
||||
* The offset on the position to render the capsule text at.
|
||||
*/
|
||||
@:optional
|
||||
var offsets:Array<Float>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for a single prop for a story mode level.
|
||||
*/
|
||||
@@ -129,4 +154,11 @@ typedef LevelPropData =
|
||||
@:default(false)
|
||||
@:optional
|
||||
var flipX:Null<Bool>;
|
||||
|
||||
/**
|
||||
* Flips the sprite on Y axis.
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var flipY:Null<Bool>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class LevelRegistry extends BaseRegistry<Level, LevelData, LevelEntryParams> imp
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateLevelData()` function.
|
||||
*/
|
||||
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.1";
|
||||
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.2";
|
||||
|
||||
public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = ">=1.0.0 <1.1.0";
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class FunkTrail extends FlxTrail
|
||||
* @param alpha The alpha value for the very first trailsprite.
|
||||
* @param diff How much lower the alpha of the next trailsprite is.
|
||||
*/
|
||||
public function new(target:FlxSprite, ?graphic:FlxGraphicAsset, length:Int = 10, delay:Int = 3, alpha:Float = 0.4, diff:Float = 0.05)
|
||||
public function new(target:FlxSprite, ?graphic:FlxGraphicAsset, length:Int = 10, delay:Float = 0.1, alpha:Float = 0.4, diff:Float = 0.05)
|
||||
{
|
||||
super(target, graphic, length, delay, alpha, diff);
|
||||
}
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@ package funkin.external.apple;
|
||||
* A utility class for FNFC loading on apple devices.
|
||||
*/
|
||||
@:build(funkin.util.macro.LinkerMacro.xml('project/Build.xml'))
|
||||
@:include('FNFCUtil.hpp')
|
||||
@:include('FNFCExtern.hpp')
|
||||
@:unreflective
|
||||
extern class FNFCUtil
|
||||
extern class FNFCExtern
|
||||
{
|
||||
/**
|
||||
* Copies the FNFC resource from the specified URL into the cache.
|
||||
@@ -15,7 +15,7 @@ extern class FNFCUtil
|
||||
* @param url The URL of the FNFC resource to copy into the cache.
|
||||
* @param callback A function to be called when the copy operation completes.
|
||||
*/
|
||||
@:native('Apple_FNFCUtil_CopyFNFCIntoCache')
|
||||
@:native('Apple_FNFCExtern_CopyFNFCIntoCache')
|
||||
static function copyFNFCIntoCache(url:cpp.ConstCharStar, callback:cpp.Callable<(event:cpp.ConstCharStar, value:cpp.ConstCharStar) -> Void>):Void;
|
||||
}
|
||||
#end
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
<file name="${this_dir}/src/AudioSession.mm" />
|
||||
<file name="${this_dir}/src/MemoryUtil.mm" />
|
||||
<file name="${this_dir}/src/ScreenUtil.mm" />
|
||||
<file name="${this_dir}/src/FNFCUtil.mm" />
|
||||
<file name="${this_dir}/src/FNFCExtern.mm" />
|
||||
</files>
|
||||
|
||||
<target id="haxe">
|
||||
|
||||
+1
-1
@@ -14,4 +14,4 @@ typedef void (*FNFCCallback)(const char* event, const char* value);
|
||||
* @param url The URL of the FNFC resource to copy into the cache.
|
||||
* @param callback A function to be called when the copy operation completes.
|
||||
*/
|
||||
void Apple_FNFCUtil_CopyFNFCIntoCache(const char *url, FNFCCallback callback);
|
||||
void Apple_FNFCExtern_CopyFNFCIntoCache(const char *url, FNFCCallback callback);
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
#include "FNFCUtil.hpp"
|
||||
#include "FNFCExtern.hpp"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
static FNFCCallback fnfcCallback = nullptr;
|
||||
|
||||
void Apple_FNFCUtil_CopyFNFCIntoCache(const char *url, FNFCCallback callback)
|
||||
void Apple_FNFCExtern_CopyFNFCIntoCache(const char *url, FNFCCallback callback)
|
||||
{
|
||||
#if TARGET_OS_IOS
|
||||
if (!fnfcCallback)
|
||||
+27
-16
@@ -11,16 +11,18 @@ void Apple_ScreenUtil_GetSafeAreaInsets(double* top, double* bottom, double* lef
|
||||
#if TARGET_OS_IOS
|
||||
if (@available(iOS 11, *))
|
||||
{
|
||||
UIWindow* window = [UIApplication sharedApplication].windows[0];
|
||||
UIEdgeInsets safeAreaInsets = window.safeAreaInsets;
|
||||
float scale = [UIScreen mainScreen].scale;
|
||||
UIWindow *window = [UIApplication sharedApplication].windows[0];
|
||||
if (window)
|
||||
{
|
||||
UIEdgeInsets insets = window.safeAreaInsets;
|
||||
CGFloat scale = window.screen.nativeScale;
|
||||
|
||||
(*top) = safeAreaInsets.top * scale;
|
||||
(*bottom) = safeAreaInsets.bottom * scale;
|
||||
(*left) = safeAreaInsets.left * scale;
|
||||
(*right) = safeAreaInsets.right * scale;
|
||||
|
||||
return;
|
||||
(*top) = (double)(insets.top * scale);
|
||||
(*bottom) = (double)(insets.bottom * scale);
|
||||
(*left) = (double)(insets.left * scale);
|
||||
(*right) = (double)(insets.right * scale);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -33,13 +35,22 @@ void Apple_ScreenUtil_GetSafeAreaInsets(double* top, double* bottom, double* lef
|
||||
void Apple_ScreenUtil_GetScreenSize(double* width, double* height)
|
||||
{
|
||||
#if TARGET_OS_IOS
|
||||
CGRect screenRect = [[UIScreen mainScreen] bounds];
|
||||
float scale = [UIScreen mainScreen].scale;
|
||||
if (@available(iOS 11, *))
|
||||
{
|
||||
UIWindow *window = [UIApplication sharedApplication].windows[0];
|
||||
if (window && window.rootViewController)
|
||||
{
|
||||
UIView *view = window.rootViewController.view;
|
||||
CGSize size = view.bounds.size;
|
||||
CGFloat scale = window.screen.nativeScale;
|
||||
|
||||
(*width) = (double)screenRect.size.width * scale;
|
||||
(*height) = (double)screenRect.size.height * scale;
|
||||
#else
|
||||
(*width) = 0.0;
|
||||
(*width) = 0.0;
|
||||
(*width) = (double)((int)(size.width * scale));
|
||||
(*height) = (double)((int)(size.height * scale));
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
(*width) = 0.0;
|
||||
(*height) = 0.0;
|
||||
}
|
||||
|
||||
-47
@@ -8,36 +8,6 @@ package funkin.external.windows;
|
||||
@:include('winapi.hpp')
|
||||
extern class WinAPI
|
||||
{
|
||||
/**
|
||||
* Shows a message box with an error icon.
|
||||
*
|
||||
* @param handle A handle to the parent window.
|
||||
* @param message The message to display.
|
||||
* @param title The title of the message box.
|
||||
*/
|
||||
@:native('WINAPI_ShowError')
|
||||
static function showError(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
|
||||
/**
|
||||
* Shows a message box with a warning icon.
|
||||
*
|
||||
* @param handle A handle to the parent window.
|
||||
* @param message The message to display.
|
||||
* @param title The title of the message box.
|
||||
*/
|
||||
@:native('WINAPI_ShowWarning')
|
||||
static function showWarning(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
|
||||
/**
|
||||
* Shows a message box with an information icon.
|
||||
*
|
||||
* @param handle A handle to the parent window.
|
||||
* @param message The message to display.
|
||||
* @param title The title of the message box.
|
||||
*/
|
||||
@:native('WINAPI_ShowInformation')
|
||||
static function showInformation(handle:cpp.RawPointer<cpp.Void>, message:cpp.ConstCharStar, title:cpp.ConstCharStar):Void;
|
||||
|
||||
/**
|
||||
* Disables the "Report to Microsoft" dialog that appears when the application crashes.
|
||||
*/
|
||||
@@ -57,22 +27,5 @@ extern class WinAPI
|
||||
*/
|
||||
@:native('WINAPI_GetProcessMemoryWorkingSetSize')
|
||||
static function getProcessMemoryWorkingSetSize():cpp.SizeT;
|
||||
|
||||
/**
|
||||
* Sets the dark mode for the active window.
|
||||
*
|
||||
* @param handle A handle to the parent window.
|
||||
* @param enable Whether to enable or disable dark mode.
|
||||
*/
|
||||
@:native('WINAPI_SetDarkMode')
|
||||
static function setDarkMode(handle:cpp.RawPointer<cpp.Void>, 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
|
||||
|
||||
@@ -1,32 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Shows an error message box.
|
||||
*
|
||||
* @param handle A handle to the parent window
|
||||
* @param message The error message to display
|
||||
* @param title The title of the message box
|
||||
*/
|
||||
void WINAPI_ShowError(void* handle, const char *message, const char *title);
|
||||
|
||||
/**
|
||||
* Shows a warning message box.
|
||||
*
|
||||
* @param handle A handle to the parent window
|
||||
* @param message The warning message to display
|
||||
* @param title The title of the message box
|
||||
*/
|
||||
void WINAPI_ShowWarning(void* handle, const char *message, const char *title);
|
||||
|
||||
/**
|
||||
* Shows an information message box.
|
||||
*
|
||||
* @param handle A handle to the parent window
|
||||
* @param message The information message to display
|
||||
* @param title The title of the message box
|
||||
*/
|
||||
void WINAPI_ShowInformation(void* handle, const char *message, const char *title);
|
||||
|
||||
/**
|
||||
* Disables Windows error reporting dialogs.
|
||||
*/
|
||||
@@ -45,18 +18,3 @@ void WINAPI_DisableWindowsGhosting();
|
||||
* @return The working set size in bytes. Returns 0 if the query fails.
|
||||
*/
|
||||
size_t WINAPI_GetProcessMemoryWorkingSetSize();
|
||||
|
||||
/**
|
||||
* Sets dark mode on a window
|
||||
* @param handle A handle to the parent window.
|
||||
*
|
||||
* @param enable True to enable dark mode, false to disable
|
||||
*/
|
||||
void WINAPI_SetDarkMode(void* handle, bool enable);
|
||||
|
||||
/**
|
||||
* Checks if the system is using dark mode.
|
||||
*
|
||||
* @return True if system is in dark mode, false otherwise
|
||||
*/
|
||||
bool WINAPI_IsSystemDarkMode();
|
||||
|
||||
@@ -11,37 +11,6 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void WINAPI_ShowError(void* handle, const char *message, const char *title)
|
||||
{
|
||||
HWND hwnd = (HWND)handle;
|
||||
|
||||
if (!IsWindow(hwnd))
|
||||
hwnd = nullptr;
|
||||
|
||||
MessageBox(hwnd, message, title, MB_OK | MB_ICONERROR);
|
||||
}
|
||||
|
||||
void WINAPI_ShowWarning(void* handle, const char *message, const char *title)
|
||||
{
|
||||
HWND hwnd = (HWND)handle;
|
||||
|
||||
if (!IsWindow(hwnd))
|
||||
hwnd = nullptr;
|
||||
|
||||
MessageBox(hwnd, message, title, MB_OK | MB_ICONWARNING);
|
||||
}
|
||||
|
||||
void WINAPI_ShowInformation(void* handle, const char *message, const char *title)
|
||||
{
|
||||
HWND hwnd = (HWND)handle;
|
||||
|
||||
if (!IsWindow(hwnd))
|
||||
hwnd = nullptr;
|
||||
|
||||
MessageBox(hwnd, message, title, MB_OK | MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
|
||||
void WINAPI_DisableErrorReporting()
|
||||
{
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
|
||||
@@ -61,39 +30,3 @@ size_t WINAPI_GetProcessMemoryWorkingSetSize()
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void WINAPI_SetDarkMode(void* handle, bool enable)
|
||||
{
|
||||
HWND hwnd = (HWND)handle;
|
||||
|
||||
if (!IsWindow(hwnd))
|
||||
return;
|
||||
|
||||
int darkMode = enable ? 1 : 0;
|
||||
|
||||
if (DwmSetWindowAttribute(hwnd, 20, &darkMode, sizeof(darkMode)) != S_OK)
|
||||
DwmSetWindowAttribute(hwnd, 19, &darkMode, sizeof(darkMode));
|
||||
|
||||
UpdateWindow(hwnd);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,420 +0,0 @@
|
||||
package funkin.graphics;
|
||||
|
||||
import flixel.FlxBasic;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.FlxG;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.math.FlxMatrix;
|
||||
import flixel.math.FlxRect;
|
||||
import lime.graphics.cairo.Cairo;
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.display.BlendMode;
|
||||
import openfl.display.DisplayObjectRenderer;
|
||||
import openfl.display.Graphics;
|
||||
import openfl.display.OpenGLRenderer;
|
||||
import openfl.display3D.Context3D;
|
||||
import openfl.filters.BitmapFilter;
|
||||
import openfl.geom.ColorTransform;
|
||||
import openfl.geom.Matrix;
|
||||
import openfl.geom.Rectangle;
|
||||
#if (js && html5)
|
||||
import lime._internal.graphics.ImageCanvasUtil;
|
||||
import openfl.display.CanvasRenderer;
|
||||
import openfl.display._internal.CanvasGraphics as GfxRenderer;
|
||||
#else
|
||||
import openfl.display.CairoRenderer;
|
||||
import openfl.display._internal.CairoGraphics as GfxRenderer;
|
||||
#end
|
||||
|
||||
/**
|
||||
* A modified `FlxSprite` that supports filters.
|
||||
* The name's pretty much self-explanatory.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(openfl.geom.Rectangle)
|
||||
@:access(openfl.filters.BitmapFilter)
|
||||
@:access(flixel.graphics.frames.FlxFrame)
|
||||
class FlxFilteredSprite extends FlxSprite
|
||||
{
|
||||
@:noCompletion var _renderer:FlxAnimateFilterRenderer = new FlxAnimateFilterRenderer();
|
||||
|
||||
@:noCompletion var _filterMatrix:FlxMatrix = new FlxMatrix();
|
||||
|
||||
/**
|
||||
* An `Array` of shader filters (aka `BitmapFilter`).
|
||||
*/
|
||||
public var filters(default, set):Null<Array<BitmapFilter>>;
|
||||
|
||||
/**
|
||||
* a flag to update the image with the filters.
|
||||
* Useful when trying to render a shader at all times.
|
||||
*/
|
||||
public var filterDirty:Bool = false;
|
||||
|
||||
@:noCompletion var filtered:Bool = false;
|
||||
|
||||
// These appear to be a little troublesome to null safe.
|
||||
@:nullSafety(Off)
|
||||
@:noCompletion var _blankFrame:FlxFrame;
|
||||
|
||||
@:nullSafety(Off)
|
||||
var _filterBmp1:BitmapData;
|
||||
@:nullSafety(Off)
|
||||
var _filterBmp2:BitmapData;
|
||||
|
||||
override public function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
if (!filterDirty && filters != null)
|
||||
{
|
||||
for (filter in filters)
|
||||
{
|
||||
if (filter.__renderDirty)
|
||||
{
|
||||
filterDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
override function initVars():Void
|
||||
{
|
||||
super.initVars();
|
||||
_filterMatrix = new FlxMatrix();
|
||||
filters = null;
|
||||
filtered = false;
|
||||
}
|
||||
|
||||
override public function draw():Void
|
||||
{
|
||||
checkEmptyFrame();
|
||||
|
||||
if (alpha == 0 || _frame.type == FlxFrameType.EMPTY) return;
|
||||
|
||||
if (dirty) // rarely
|
||||
calcFrame(useFramePixels);
|
||||
|
||||
if (filterDirty) filterFrame();
|
||||
|
||||
for (camera in cameras)
|
||||
{
|
||||
if (!camera.visible || !camera.exists || !isOnScreen(camera)) continue;
|
||||
|
||||
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||
|
||||
if (isSimpleRender(camera)) drawSimple(camera);
|
||||
else
|
||||
drawComplex(camera);
|
||||
|
||||
#if FLX_DEBUG
|
||||
FlxBasic.visibleCount++;
|
||||
#end
|
||||
}
|
||||
|
||||
#if FLX_DEBUG
|
||||
if (FlxG.debugger.drawDebug) drawDebug();
|
||||
#end
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
override function drawComplex(camera:FlxCamera):Void
|
||||
{
|
||||
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
|
||||
_matrix.concat(_filterMatrix);
|
||||
_matrix.translate(-origin.x, -origin.y);
|
||||
_matrix.scale(scale.x, scale.y);
|
||||
|
||||
if (bakedRotationAngle <= 0)
|
||||
{
|
||||
updateTrig();
|
||||
|
||||
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
|
||||
}
|
||||
|
||||
_point.add(origin.x, origin.y);
|
||||
_matrix.translate(_point.x, _point.y);
|
||||
|
||||
if (isPixelPerfectRender(camera))
|
||||
{
|
||||
_matrix.tx = Math.floor(_matrix.tx);
|
||||
_matrix.ty = Math.floor(_matrix.ty);
|
||||
}
|
||||
|
||||
camera.drawPixels((filtered) ? _blankFrame : _frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
function filterFrame()
|
||||
{
|
||||
filterDirty = false;
|
||||
_filterMatrix.identity();
|
||||
|
||||
if (filters != null && filters.length > 0)
|
||||
{
|
||||
_flashRect.setEmpty();
|
||||
|
||||
for (filter in filters)
|
||||
{
|
||||
_flashRect.__expand(-filter.__leftExtension,
|
||||
-filter.__topExtension, filter.__leftExtension
|
||||
+ filter.__rightExtension,
|
||||
filter.__topExtension
|
||||
+ filter.__bottomExtension);
|
||||
}
|
||||
_flashRect.width += frameWidth;
|
||||
_flashRect.height += frameHeight;
|
||||
@:nullSafety(Off)
|
||||
if (_blankFrame == null) _blankFrame = new FlxFrame(null);
|
||||
|
||||
if (_blankFrame.parent == null || _flashRect.width > _blankFrame.parent.width || _flashRect.height > _blankFrame.parent.height)
|
||||
{
|
||||
if (_blankFrame.parent != null)
|
||||
{
|
||||
_blankFrame.parent.destroy();
|
||||
_filterBmp1.dispose();
|
||||
_filterBmp2.dispose();
|
||||
}
|
||||
|
||||
_blankFrame.parent = FlxGraphic.fromRectangle(Math.ceil(_flashRect.width * 1.25), Math.ceil(_flashRect.height * 1.25), 0, true);
|
||||
_filterBmp1 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
|
||||
_filterBmp2 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
|
||||
}
|
||||
_blankFrame.offset.copyFrom(_frame.offset);
|
||||
@:nullSafety(Off)
|
||||
_blankFrame.parent.bitmap = _renderer.applyFilter(_blankFrame.parent.bitmap, _filterBmp1, _filterBmp2, frame.parent.bitmap, filters, _flashRect,
|
||||
frame.frame.copyToFlash());
|
||||
_blankFrame.frame = FlxRect.get(0, 0, _blankFrame.parent.bitmap.width, _blankFrame.parent.bitmap.height);
|
||||
_filterMatrix.translate(_flashRect.x, _flashRect.y);
|
||||
_frame = _blankFrame.copyTo();
|
||||
filtered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
resetFrame();
|
||||
filtered = false;
|
||||
}
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
function set_filters(value:Null<Array<BitmapFilter>>)
|
||||
{
|
||||
if (filters != value) filterDirty = true;
|
||||
|
||||
return filters = value;
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
override function set_frame(value:FlxFrame)
|
||||
{
|
||||
if (value != frame) filterDirty = true;
|
||||
|
||||
return super.set_frame(value);
|
||||
}
|
||||
|
||||
override public function destroy()
|
||||
{
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
@:access(openfl.display.OpenGLRenderer)
|
||||
@:access(openfl.filters.BitmapFilter)
|
||||
@:access(openfl.geom.Rectangle)
|
||||
@:access(openfl.display.Stage)
|
||||
@:access(openfl.display.Graphics)
|
||||
@:access(openfl.display.Shader)
|
||||
@:access(openfl.display.BitmapData)
|
||||
@:access(openfl.geom.ColorTransform)
|
||||
@:access(openfl.display.DisplayObject)
|
||||
@:access(openfl.display3D.Context3D)
|
||||
@:access(openfl.display.CanvasRenderer)
|
||||
@:access(openfl.display.CairoRenderer)
|
||||
@:access(openfl.display3D.Context3D)
|
||||
class FlxAnimateFilterRenderer
|
||||
{
|
||||
var renderer:OpenGLRenderer;
|
||||
var context:Context3D;
|
||||
|
||||
public function new()
|
||||
{
|
||||
// context = new openfl.display3D.Context3D(null);
|
||||
renderer = new OpenGLRenderer(FlxG.game.stage.context3D);
|
||||
renderer.__worldTransform = new Matrix();
|
||||
renderer.__worldColorTransform = new ColorTransform();
|
||||
}
|
||||
|
||||
@:noCompletion function setRenderer(renderer:DisplayObjectRenderer, rect:Rectangle)
|
||||
{
|
||||
@:privateAccess
|
||||
if (true)
|
||||
{
|
||||
var displayObject = FlxG.game;
|
||||
var pixelRatio = FlxG.game.stage.__renderer.__pixelRatio;
|
||||
|
||||
var offsetX = rect.x > 0 ? Math.ceil(rect.x) : Math.floor(rect.x);
|
||||
var offsetY = rect.y > 0 ? Math.ceil(rect.y) : Math.floor(rect.y);
|
||||
if (renderer.__worldTransform == null)
|
||||
{
|
||||
renderer.__worldTransform = new Matrix();
|
||||
renderer.__worldColorTransform = new ColorTransform();
|
||||
}
|
||||
if (displayObject.__cacheBitmapColorTransform == null) displayObject.__cacheBitmapColorTransform = new ColorTransform();
|
||||
|
||||
renderer.__stage = displayObject.stage;
|
||||
|
||||
renderer.__allowSmoothing = true;
|
||||
renderer.__setBlendMode(NORMAL);
|
||||
renderer.__worldAlpha = 1 / displayObject.__worldAlpha;
|
||||
|
||||
renderer.__worldTransform.identity();
|
||||
renderer.__worldTransform.invert();
|
||||
renderer.__worldTransform.concat(new Matrix());
|
||||
renderer.__worldTransform.tx -= offsetX;
|
||||
renderer.__worldTransform.ty -= offsetY;
|
||||
renderer.__worldTransform.scale(pixelRatio, pixelRatio);
|
||||
|
||||
renderer.__pixelRatio = pixelRatio;
|
||||
}
|
||||
}
|
||||
|
||||
public function applyFilter(target:BitmapData = null, target1:BitmapData = null, target2:BitmapData = null, bmp:BitmapData, filters:Array<BitmapFilter>,
|
||||
rect:Rectangle, bmpRect:Rectangle)
|
||||
{
|
||||
if (filters == null || filters.length == 0) return bmp;
|
||||
|
||||
renderer.__setBlendMode(NORMAL);
|
||||
renderer.__worldAlpha = 1;
|
||||
|
||||
if (renderer.__worldTransform == null)
|
||||
{
|
||||
renderer.__worldTransform = new Matrix();
|
||||
renderer.__worldColorTransform = new ColorTransform();
|
||||
}
|
||||
renderer.__worldTransform.identity();
|
||||
renderer.__worldColorTransform.__identity();
|
||||
|
||||
var bitmap:BitmapData = (target == null) ? new BitmapData(Math.ceil(rect.width * 1.25), Math.ceil(rect.height * 1.25), true, 0) : target;
|
||||
|
||||
var bitmap2 = (target1 == null) ? new BitmapData(Math.ceil(rect.width * 1.25), Math.ceil(rect.height * 1.25), true, 0) : target1,
|
||||
bitmap3 = (target2 == null) ? bitmap2.clone() : target2;
|
||||
renderer.__setRenderTarget(bitmap);
|
||||
|
||||
bmp.__renderTransform.translate(Math.abs(rect.x) - bmpRect.x, Math.abs(rect.y) - bmpRect.y);
|
||||
bmpRect.x = Math.abs(rect.x);
|
||||
bmpRect.y = Math.abs(rect.y);
|
||||
|
||||
var bestResolution = renderer.__context3D.__backBufferWantsBestResolution;
|
||||
renderer.__context3D.__backBufferWantsBestResolution = false;
|
||||
renderer.__scissorRect(bmpRect);
|
||||
renderer.__renderFilterPass(bmp, renderer.__defaultDisplayShader, true);
|
||||
renderer.__scissorRect();
|
||||
|
||||
renderer.__context3D.__backBufferWantsBestResolution = bestResolution;
|
||||
|
||||
bmp.__renderTransform.identity();
|
||||
|
||||
var shader, cacheBitmap = null;
|
||||
for (filter in filters)
|
||||
{
|
||||
if (filter.__preserveObject)
|
||||
{
|
||||
renderer.__setRenderTarget(bitmap3);
|
||||
renderer.__renderFilterPass(bitmap, renderer.__defaultDisplayShader, filter.__smooth);
|
||||
}
|
||||
|
||||
for (i in 0...filter.__numShaderPasses)
|
||||
{
|
||||
shader = filter.__initShader(renderer, i, (filter.__preserveObject) ? bitmap3 : null);
|
||||
renderer.__setBlendMode(filter.__shaderBlendMode);
|
||||
renderer.__setRenderTarget(bitmap2);
|
||||
renderer.__renderFilterPass(bitmap, shader, filter.__smooth);
|
||||
|
||||
cacheBitmap = bitmap;
|
||||
bitmap = bitmap2;
|
||||
bitmap2 = cacheBitmap;
|
||||
}
|
||||
filter.__renderDirty = false;
|
||||
}
|
||||
if (target1 == null) bitmap2.dispose();
|
||||
if (target2 == null) bitmap3.dispose();
|
||||
|
||||
// var gl = renderer.__gl;
|
||||
|
||||
// var renderBuffer = bitmap.getTexture(renderer.__context3D);
|
||||
// @:privateAccess
|
||||
// gl.readPixels(0, 0, bitmap.width, bitmap.height, renderBuffer.__format, gl.UNSIGNED_BYTE, bitmap.image.data);
|
||||
// bitmap.image.version = 0;
|
||||
// @:privateAccess
|
||||
// bitmap.__textureVersion = -1;
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public function applyBlend(blend:BlendMode, bitmap:BitmapData)
|
||||
{
|
||||
bitmap.__update(false, true);
|
||||
var bmp = new BitmapData(bitmap.width, bitmap.height, 0);
|
||||
|
||||
#if (js && html5)
|
||||
ImageCanvasUtil.convertToCanvas(bmp.image);
|
||||
@:privateAccess
|
||||
var renderer = new CanvasRenderer(bmp.image.buffer.__srcContext);
|
||||
#else
|
||||
var renderer = new CairoRenderer(new Cairo(bmp.getSurface()));
|
||||
#end
|
||||
|
||||
// setRenderer(renderer, bmp.rect);
|
||||
|
||||
var m = new Matrix();
|
||||
var c = new ColorTransform();
|
||||
renderer.__allowSmoothing = true;
|
||||
renderer.__overrideBlendMode = blend;
|
||||
renderer.__worldTransform = m;
|
||||
renderer.__worldAlpha = 1;
|
||||
renderer.__worldColorTransform = c;
|
||||
|
||||
renderer.__setBlendMode(blend);
|
||||
#if (js && html5)
|
||||
bmp.__drawCanvas(bitmap, renderer);
|
||||
#else
|
||||
bmp.__drawCairo(bitmap, renderer);
|
||||
#end
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public function graphicstoBitmapData(gfx:Graphics)
|
||||
{
|
||||
if (gfx.__bounds == null) return null;
|
||||
// var cacheRTT = renderer.__context3D.__state.renderToTexture;
|
||||
// var cacheRTTDepthStencil = renderer.__context3D.__state.renderToTextureDepthStencil;
|
||||
// var cacheRTTAntiAlias = renderer.__context3D.__state.renderToTextureAntiAlias;
|
||||
// var cacheRTTSurfaceSelector = renderer.__context3D.__state.renderToTextureSurfaceSelector;
|
||||
|
||||
// var bmp = new BitmapData(Math.ceil(gfx.__width), Math.ceil(gfx.__height), 0);
|
||||
// renderer.__context3D.setRenderToTexture(bmp.getTexture(renderer.__context3D));
|
||||
// gfx.__owner.__renderTransform.identity();
|
||||
// gfx.__renderTransform.identity();
|
||||
// Context3DGraphics.render(gfx, renderer);
|
||||
GfxRenderer.render(gfx, cast renderer.__softwareRenderer);
|
||||
var bmp = gfx.__bitmap;
|
||||
|
||||
gfx.__bitmap = null;
|
||||
|
||||
// if (cacheRTT != null)
|
||||
// {
|
||||
// renderer.__context3D.setRenderToTexture(cacheRTT, cacheRTTDepthStencil, cacheRTTAntiAlias, cacheRTTSurfaceSelector);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// renderer.__context3D.setRenderToBackBuffer();
|
||||
// }
|
||||
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package funkin.graphics;
|
||||
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import animate.FlxAnimateController;
|
||||
|
||||
@:access(funkin.graphics.FunkinSprite)
|
||||
class FunkinAnimationController extends FlxAnimateController
|
||||
{
|
||||
/**
|
||||
@@ -16,6 +16,12 @@ class FunkinAnimationController extends FlxAnimateController
|
||||
_parentSprite = sprite;
|
||||
}
|
||||
|
||||
override function set_frameIndex(frame:Int):Int
|
||||
{
|
||||
_parentSprite._renderTextureDirty = true;
|
||||
return super.set_frameIndex(frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* We override `FlxAnimationController`'s `play` method to account for texture atlases.
|
||||
*/
|
||||
|
||||
@@ -186,11 +186,15 @@ class FunkinCamera extends FlxCamera
|
||||
|
||||
_backgroundFrame.parent.bitmap = _blendRenderTexture.graphic.bitmap;
|
||||
|
||||
_backgroundRenderTexture.init(Std.int(this.width * Lib.current.stage.window.scale), Std.int(this.height * Lib.current.stage.window.scale));
|
||||
// On some displays, the DPI can be less than 1, which causes the blend shader to look bad
|
||||
// We just clamp the scale to 1 to avoid this!
|
||||
var clampedScale:Float = Math.max(1, Lib.current.stage.window.scale);
|
||||
|
||||
_backgroundRenderTexture.init(Std.int(this.width * clampedScale), Std.int(this.height * clampedScale));
|
||||
_backgroundRenderTexture.drawToCamera((camera, matrix) ->
|
||||
{
|
||||
camera.zoom = this.zoom;
|
||||
matrix.scale(Lib.current.stage.window.scale, Lib.current.stage.window.scale);
|
||||
matrix.scale(clampedScale, clampedScale);
|
||||
camera.drawPixels(_backgroundFrame, null, matrix, canvas.transform.colorTransform, null, false, _blendShader);
|
||||
});
|
||||
|
||||
@@ -198,7 +202,7 @@ class FunkinCamera extends FlxCamera
|
||||
|
||||
// Resize the frame so it always fills the screen
|
||||
_cameraMatrix.identity();
|
||||
_cameraMatrix.scale(1 / (this.scaleX * Lib.current.stage.window.scale), 1 / (this.scaleY * Lib.current.stage.window.scale));
|
||||
_cameraMatrix.scale(1 / (this.scaleX * clampedScale), 1 / (this.scaleY * clampedScale));
|
||||
_cameraMatrix.translate(((width - width / this.scaleX) * 0.5), ((height - height / this.scaleY) * 0.5));
|
||||
|
||||
super.drawPixels(_backgroundRenderTexture.graphic.imageFrame.frame, null, _cameraMatrix, null, null, smoothing, null);
|
||||
|
||||
@@ -6,6 +6,7 @@ import flixel.graphics.FlxGraphic;
|
||||
import flixel.tweens.FlxTween;
|
||||
import openfl.display3D.textures.TextureBase;
|
||||
import funkin.graphics.framebuffer.FixedBitmapData;
|
||||
import funkin.graphics.framebuffer.FunkinFilterRenderer;
|
||||
import openfl.display.BitmapData;
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.math.FlxPoint;
|
||||
@@ -21,6 +22,8 @@ import animate.internal.elements.AtlasInstance;
|
||||
import animate.internal.elements.SymbolInstance;
|
||||
import animate.FlxAnimate;
|
||||
import animate.FlxAnimateFrames;
|
||||
import animate.internal.RenderTexture;
|
||||
import openfl.filters.BitmapFilter;
|
||||
import haxe.io.Path;
|
||||
|
||||
using StringTools;
|
||||
@@ -114,6 +117,11 @@ typedef AtlasSpriteSettings =
|
||||
@:access(animate.FlxAnimateController)
|
||||
class FunkinSprite extends FlxAnimate
|
||||
{
|
||||
/**
|
||||
* The filters array to be applied to the sprite.
|
||||
*/
|
||||
public var filters(default, set):Null<Array<BitmapFilter>> = null;
|
||||
|
||||
/**
|
||||
* @param x Starting X position
|
||||
* @param y Starting Y position
|
||||
@@ -124,6 +132,8 @@ class FunkinSprite extends FlxAnimate
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
filterRenderer = new FunkinFilterRenderer(this);
|
||||
|
||||
if (path != null)
|
||||
{
|
||||
var ext:String = Path.extension(path);
|
||||
@@ -774,10 +784,136 @@ class FunkinSprite extends FlxAnimate
|
||||
matrix.ty = Math.round(matrix.ty / this.scale.y) * this.scale.y;
|
||||
}
|
||||
|
||||
var filterRenderer:FunkinFilterRenderer;
|
||||
var filtered:Bool = false;
|
||||
var filterOffsets:Array<Float> = [0, 0];
|
||||
|
||||
override function checkRenderTexture():Bool
|
||||
{
|
||||
// Forcefully enable render texture when we have filters.
|
||||
if (filters != null && filters.length > 0) return true;
|
||||
|
||||
return super.checkRenderTexture();
|
||||
}
|
||||
|
||||
function set_filters(value:Null<Array<BitmapFilter>>):Null<Array<BitmapFilter>>
|
||||
{
|
||||
if (filters != value) _renderTextureDirty = true;
|
||||
filters = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
override public function draw():Void
|
||||
{
|
||||
for (filter in filters ?? [])
|
||||
{
|
||||
@:privateAccess
|
||||
if (filter.__renderDirty) _renderTextureDirty = true;
|
||||
}
|
||||
|
||||
super.draw();
|
||||
}
|
||||
|
||||
override function drawFrameComplex(frame:FlxFrame, camera:FlxCamera):Void
|
||||
{
|
||||
final willUseRenderTexture = checkRenderTexture();
|
||||
final matrix = this._matrix;
|
||||
|
||||
frame.prepareMatrix(matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
|
||||
prepareDrawMatrix(matrix, camera);
|
||||
|
||||
if (willUseRenderTexture)
|
||||
{
|
||||
var bounds:Array<Int> = [Math.ceil(frame.frame.width), Math.ceil(frame.frame.height)];
|
||||
if (_renderTexture == null) _renderTexture = new RenderTexture(bounds[0], bounds[1]);
|
||||
|
||||
if (_renderTextureDirty)
|
||||
{
|
||||
_renderTexture.init(bounds[0], bounds[1]);
|
||||
_renderTexture.drawToCamera((camera, mat) ->
|
||||
{
|
||||
camera.drawPixels(frame, framePixels, mat, null, null, antialiasing, null);
|
||||
});
|
||||
|
||||
_renderTexture.render();
|
||||
|
||||
filterRenderer.applyFilters();
|
||||
_renderTextureDirty = false;
|
||||
}
|
||||
|
||||
if (filtered)
|
||||
{
|
||||
matrix.translate(filterOffsets[0], filterOffsets[1]);
|
||||
camera.drawPixels(filterRenderer.graphic?.imageFrame.frame, null, matrix, colorTransform, blend, antialiasing, shader);
|
||||
}
|
||||
else
|
||||
{
|
||||
camera.drawPixels(_renderTexture.graphic.imageFrame.frame, framePixels, matrix, colorTransform, blend, antialiasing, shader);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
camera.drawPixels(frame, framePixels, matrix, colorTransform, blend, antialiasing, shader);
|
||||
}
|
||||
}
|
||||
|
||||
override function drawAnimate(camera:FlxCamera):Void
|
||||
{
|
||||
final willUseRenderTexture = checkRenderTexture();
|
||||
final matrix = _matrix;
|
||||
matrix.identity();
|
||||
|
||||
@:privateAccess
|
||||
var bounds = timeline._bounds;
|
||||
if (!willUseRenderTexture) matrix.translate(-bounds.x, -bounds.y);
|
||||
|
||||
prepareAnimateMatrix(matrix, camera, bounds);
|
||||
|
||||
if (renderStage) drawStage(camera);
|
||||
|
||||
timeline.currentFrame = animation.frameIndex;
|
||||
|
||||
#if !flash
|
||||
if (willUseRenderTexture)
|
||||
{
|
||||
if (_renderTexture == null) _renderTexture = new RenderTexture(Math.ceil(bounds.width), Math.ceil(bounds.height));
|
||||
|
||||
if (_renderTextureDirty)
|
||||
{
|
||||
_renderTexture.init(Math.ceil(bounds.width), Math.ceil(bounds.height));
|
||||
_renderTexture.drawToCamera((camera, matrix) ->
|
||||
{
|
||||
matrix.translate(-bounds.x, -bounds.y);
|
||||
timeline.draw(camera, matrix, null, null, antialiasing, null);
|
||||
});
|
||||
_renderTexture.render();
|
||||
|
||||
filterRenderer.applyFilters();
|
||||
_renderTextureDirty = false;
|
||||
}
|
||||
|
||||
if (filtered)
|
||||
{
|
||||
matrix.translate(filterOffsets[0], filterOffsets[1]);
|
||||
camera.drawPixels(filterRenderer.graphic?.imageFrame.frame, null, matrix, colorTransform, blend, antialiasing, shader);
|
||||
}
|
||||
else
|
||||
{
|
||||
camera.drawPixels(_renderTexture.graphic.imageFrame.frame, framePixels, matrix, colorTransform, blend, antialiasing, shader);
|
||||
}
|
||||
}
|
||||
else
|
||||
#end
|
||||
{
|
||||
timeline.draw(camera, matrix, colorTransform, blend, antialiasing, shader);
|
||||
}
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
@:nullSafety(Off) // TODO: Remove when flixel.FlxSprite is null safed.
|
||||
frames = null;
|
||||
filterRenderer.destroy();
|
||||
// Cancel all tweens so they don't continue to run on a destroyed sprite.
|
||||
// This prevents crashes.
|
||||
FlxTween.cancelTweensOf(this);
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package funkin.graphics.framebuffer;
|
||||
|
||||
import funkin.graphics.framebuffer.FixedBitmapData;
|
||||
import openfl.display.BitmapData;
|
||||
import flixel.math.FlxRect;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.util.FlxDestroyUtil;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import openfl.filters.BitmapFilter;
|
||||
import animate.internal.FilterRenderer;
|
||||
|
||||
/**
|
||||
* A helper for rendering filters on `FunkinSprite` instances.
|
||||
*/
|
||||
@:access(animate.FlxAnimate)
|
||||
@:access(openfl.filters.BitmapFilter)
|
||||
@:access(animate.internal.FilterRenderer)
|
||||
@:access(openfl.display.OpenGLRenderer)
|
||||
@:access(openfl.geom.ColorTransform)
|
||||
@:access(openfl.display.BitmapData)
|
||||
@:nullSafety
|
||||
class FunkinFilterRenderer implements IFlxDestroyable
|
||||
{
|
||||
/**
|
||||
* Graphic containing the current frame with filters.
|
||||
*/
|
||||
public var graphic(default, null):Null<FlxGraphic>;
|
||||
|
||||
var bitmapPool:Map<String, Array<BitmapData>> = [];
|
||||
var parent:FunkinSprite;
|
||||
|
||||
public function new(parent:FunkinSprite)
|
||||
{
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to the current frame.
|
||||
* The result will be contained in the `graphic` variable.
|
||||
*/
|
||||
public function applyFilters():Void
|
||||
{
|
||||
parent.filtered = false;
|
||||
if (parent.filters == null || parent.filters.length < 1) return;
|
||||
|
||||
var textureBitmap:BitmapData = parent._renderTexture.graphic.bitmap;
|
||||
|
||||
var bounds:FlxRect = FlxRect.get().copyFromFlash(textureBitmap.rect);
|
||||
FilterRenderer.expandFilterBounds(bounds, parent.filters);
|
||||
parent.filterOffsets = [bounds.x * parent.scale.x, bounds.y * parent.scale.x];
|
||||
|
||||
var ceilWidth:Int = Math.ceil(bounds.width);
|
||||
var ceilHeight:Int = Math.ceil(bounds.height);
|
||||
|
||||
if (graphic != null) putBitmap(graphic.bitmap);
|
||||
var bitmap:BitmapData = getBitmap(ceilWidth, ceilHeight);
|
||||
|
||||
if (graphic == null)
|
||||
{
|
||||
graphic = FlxGraphic.fromBitmapData(bitmap, false, null, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
graphic.bitmap = bitmap;
|
||||
graphic.imageFrame.frame.frame.set(0, 0, bitmap.width, bitmap.height);
|
||||
}
|
||||
|
||||
var filterBmp1:Null<BitmapData> = null;
|
||||
var filterBmp2:Null<BitmapData> = null;
|
||||
|
||||
var needsSecondBitmap:Bool = false;
|
||||
var needsPreserveObject:Bool = false;
|
||||
for (filter in parent.filters)
|
||||
{
|
||||
if (filter != null)
|
||||
{
|
||||
if (filter.__needSecondBitmapData) needsSecondBitmap = true;
|
||||
if (filter.__preserveObject) needsPreserveObject = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsSecondBitmap) filterBmp1 = getBitmap(graphic.width, graphic.height);
|
||||
if (needsPreserveObject) filterBmp2 = getBitmap(filterBmp1?.width ?? 1, filterBmp1?.height ?? 1);
|
||||
|
||||
_applyFilters(graphic.bitmap, textureBitmap, parent.filters, filterBmp1, filterBmp2, bounds);
|
||||
|
||||
if (filterBmp1 != null) putBitmap(filterBmp1);
|
||||
if (filterBmp2 != null) putBitmap(filterBmp2);
|
||||
|
||||
bounds.put();
|
||||
parent.filtered = true;
|
||||
}
|
||||
|
||||
function _applyFilters(target:BitmapData, bmp:BitmapData, filters:Array<BitmapFilter>, target1:Null<BitmapData>, target2:Null<BitmapData>,
|
||||
bounds:FlxRect):Void
|
||||
{
|
||||
var renderer = FilterRenderer.renderer;
|
||||
|
||||
var bitmap:BitmapData = target;
|
||||
var bitmap2:BitmapData = target1 ?? bmp;
|
||||
var bitmap3:BitmapData = target2 ?? bmp;
|
||||
|
||||
renderer.__setBlendMode(NORMAL);
|
||||
renderer.__worldAlpha = 1;
|
||||
if (renderer.__worldTransform == null)
|
||||
{
|
||||
renderer.__worldTransform = new openfl.geom.Matrix();
|
||||
renderer.__worldColorTransform = new openfl.geom.ColorTransform();
|
||||
}
|
||||
renderer.__worldTransform.identity();
|
||||
renderer.__worldColorTransform.__identity();
|
||||
bmp.__renderTransform.identity();
|
||||
bmp.__renderTransform.translate(-bounds.x, -bounds.y);
|
||||
renderer.setShader(renderer.__defaultShader);
|
||||
renderer.__setRenderTarget(bitmap);
|
||||
renderer.__scissorRect(null);
|
||||
renderer.__renderFilterPass(bmp, renderer.__defaultDisplayShader, true);
|
||||
for (filter in filters)
|
||||
{
|
||||
if (filter == null) continue;
|
||||
bitmap = FilterRenderer.__renderGpuFilter(filter, bitmap, bitmap2, bitmap3);
|
||||
}
|
||||
}
|
||||
|
||||
function getBitmap(width:Int, height:Int):BitmapData
|
||||
{
|
||||
final id:String = Std.string(width) + 'x' + Std.string(height);
|
||||
var bitmaps:Array<BitmapData> = bitmapPool.get(id) ?? [];
|
||||
if (bitmaps.length < 1)
|
||||
{
|
||||
var bitmap:BitmapData = FixedBitmapData.create(width, height);
|
||||
bitmaps.push(bitmap);
|
||||
}
|
||||
var bitmap:Null<BitmapData> = bitmaps.shift();
|
||||
if (bitmap == null) throw 'The bitmap is null???? :whattheshit:';
|
||||
bitmap.__fillRect(bitmap.rect, 0, true);
|
||||
bitmapPool.set(id, bitmaps);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
function putBitmap(bitmap:BitmapData):Void
|
||||
{
|
||||
final id:String = Std.string(bitmap.width) + 'x' + Std.string(bitmap.height);
|
||||
var bitmaps:Array<BitmapData> = bitmapPool.get(id) ?? [];
|
||||
if (!bitmaps.contains(bitmap)) bitmaps.push(bitmap);
|
||||
bitmapPool.set(id, bitmaps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up memory.
|
||||
*/
|
||||
public function destroy():Void
|
||||
{
|
||||
for (bitmaps in bitmapPool.iterator())
|
||||
{
|
||||
for (bitmap in bitmaps)
|
||||
{
|
||||
if (bitmap.__texture != null) bitmap.__texture.dispose();
|
||||
bitmap.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,14 +216,7 @@ class DropShadowShader extends FlxShader
|
||||
*/
|
||||
public function loadAltMask(path:String):Void
|
||||
{
|
||||
#if html5
|
||||
BitmapData.loadFromFile(path).onComplete(function(bmp:BitmapData)
|
||||
{
|
||||
altMaskImage = bmp;
|
||||
});
|
||||
#else
|
||||
altMaskImage = Assets.getBitmapData(path, false);
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,16 +8,9 @@ class TextureSwap extends FlxShader
|
||||
public var swappedImage(default, set):BitmapData;
|
||||
public var amount(default, set):Float;
|
||||
|
||||
public function loadSwapImage(path:String)
|
||||
public function loadSwapImage(path:String):Void
|
||||
{
|
||||
#if html5
|
||||
BitmapData.loadFromFile(path).onComplete(function(bmp:BitmapData)
|
||||
{
|
||||
swappedImage = bmp;
|
||||
});
|
||||
#else
|
||||
swappedImage = BitmapData.fromFile(path);
|
||||
#end
|
||||
swappedImage = Assets.getBitmapData(path, false);
|
||||
}
|
||||
|
||||
function set_swappedImage(_bitmapData:BitmapData):BitmapData
|
||||
|
||||
@@ -0,0 +1,770 @@
|
||||
package funkin.group;
|
||||
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.FlxG;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxSort;
|
||||
import funkin.util.SortUtil;
|
||||
import flixel.math.FlxPoint;
|
||||
|
||||
/**
|
||||
* A FunkinGroup of FlxSprites.
|
||||
*/
|
||||
typedef FunkinSpriteGroup = FunkinGroup<FlxSprite>;
|
||||
|
||||
/**
|
||||
* FlxSpriteGroup but better. Kinda like if `FlxNestedSprite` and `FlxSpriteGroup` were merged.
|
||||
*/
|
||||
class FunkinGroup<T:FlxSprite> extends FlxSprite
|
||||
{
|
||||
/**
|
||||
* The children of this FunkinGroup.
|
||||
*/
|
||||
public var children:Null<Array<T>>;
|
||||
|
||||
/**
|
||||
* The size of this FunkinGroup. Read only.
|
||||
*/
|
||||
public var size(get, never):Int;
|
||||
|
||||
function get_size():Int
|
||||
{
|
||||
return children.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of this FunkinGroup. Read only.
|
||||
*
|
||||
* Alias to `size`.
|
||||
*/
|
||||
public var length(get, never):Int;
|
||||
|
||||
function get_length():Int
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* The max size of this FunkinGroup. 0 and below is infinite.
|
||||
*/
|
||||
public var maxSize(default, set):Int = 0;
|
||||
|
||||
function set_maxSize(value:Int):Int
|
||||
{
|
||||
if (value < 0) value = 0;
|
||||
|
||||
maxSize = value;
|
||||
|
||||
if (size > maxSize && maxSize > 0)
|
||||
{
|
||||
for (child in 0...size)
|
||||
{
|
||||
if (child > maxSize) remove(children[child]);
|
||||
}
|
||||
}
|
||||
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* The width of all the FunkinGroup's children's displays put together.
|
||||
*
|
||||
* Meant as a replacement of frameWidth.
|
||||
*/
|
||||
public var accurateWidth(get, null):Float = 0;
|
||||
|
||||
/**
|
||||
* The height of all the FunkinGroup's children's displays put together.
|
||||
*
|
||||
* Meant as a replacement of frameHeight.
|
||||
*/
|
||||
public var accurateHeight(get, null):Float = 0;
|
||||
|
||||
/**
|
||||
* If this is false, the FunkinGroup will update children normally. Otherwise,
|
||||
* it will not (obviously).
|
||||
*
|
||||
* Useful for outside objects to modify this group's children. (Extending
|
||||
* classes can just override updateChildren)
|
||||
*
|
||||
* `false` by default.
|
||||
*/
|
||||
public var customChildUpdate:Bool = false;
|
||||
|
||||
/**
|
||||
* Should this FunkinGroup treat itself more like one image (in scale terms).
|
||||
*
|
||||
* `true` by default.
|
||||
*/
|
||||
public var preciseScale:Bool = true;
|
||||
|
||||
/**
|
||||
* Should this FunkinGroup treat itself more like one image (in angle terms).
|
||||
*
|
||||
* `true` by default.
|
||||
*/
|
||||
public var preciseAngle:Bool = true;
|
||||
|
||||
override function get_width():Float
|
||||
{
|
||||
if (size < 1) return 0;
|
||||
|
||||
var leftMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, x + a.localX, x + b.localX);
|
||||
}, false)[0];
|
||||
|
||||
var rightMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, (x + a.localX) + a.width, (x + b.localX) + b.width);
|
||||
}, false, FlxSort.DESCENDING)[0];
|
||||
|
||||
return Math.abs((x + rightMostSpr.localX) + rightMostSpr.width) - (x + leftMostSpr.localX);
|
||||
}
|
||||
|
||||
override function get_height():Float
|
||||
{
|
||||
if (size < 1) return 0;
|
||||
|
||||
var downwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, (y + a.localY) + a.height, (y + b.localY) + b.height);
|
||||
}, false, FlxSort.DESCENDING)[0];
|
||||
|
||||
var upwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, y + a.localY, y + b.localY);
|
||||
}, false)[0];
|
||||
|
||||
return Math.abs(((y + downwardsMostSpr.localY) + downwardsMostSpr.height) - (y + upwardsMostSpr.localY));
|
||||
}
|
||||
|
||||
// include scale with frame sizes for better accuracy
|
||||
|
||||
function get_accurateWidth():Float
|
||||
{
|
||||
if (size < 1) return 0;
|
||||
|
||||
var leftMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, x + a.localX, x + b.localX);
|
||||
}, false)[0];
|
||||
|
||||
var rightMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, (x + a.localX) + (a.frameWidth * a.scale.x), (x + b.localX) + (b.frameWidth * a.scale.x));
|
||||
}, false, FlxSort.DESCENDING)[0];
|
||||
|
||||
return Math.abs(((x + rightMostSpr.localX) + (rightMostSpr.frameWidth * rightMostSpr.scale.x)) - (x + leftMostSpr.localX));
|
||||
}
|
||||
|
||||
function get_accurateHeight():Float
|
||||
{
|
||||
if (size < 1) return 0;
|
||||
|
||||
var downwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, (y + a.localY) + (a.frameHeight * a.scale.y), (y + b.localY) + (b.frameHeight * a.scale.y));
|
||||
}, false, FlxSort.DESCENDING)[0];
|
||||
|
||||
var upwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, y + a.localY, y + b.localY);
|
||||
}, false)[0];
|
||||
|
||||
return Math.abs(((y + downwardsMostSpr.localY) + (downwardsMostSpr.frameHeight * downwardsMostSpr.scale.y)) - (y + upwardsMostSpr.localY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this FunkinGroup's `origin` to the center of its complete graphic.
|
||||
*
|
||||
* Replacement for `centerOrigin`.
|
||||
*/
|
||||
public function resetOrigin():Void
|
||||
{
|
||||
if (size < 1) return;
|
||||
|
||||
var upwardsMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, y + a.localY, y + b.localY);
|
||||
}, false)[0];
|
||||
|
||||
var leftMostSpr:T = sort(function(order:Int, a:T, b:T):Int
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
return FlxSort.byValues(order, x + a.localX, x + b.localX);
|
||||
}, false)[0];
|
||||
|
||||
origin.set(leftMostSpr.localX + width / 2, upwardsMostSpr.localY + height / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for FunkinGroup.
|
||||
*
|
||||
* @param x Starting X.
|
||||
* @param y Starting Y.
|
||||
* @param maxSize Starting max size.
|
||||
* @param preciseScale Whether to treat the FunkinGroup like one image (with scale).
|
||||
* @param preciseAngle Whether to treat the FunkinGroup like one image (with angle).
|
||||
*/
|
||||
public function new(?x:Float, ?y:Float, ?maxSize:Int, ?preciseScale:Bool, ?preciseAngle:Bool)
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
children = [];
|
||||
|
||||
this.maxSize = maxSize ?? 0;
|
||||
|
||||
if (preciseScale != null) this.preciseScale = preciseScale;
|
||||
if (preciseAngle != null) this.preciseAngle = preciseAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the child at an index.
|
||||
*
|
||||
* @param index The position.
|
||||
* @return The child or null.
|
||||
*/
|
||||
public inline function getChildAt(index:Int):Null<T>
|
||||
{
|
||||
if (index < 0 || index >= size) return null;
|
||||
|
||||
return children[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the child at an index.
|
||||
*
|
||||
* @param index The position.
|
||||
* @param replacement A new child to replace the old one.
|
||||
*/
|
||||
public inline function setChildAt(index:Int, replacement:T):Void
|
||||
{
|
||||
if (index < 0 || index >= size) return;
|
||||
|
||||
children[index] = replacement;
|
||||
}
|
||||
|
||||
override public function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
updateChildren();
|
||||
|
||||
for (child in children)
|
||||
{
|
||||
if (child != null && child.exists && child.active) child.update(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
override public function draw():Void
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (child != null && child.exists && child.visible) child.draw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the children here. Uses the child's local variables like `localX` and `localY` to update the child's position. Like `FlxNestedSprite`!
|
||||
* Can be overriden by outside classes with `customChildUpdate`.
|
||||
*/
|
||||
public function updateChildren():Void
|
||||
{
|
||||
if (customChildUpdate) return;
|
||||
|
||||
for (child in children)
|
||||
{
|
||||
if (child != null && child.exists && child.active)
|
||||
{
|
||||
child.angle = angle + child.localAngle;
|
||||
child.scale.x = scale.x * child.localScale.x;
|
||||
child.scale.y = scale.y * child.localScale.y;
|
||||
|
||||
var displace = new FlxPoint(child.localX, child.localY);
|
||||
|
||||
var dx:Float = 0;
|
||||
var dy:Float = 0;
|
||||
|
||||
dx = origin.x - child.width / 2;
|
||||
dy = origin.y - child.height / 2;
|
||||
|
||||
if (preciseScale && !preciseAngle)
|
||||
{
|
||||
dx += scale.x * (child.localX - origin.x + child.width / 2);
|
||||
|
||||
dy += scale.y * (child.localY - origin.y + child.height / 2);
|
||||
}
|
||||
else if (preciseAngle && !preciseScale)
|
||||
{
|
||||
var radians:Float = angle * (Math.PI / 180);
|
||||
var cos:Float = Math.cos(radians);
|
||||
var sin:Float = Math.sin(radians);
|
||||
|
||||
dx += cos * (child.localX - origin.x + child.width / 2);
|
||||
dx -= sin * (child.localY - origin.y + child.height / 2);
|
||||
|
||||
dy += cos * (child.localY - origin.y + child.height / 2);
|
||||
dy += sin * (child.localX - origin.x + child.width / 2);
|
||||
}
|
||||
else if (preciseAngle && preciseScale)
|
||||
{
|
||||
var radians:Float = angle * (Math.PI / 180);
|
||||
var cos:Float = Math.cos(radians);
|
||||
var sin:Float = Math.sin(radians);
|
||||
|
||||
dx += scale.x * cos * (child.localX - origin.x + child.width / 2);
|
||||
dx -= scale.y * sin * (child.localY - origin.y + child.height / 2);
|
||||
|
||||
dy += scale.y * cos * (child.localY - origin.y + child.height / 2);
|
||||
dy += scale.x * sin * (child.localX - origin.x + child.width / 2);
|
||||
}
|
||||
|
||||
if (preciseScale || preciseAngle) displace.set(dx, dy);
|
||||
|
||||
child.x = x + displace.x;
|
||||
child.y = y + displace.y;
|
||||
|
||||
child.alpha = alpha * child.localAlpha;
|
||||
child.visible = visible && child.localVisible;
|
||||
// force child cameras to the group's cameras.
|
||||
if (child.cameras != cameras) child.cameras = cameras;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child to this FunkinGroup. Will also return said child for convenience.
|
||||
* Can't add if `size` is at `maxSize`, instead returning null.
|
||||
*
|
||||
* @param child The child that the caller wants this FunkinGroup to add.
|
||||
* @return The same child or null.
|
||||
*/
|
||||
public function add(child:T):Null<T>
|
||||
{
|
||||
if (maxSize > 0 && size >= maxSize) return null;
|
||||
|
||||
children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a child right in this FunkinGroup. Will also return said child for convenience.
|
||||
* Can't create the child if `size` is at `maxSize`, instead returning null.
|
||||
*
|
||||
* @return The created child or null.
|
||||
*/
|
||||
public function make():Null<T>
|
||||
{
|
||||
if (maxSize > 0 && size >= maxSize) return null;
|
||||
|
||||
var newChild:T = cast new FlxSprite();
|
||||
children.push(newChild);
|
||||
return newChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child to this FunkinGroup at a given index. Will also return said child for convenience.
|
||||
*
|
||||
* @param child The child that the caller wants this FunkinGroup to add.
|
||||
* @param index The position the caller wants the child to go in.
|
||||
* @return The same child or null.
|
||||
*/
|
||||
public function insert(child:T, index:Int):Null<T>
|
||||
{
|
||||
if (size < index) return null;
|
||||
children.insert(index, child);
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves select children from another FunkinGroup into this one. Only works if both FunkinGroups contain the same type.
|
||||
*
|
||||
* @param grp The other group to take from.
|
||||
* @param children The children to move.
|
||||
*/
|
||||
public function move(grp:FunkinGroup<T>, children:Array<T>):Void
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (grp.children.contains(child))
|
||||
{
|
||||
grp.remove(child);
|
||||
add(child);
|
||||
// update child's local position so the child stays where it was
|
||||
child.localX = x - child.x;
|
||||
child.localY = y - child.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public function destroy():Void
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
child.destroy();
|
||||
}
|
||||
|
||||
children = null;
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a child from the group, also returns it for convenience.
|
||||
*
|
||||
* @param child The child to remove.
|
||||
* @return The removed child.
|
||||
*/
|
||||
public function remove(child:T):Null<T>
|
||||
{
|
||||
var index = children.indexOf(child);
|
||||
if (index != -1) children.splice(index, 1);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a function to all children.
|
||||
*
|
||||
* @param func A function that modifies one child at a time.
|
||||
*/
|
||||
public function forEach(func:T->Void):Void
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
func(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the children of this FunkinGroup. Returns the sorted children
|
||||
*
|
||||
* @param func The sorting function to use - you can use one of the premade ones in
|
||||
* `FlxSort` or write your own using `FlxSort.byValues()` as a "backend".
|
||||
* @param setGroup Whether to actually sort the children of this group,
|
||||
* so the caller can grab a sorted list without this group
|
||||
* actually sorting the children.
|
||||
* @param order A constant that defines the sort order.
|
||||
* Possible values are `FlxSort.ASCENDING` (default) and `FlxSort.DESCENDING`.
|
||||
* @return The sorted children list.
|
||||
*/
|
||||
public inline function sort(func:(Int, T, T) -> Int, setGroup:Bool = true, order = FlxSort.ASCENDING):Null<Array<T>>
|
||||
{
|
||||
if (setGroup)
|
||||
{
|
||||
children.sort(func.bind(order));
|
||||
return children;
|
||||
}
|
||||
else
|
||||
{
|
||||
var fakeKids = children.copy();
|
||||
fakeKids.sort(func.bind(order));
|
||||
return fakeKids;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the group, by redoing the render order of all children.
|
||||
* It does this based on the `zIndex` of each child.
|
||||
*/
|
||||
public function refresh():Void
|
||||
{
|
||||
sort(SortUtil.byZIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's the first alive child under this FunkinGroup. Returns null if it can't
|
||||
* find squat.
|
||||
*
|
||||
* @return The alive child or null.
|
||||
*/
|
||||
public inline function getFirstAlive():Null<T>
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (child.exists && child.alive) return child;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get's the first dead child under this FunkinGroup. Returns null if it can't
|
||||
* find squat.
|
||||
* getFirstAlive's evil twin.
|
||||
*
|
||||
* @return The dead child or null.
|
||||
*/
|
||||
public inline function getFirstDead():Null<T>
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (!child.alive) return child;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the amount of alive children in this FunkinGroup.
|
||||
*
|
||||
* @return The alive child number or null.
|
||||
*/
|
||||
public inline function countLiving():Int
|
||||
{
|
||||
var i = 0;
|
||||
|
||||
for (child in children)
|
||||
{
|
||||
if (child.alive) i++;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the amount of dead children in this FunkinGroup.
|
||||
*
|
||||
* @return The dead child number or null.
|
||||
*/
|
||||
public inline function countDead():Int
|
||||
{
|
||||
var i = 0;
|
||||
|
||||
for (child in children)
|
||||
{
|
||||
if (!child.alive) i++;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first nonexistent child in the family and returns it.
|
||||
* Good for recycling.
|
||||
*
|
||||
* @return The child or null.
|
||||
*/
|
||||
public inline function getFirstAvailable():Null<T>
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (!child.exists) return child;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index of the first null child under this FunkinGroup.
|
||||
* -1 means it failed
|
||||
*
|
||||
* @return The index.
|
||||
*/
|
||||
public inline function getFirstNull():Int
|
||||
{
|
||||
for (child in 0...size)
|
||||
{
|
||||
if (children[child] == null) return child;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first existing child in the family and returns it.
|
||||
* Good for recycling.
|
||||
*
|
||||
* @return The child or null.
|
||||
*/
|
||||
public inline function getFirstExisting():Null<T>
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (child.exists) return child;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a random child from this FunkinGroup.
|
||||
* @param startIndex Optional offset off the front of the array.
|
||||
* Default value is `0`, or the beginning of the array.
|
||||
* @param length Optional restriction on the number of values you want to randomly select from.
|
||||
* @return A child or null.
|
||||
*/
|
||||
public inline function getRandom(startIndex:Int = 0, length:Int = 0):Null<T>
|
||||
{
|
||||
if (size <= 0) return null;
|
||||
|
||||
if (startIndex < 0) startIndex = 0;
|
||||
if (length <= 0) length = size;
|
||||
|
||||
return FlxG.random.getObject(children, startIndex, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Brings a child back from the graveyard.
|
||||
*
|
||||
* @param child The child to revive.
|
||||
*/
|
||||
public function reviveChild(child:T):Void
|
||||
{
|
||||
if (child != null) child.revive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills all the children and then itself.
|
||||
* Revive this group via `revive()`.
|
||||
*/
|
||||
override public function kill():Void
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (child != null) child.kill();
|
||||
}
|
||||
|
||||
super.kill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Revives all the children and then itself.
|
||||
*/
|
||||
override public function revive():Void
|
||||
{
|
||||
for (child in children)
|
||||
{
|
||||
if (child != null) child.revive();
|
||||
}
|
||||
|
||||
super.revive();
|
||||
}
|
||||
|
||||
override public function clone():FunkinGroup<T>
|
||||
{
|
||||
var group = new FunkinGroup<T>(x, y, maxSize);
|
||||
|
||||
for (child in children)
|
||||
{
|
||||
group.add(cast child.clone());
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Unavailable functions that won't work with `FunkinGroup`.
|
||||
// =============================================================
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in `FunkinGroup`.
|
||||
* @return this group
|
||||
*/
|
||||
override public function makeGraphic(Width:Int, Height:Int, Color:Int = FlxColor.WHITE, Unique:Bool = false, ?Key:String):FlxSprite
|
||||
{
|
||||
#if FLX_DEBUG
|
||||
throw "This function is not supported in FunkinGroup";
|
||||
#end
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in `FunkinGroup`.
|
||||
* @return this group
|
||||
*/
|
||||
override public function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
|
||||
{
|
||||
#if FLX_DEBUG
|
||||
throw "This function is not supported in FunkinGroup";
|
||||
#end
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in `FunkinGroup`.
|
||||
* @return this group
|
||||
*/
|
||||
override public function loadGraphic(Graphic:flixel.system.FlxAssets.FlxGraphicAsset, Animated:Bool = false, Width:Int = 0, Height:Int = 0,
|
||||
Unique:Bool = false, ?Key:String):FlxSprite
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in `FunkinGroup`.
|
||||
* @return this group
|
||||
*/
|
||||
override public function loadRotatedGraphic(Graphic:flixel.system.FlxAssets.FlxGraphicAsset, Rotations:Int = 16, Frame:Int = -1, AntiAliasing:Bool = false,
|
||||
AutoBuffer:Bool = false, ?Key:String):FlxSprite
|
||||
{
|
||||
#if FLX_DEBUG
|
||||
throw "This function is not supported in FunkinGroup";
|
||||
#end
|
||||
return this;
|
||||
}
|
||||
|
||||
override function set_pixels(Value:openfl.display.BitmapData):openfl.display.BitmapData
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
override function set_frame(Value:flixel.graphics.frames.FlxFrame):flixel.graphics.frames.FlxFrame
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
override function get_pixels():openfl.display.BitmapData
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function to update the current animation frame.
|
||||
*
|
||||
* @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target
|
||||
*/
|
||||
override inline function calcFrame(RunOnCpp:Bool = false):Void
|
||||
{
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in `FunkinGroup`.
|
||||
*/
|
||||
override inline function resetHelpers():Void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in `FunkinGroup`.
|
||||
*/
|
||||
override public inline function stamp(Brush:FlxSprite, X:Int = 0, Y:Int = 0):Void
|
||||
{
|
||||
}
|
||||
|
||||
override function set_frames(Frames:flixel.graphics.frames.FlxFramesCollection):flixel.graphics.frames.FlxFramesCollection
|
||||
{
|
||||
return Frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* This functionality isn't supported in `FunkinGroup`.
|
||||
*/
|
||||
override inline function updateColorTransform():Void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package funkin.group;
|
||||
|
||||
/**
|
||||
* A script that can be tied to a FunkinGroup.
|
||||
* Create a scripted class that extends FunkinGroup to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedFunkinGroup extends funkin.group.FunkinGroup<Dynamic> implements polymod.hscript.HScriptedClass {}
|
||||
|
||||
/**
|
||||
* A script that can be tied to a FunkinSpriteGroup.
|
||||
* Create a scripted class that extends FunkinSpriteGroup to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedFunkinSpriteGroup extends funkin.group.FunkinGroup.FunkinSpriteGroup implements polymod.hscript.HScriptedClass {}
|
||||
@@ -1,5 +1,6 @@
|
||||
package funkin.input;
|
||||
|
||||
import funkin.util.InputUtil;
|
||||
import flixel.input.gamepad.FlxGamepad;
|
||||
import flixel.util.FlxDirectionFlags;
|
||||
import flixel.input.FlxInput.FlxInputState;
|
||||
@@ -381,22 +382,22 @@ class Controls extends FlxActionSet
|
||||
public function getDialogueName(action:FlxActionDigital, ?ignoreSurrounding:Bool = false):String
|
||||
{
|
||||
if (action.inputs.length == 0) return 'N/A';
|
||||
var input = action.inputs[0];
|
||||
var input = FunkinAction.lastDeviceUsed == KEYBOARD ? action.inputs[0] : action.inputs[1];
|
||||
if (ignoreSurrounding == false)
|
||||
{
|
||||
return switch (input.device)
|
||||
return switch (FunkinAction.lastDeviceUsed)
|
||||
{
|
||||
case KEYBOARD: return '[${(input.inputID : FlxKey)}]';
|
||||
case GAMEPAD: return '(${(input.inputID : FlxGamepadInputID)})';
|
||||
case KEYBOARD: return '[${InputUtil.format(input.inputID, Keys).toUpperCase()}]';
|
||||
case GAMEPAD: return '(${InputUtil.format(input.inputID, Gamepad(input.deviceID)).toUpperCase()})';
|
||||
case device: throw 'unhandled device: $device';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return switch (input.device)
|
||||
return switch (FunkinAction.lastDeviceUsed)
|
||||
{
|
||||
case KEYBOARD: return '${(input.inputID : FlxKey)}';
|
||||
case GAMEPAD: return '${(input.inputID : FlxGamepadInputID)}';
|
||||
case KEYBOARD: return InputUtil.format(input.inputID, Keys).toUpperCase();
|
||||
case GAMEPAD: return InputUtil.format(input.inputID, Gamepad(input.deviceID)).toUpperCase();
|
||||
case device: throw 'unhandled device: $device';
|
||||
}
|
||||
}
|
||||
@@ -1180,7 +1181,7 @@ class FunkinAction extends FlxActionDigital
|
||||
public var nameReleased(default, null):Null<String>;
|
||||
|
||||
var cache:Map<String,
|
||||
{timestamp:Int, value:Bool}> = [];
|
||||
{timestamp:Float, value:Bool}> = [];
|
||||
|
||||
public function new(?name:String = "", ?namePressed:String, ?nameReleased:String)
|
||||
{
|
||||
@@ -1188,6 +1189,8 @@ class FunkinAction extends FlxActionDigital
|
||||
|
||||
this.namePressed = namePressed;
|
||||
this.nameReleased = nameReleased;
|
||||
|
||||
updateLastDeviceUsed();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1203,6 +1206,7 @@ class FunkinAction extends FlxActionDigital
|
||||
*/
|
||||
public function checkPressed():Bool
|
||||
{
|
||||
if (checkFiltered(PRESSED)) updateLastDeviceUsed();
|
||||
return checkFiltered(PRESSED);
|
||||
}
|
||||
|
||||
@@ -1211,6 +1215,7 @@ class FunkinAction extends FlxActionDigital
|
||||
*/
|
||||
public function checkJustPressed():Bool
|
||||
{
|
||||
if (checkFiltered(JUST_PRESSED)) updateLastDeviceUsed();
|
||||
return checkFiltered(JUST_PRESSED);
|
||||
}
|
||||
|
||||
@@ -1227,6 +1232,7 @@ class FunkinAction extends FlxActionDigital
|
||||
*/
|
||||
public function checkJustReleased():Bool
|
||||
{
|
||||
if (checkFiltered(JUST_RELEASED)) updateLastDeviceUsed();
|
||||
return checkFiltered(JUST_RELEASED);
|
||||
}
|
||||
|
||||
@@ -1353,6 +1359,29 @@ class FunkinAction extends FlxActionDigital
|
||||
|
||||
return triggered;
|
||||
}
|
||||
|
||||
public static var lastDeviceUsed:FlxInputDevice;
|
||||
|
||||
/**
|
||||
* Checks which is the last device you have used and stores the value in `lastDeviceUsed`.
|
||||
*/
|
||||
public function updateLastDeviceUsed()
|
||||
{
|
||||
if (FlxG.keys.pressed.ANY)
|
||||
{
|
||||
lastDeviceUsed = FlxInputDevice.KEYBOARD;
|
||||
return;
|
||||
}
|
||||
|
||||
if (FlxG.gamepads.lastActive != null)
|
||||
{
|
||||
lastDeviceUsed = FlxInputDevice.GAMEPAD;
|
||||
return;
|
||||
}
|
||||
|
||||
// Default value (just in case everything's null somehow)
|
||||
lastDeviceUsed = FlxInputDevice.KEYBOARD;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+58
-354
@@ -156,384 +156,87 @@ class Cursor
|
||||
if (value != null && cursorMode != value)
|
||||
{
|
||||
cursorMode = value;
|
||||
setCursorGraphic(cursorMode);
|
||||
loadCursorGraphicSync(cursorMode);
|
||||
}
|
||||
return cursorMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous.
|
||||
* Loads the cursor graphic synchronously.
|
||||
*
|
||||
* @param value The cursor mode to load. If null, the cursor is unloaded.
|
||||
*/
|
||||
static function setCursorGraphic(?value:CursorMode = null):Void
|
||||
static function loadCursorGraphicSync(?value:CursorMode = null):Void
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
FlxG.mouse.unload();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case Default:
|
||||
if (assetCursorDefault == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_DEFAULT_PARAMS.graphic);
|
||||
assetCursorDefault = bitmapData;
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
}
|
||||
|
||||
case Cross:
|
||||
if (assetCursorCross == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CROSS_PARAMS.graphic);
|
||||
assetCursorCross = bitmapData;
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
}
|
||||
|
||||
case Eraser:
|
||||
if (assetCursorEraser == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ERASER_PARAMS.graphic);
|
||||
assetCursorEraser = bitmapData;
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
}
|
||||
|
||||
case Grabbing:
|
||||
if (assetCursorGrabbing == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_GRABBING_PARAMS.graphic);
|
||||
assetCursorGrabbing = bitmapData;
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
}
|
||||
|
||||
case Hourglass:
|
||||
if (assetCursorHourglass == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_HOURGLASS_PARAMS.graphic);
|
||||
assetCursorHourglass = bitmapData;
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
}
|
||||
|
||||
case Pointer:
|
||||
if (assetCursorPointer == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_POINTER_PARAMS.graphic);
|
||||
assetCursorPointer = bitmapData;
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
}
|
||||
|
||||
case Text:
|
||||
if (assetCursorText == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_TEXT_PARAMS.graphic);
|
||||
assetCursorText = bitmapData;
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
}
|
||||
|
||||
case ZoomIn:
|
||||
if (assetCursorZoomIn == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ZOOM_IN_PARAMS.graphic);
|
||||
assetCursorZoomIn = bitmapData;
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
}
|
||||
|
||||
case ZoomOut:
|
||||
if (assetCursorZoomOut == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ZOOM_OUT_PARAMS.graphic);
|
||||
assetCursorZoomOut = bitmapData;
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
}
|
||||
|
||||
case Crosshair:
|
||||
if (assetCursorCrosshair == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CROSSHAIR_PARAMS.graphic);
|
||||
assetCursorCrosshair = bitmapData;
|
||||
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
|
||||
}
|
||||
|
||||
case Cell:
|
||||
if (assetCursorCell == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CELL_PARAMS.graphic);
|
||||
assetCursorCell = bitmapData;
|
||||
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
|
||||
}
|
||||
|
||||
case Scroll:
|
||||
if (assetCursorScroll == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_SCROLL_PARAMS.graphic);
|
||||
assetCursorScroll = bitmapData;
|
||||
applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS);
|
||||
}
|
||||
|
||||
default:
|
||||
setCursorGraphic(null);
|
||||
}
|
||||
applyCursorParams(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous.
|
||||
* Loads the cursor graphic asynchronously.
|
||||
*
|
||||
* @param value The cursor mode to load. If null, the cursor is unloaded.
|
||||
*/
|
||||
static function loadCursorGraphic(?value:CursorMode = null):Void
|
||||
static function loadCursorGraphicAsync(?value:CursorMode = null):Void
|
||||
{
|
||||
if (value == null)
|
||||
applyCursorParams(value, true);
|
||||
}
|
||||
|
||||
static function applyCursorParams(mode:Null<CursorMode>, async:Bool = false)
|
||||
{
|
||||
if (mode == null)
|
||||
{
|
||||
FlxG.mouse.unload();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
var data = switch (mode)
|
||||
{
|
||||
case Default:
|
||||
if (assetCursorDefault == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_DEFAULT_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorDefault = bitmapData;
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Default));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
}
|
||||
case Default: { cache: assetCursorDefault, params: CURSOR_DEFAULT_PARAMS, set: (bmp) -> assetCursorDefault = bmp };
|
||||
case Cross: { cache: assetCursorCross, params: CURSOR_CROSS_PARAMS, set: (bmp) -> assetCursorCross = bmp };
|
||||
case Eraser: { cache: assetCursorEraser, params: CURSOR_ERASER_PARAMS, set: (bmp) -> assetCursorEraser = bmp };
|
||||
case Grabbing: { cache: assetCursorGrabbing, params: CURSOR_GRABBING_PARAMS, set: (bmp) -> assetCursorGrabbing = bmp };
|
||||
case Hourglass: { cache: assetCursorHourglass, params: CURSOR_HOURGLASS_PARAMS, set: (bmp) -> assetCursorHourglass = bmp };
|
||||
case Pointer: { cache: assetCursorPointer, params: CURSOR_POINTER_PARAMS, set: (bmp) -> assetCursorPointer = bmp };
|
||||
case Text: { cache: assetCursorText, params: CURSOR_TEXT_PARAMS, set: (bmp) -> assetCursorText = bmp };
|
||||
case TextVertical: { cache: assetCursorTextVertical, params: CURSOR_TEXT_VERTICAL_PARAMS, set: (bmp) -> assetCursorTextVertical = bmp };
|
||||
case ZoomIn: { cache: assetCursorZoomIn, params: CURSOR_ZOOM_IN_PARAMS, set: (bmp) -> assetCursorZoomIn = bmp };
|
||||
case ZoomOut: { cache: assetCursorZoomOut, params: CURSOR_ZOOM_OUT_PARAMS, set: (bmp) -> assetCursorZoomOut = bmp };
|
||||
case Crosshair: { cache: assetCursorCrosshair, params: CURSOR_CROSSHAIR_PARAMS, set: (bmp) -> assetCursorCrosshair = bmp };
|
||||
case Cell: { cache: assetCursorCell, params: CURSOR_CELL_PARAMS, set: (bmp) -> assetCursorCell = bmp };
|
||||
case Scroll: { cache: assetCursorScroll, params: CURSOR_SCROLL_PARAMS, set: (bmp) -> assetCursorScroll = bmp };
|
||||
default: null;
|
||||
}
|
||||
|
||||
case Cross:
|
||||
if (assetCursorCross == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CROSS_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorCross = bitmapData;
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Cross));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
}
|
||||
if (data == null)
|
||||
{
|
||||
FlxG.mouse.unload();
|
||||
return;
|
||||
}
|
||||
|
||||
case Eraser:
|
||||
if (assetCursorEraser == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ERASER_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorEraser = bitmapData;
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Eraser));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
}
|
||||
if (data.cache != null)
|
||||
{
|
||||
applyGraphic(data.cache, data.params);
|
||||
return;
|
||||
}
|
||||
|
||||
case Grabbing:
|
||||
if (assetCursorGrabbing == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_GRABBING_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorGrabbing = bitmapData;
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Grabbing));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
}
|
||||
|
||||
case Hourglass:
|
||||
if (assetCursorHourglass == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_HOURGLASS_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorHourglass = bitmapData;
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Hourglass));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
}
|
||||
|
||||
case Pointer:
|
||||
if (assetCursorPointer == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_POINTER_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorPointer = bitmapData;
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Pointer));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
}
|
||||
|
||||
case Text:
|
||||
if (assetCursorText == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_TEXT_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorText = bitmapData;
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Text));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
}
|
||||
|
||||
case ZoomIn:
|
||||
if (assetCursorZoomIn == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ZOOM_IN_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorZoomIn = bitmapData;
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(ZoomIn));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
}
|
||||
|
||||
case ZoomOut:
|
||||
if (assetCursorZoomOut == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ZOOM_OUT_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorZoomOut = bitmapData;
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(ZoomOut));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
}
|
||||
|
||||
case Crosshair:
|
||||
if (assetCursorCrosshair == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CROSSHAIR_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorCrosshair = bitmapData;
|
||||
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Crosshair));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
|
||||
}
|
||||
|
||||
case Cell:
|
||||
if (assetCursorCell == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CELL_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorCell = bitmapData;
|
||||
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Cell));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
|
||||
}
|
||||
|
||||
case Scroll:
|
||||
if (assetCursorScroll == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_SCROLL_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorScroll = bitmapData;
|
||||
applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Scroll));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS);
|
||||
}
|
||||
|
||||
default:
|
||||
loadCursorGraphic(null);
|
||||
if (async)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(data.params.graphic);
|
||||
future.onComplete((bmp:BitmapData) -> {
|
||||
data.set(bmp);
|
||||
applyGraphic(bmp, data.params);
|
||||
});
|
||||
future.onError(onCursorError.bind(mode));
|
||||
}
|
||||
else
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(data.params.graphic);
|
||||
data.set(bitmapData);
|
||||
applyGraphic(bitmapData, data.params);
|
||||
}
|
||||
}
|
||||
|
||||
static inline function applyCursorParams(graphic:BitmapData, params:CursorParams):Void
|
||||
private static inline function applyGraphic(graphic:BitmapData, params:CursorParams):Void
|
||||
{
|
||||
FlxG.mouse.load(graphic, params.scale, params.offsetX, params.offsetY);
|
||||
}
|
||||
@@ -580,6 +283,7 @@ enum CursorMode
|
||||
Hourglass;
|
||||
Pointer;
|
||||
Text;
|
||||
TextVertical;
|
||||
ZoomIn;
|
||||
ZoomOut;
|
||||
Crosshair;
|
||||
|
||||
@@ -133,11 +133,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from int to Int64.
|
||||
*/
|
||||
static final NS_PER_MS:Int64 = Constants.NS_PER_MS;
|
||||
|
||||
/**
|
||||
* Returns a precise timestamp, measured in nanoseconds.
|
||||
* Timestamp is only useful for comparing against other timestamps.
|
||||
@@ -151,11 +146,11 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
||||
// NOTE: This timestamp isn't that precise on standard HTML5 builds.
|
||||
// This is because of browser safeguards against timing attacks.
|
||||
// See https://web.dev/coop-coep to enable headers which allow for more precise timestamps.
|
||||
return haxe.Int64.fromFloat(js.Browser.window.performance.now()) * NS_PER_MS;
|
||||
#elseif cpp
|
||||
return haxe.Int64.fromFloat(js.Browser.window.performance.now()) * Constants.NS_PER_MS;
|
||||
#elseif lime_cffi
|
||||
// NOTE: If the game hard crashes on this line, rebuild Lime!
|
||||
// `lime rebuild windows -clean`
|
||||
return lime._internal.backend.native.NativeCFFI.lime_sdl_get_ticks() * NS_PER_MS;
|
||||
return Int64.fromFloat(lime._internal.backend.native.NativeCFFI.lime_system_get_timer());
|
||||
#else
|
||||
throw "Eric didn't implement precise timestamps on this platform!";
|
||||
#end
|
||||
@@ -290,11 +285,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
||||
var key:FlxKey = convertKeyCode(keyCode);
|
||||
if (_keyList.indexOf(key) == -1) return;
|
||||
|
||||
// TODO: Remove this line with SDL3 when timestamps change meaning.
|
||||
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
|
||||
timestamp *= Constants.NS_PER_MS; // 18126000000 38367000000
|
||||
// timestamp -= globalOffset * Constants.NS_PER_MS;
|
||||
// trace(timestamp);
|
||||
updateKeyStates(key, true);
|
||||
|
||||
if (getInputByKey(key)?.justPressed ?? false)
|
||||
@@ -313,10 +303,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
||||
var key:FlxKey = convertKeyCode(keyCode);
|
||||
if (_keyList.indexOf(key) == -1) return;
|
||||
|
||||
// TODO: Remove this line with SDL3 when timestamps change meaning.
|
||||
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
|
||||
timestamp *= Constants.NS_PER_MS;
|
||||
|
||||
updateKeyStates(key, false);
|
||||
|
||||
if (getInputByKey(key)?.justReleased ?? false)
|
||||
@@ -337,10 +323,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
||||
var buttonListEntry = _buttonList.get(gamepad.id);
|
||||
if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
|
||||
|
||||
// TODO: Remove this line with SDL3 when timestamps change meaning.
|
||||
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
|
||||
timestamp *= Constants.NS_PER_MS;
|
||||
|
||||
updateButtonStates(gamepad, buttonId, true);
|
||||
|
||||
if (getInputByButton(gamepad, buttonId)?.justPressed ?? false)
|
||||
@@ -361,10 +343,6 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
|
||||
var buttonListEntry = _buttonList.get(gamepad.id);
|
||||
if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
|
||||
|
||||
// TODO: Remove this line with SDL3 when timestamps change meaning.
|
||||
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
|
||||
timestamp *= Constants.NS_PER_MS;
|
||||
|
||||
updateButtonStates(gamepad, buttonId, false);
|
||||
|
||||
if (getInputByButton(gamepad, buttonId)?.justReleased ?? false)
|
||||
|
||||
@@ -97,6 +97,10 @@ class AdMobUtil
|
||||
|
||||
Admob.configureUnity(Admob.getTCFConsentForPurpose(0) == 1, StringTools.startsWith(Admob.getUSPrivacy(), '1Y'));
|
||||
|
||||
Admob.configurePangle(Admob.getTCFConsentForPurpose(0) == 1, StringTools.startsWith(Admob.getUSPrivacy(), '1Y'));
|
||||
|
||||
Admob.configureVungle(StringTools.startsWith(Admob.getUSPrivacy(), '1Y'));
|
||||
|
||||
Admob.init(#if TESTING_ADS true #else false #end);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package funkin.mobile.util;
|
||||
|
||||
#if ios
|
||||
import funkin.external.apple.FNFCUtil as NativeFNFCUtil;
|
||||
import funkin.external.apple.FNFCExtern;
|
||||
#end
|
||||
#if android
|
||||
import funkin.external.android.JNIUtil;
|
||||
@@ -13,7 +13,7 @@ import flixel.util.FlxSignal;
|
||||
/**
|
||||
* A class for handling the flow of loading FNFC song packs on mobile.
|
||||
*/
|
||||
class FNFCUtil
|
||||
class FNFCProvider
|
||||
{
|
||||
public static var onFNFCOpen:FlxTypedSignal<String->Void>;
|
||||
|
||||
@@ -22,10 +22,9 @@ class FNFCUtil
|
||||
onFNFCOpen = new FlxTypedSignal<String->Void>();
|
||||
|
||||
#if ios
|
||||
FlxG.stage.window.onDropFile.add(function(_)
|
||||
FlxG.stage.window.onDropFile.add(function(path:String, state:String, x:Float, y:Float):Void
|
||||
{
|
||||
final url = queryFNFC();
|
||||
if (url != null) getFNFCFromURL(url);
|
||||
queryFNFC();
|
||||
});
|
||||
#elseif android
|
||||
CallbackUtil.onFNFCOpen.add(onFNFCOpen.dispatch);
|
||||
@@ -52,7 +51,7 @@ class FNFCUtil
|
||||
private static function getFNFCFromURL(url:String):Void
|
||||
{
|
||||
var cURL:cpp.ConstCharStar = cast url;
|
||||
NativeFNFCUtil.copyFNFCIntoCache(cURL, cpp.Callable.fromStaticFunction(fnfcCallback));
|
||||
FNFCExtern.copyFNFCIntoCache(cURL, cpp.Callable.fromStaticFunction(fnfcCallback));
|
||||
}
|
||||
|
||||
@:noCompletion
|
||||
@@ -5,8 +5,9 @@ import funkin.external.apple.ScreenUtil as NativeScreenUtil;
|
||||
#elseif android
|
||||
import funkin.external.android.ScreenUtil as NativeScreenUtil;
|
||||
#end
|
||||
import lime.math.Rectangle;
|
||||
import lime.system.System;
|
||||
import lime.app.Application;
|
||||
import openfl.geom.Rectangle;
|
||||
|
||||
/**
|
||||
* A Utility class to get mobile screen related informations.
|
||||
@@ -22,6 +23,9 @@ class ScreenUtil
|
||||
{
|
||||
final notchRect:Rectangle = new Rectangle();
|
||||
|
||||
notchRect.x = 0.0;
|
||||
notchRect.y = 0.0;
|
||||
|
||||
#if android
|
||||
final rectDimensions:Array<Array<Float>> = [[], [], [], []];
|
||||
|
||||
@@ -59,32 +63,24 @@ class ScreenUtil
|
||||
var bottomInset:Float = -1;
|
||||
var deviceWidth:Float = -1;
|
||||
var deviceHeight:Float = -1;
|
||||
var displayOrientation:DisplayOrientation = System.getDisplayOrientation(0);
|
||||
|
||||
NativeScreenUtil.getSafeAreaInsets(cpp.RawPointer.addressOf(topInset), cpp.RawPointer.addressOf(bottomInset), cpp.RawPointer.addressOf(leftInset),
|
||||
cpp.RawPointer.addressOf(rightInset));
|
||||
|
||||
NativeScreenUtil.getScreenSize(cpp.RawPointer.addressOf(deviceWidth), cpp.RawPointer.addressOf(deviceHeight));
|
||||
|
||||
notchRect.x = 0;
|
||||
notchRect.y = 0.0;
|
||||
|
||||
// Calculate the rectangle dimensions for the notch
|
||||
// Note: iOS only spits out *insets* for "safe areas", so we can only get a broad position for the notch
|
||||
// left + right insets are the same, so we can use either
|
||||
// Note: *inset* is the distance from the edge of the screen where a safe area gets defined
|
||||
// see: https://developer.apple.com/documentation/uikit/uiview/safeareainsets
|
||||
switch (displayOrientation)
|
||||
switch (System.getDisplayOrientation(Application.current.window.display))
|
||||
{
|
||||
case DISPLAY_ORIENTATION_LANDSCAPE: // landscape
|
||||
notchRect.width = leftInset + rightInset;
|
||||
notchRect.height = bottomInset - topInset;
|
||||
notchRect.y = topInset;
|
||||
notchRect.width = leftInset;
|
||||
notchRect.height = deviceHeight;
|
||||
case DISPLAY_ORIENTATION_LANDSCAPE_FLIPPED: // landscape
|
||||
notchRect.width = leftInset + rightInset;
|
||||
notchRect.height = bottomInset - topInset;
|
||||
notchRect.y = topInset;
|
||||
notchRect.x = deviceWidth - notchRect.width; // move notchRect if we are flipped, notch is at the right of screen
|
||||
notchRect.width = leftInset;
|
||||
notchRect.height = deviceHeight;
|
||||
notchRect.x = deviceWidth - rightInset;
|
||||
case DISPLAY_ORIENTATION_PORTRAIT: // portrait
|
||||
notchRect.width = deviceWidth;
|
||||
notchRect.height = topInset;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package funkin.modding;
|
||||
|
||||
import polymod.Polymod;
|
||||
import polymod.Polymod.PolymodError;
|
||||
|
||||
@:nullSafety
|
||||
class PolymodErrorHandler
|
||||
@@ -10,67 +10,206 @@ class PolymodErrorHandler
|
||||
// Perform an action based on the error code.
|
||||
switch (error.code)
|
||||
{
|
||||
case FRAMEWORK_INIT, FRAMEWORK_AUTODETECT, SCRIPT_PARSING:
|
||||
// Unimportant.
|
||||
return;
|
||||
//
|
||||
// Mod Metadata Parsing Errors
|
||||
//
|
||||
|
||||
case MOD_LOAD_PREPARE, MOD_LOAD_DONE:
|
||||
logInfo('LOADING MOD - ${error.message}');
|
||||
|
||||
case MISSING_ICON:
|
||||
logWarn('A mod is missing an icon. Please add one.');
|
||||
|
||||
case SCRIPT_PARSE_ERROR:
|
||||
// A syntax error when parsing a script.
|
||||
logError(error.message);
|
||||
case MOD_MISSING_DIRECTORY:
|
||||
// A mod directory was included in the list of mods to load, but it isn't installed.
|
||||
trace(' WARNING '.warning() + 'Tried to load a mod that was not installed: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Polymod Script Parsing Error', error.message);
|
||||
funkin.util.WindowUtil.showError('Mod Load Error', error.message);
|
||||
|
||||
case MOD_MISSING_ID:
|
||||
// A mod ID was included in the list of mods to load, but it isn't installed.
|
||||
trace(' WARNING '.warning() + ' Tried to load a mod that was not installed: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Load Error', error.message);
|
||||
|
||||
case MOD_MISSING_METADATA:
|
||||
// A mod ID was included in the list of mods to load, but the mod folder doesn't have metadata.
|
||||
trace(' ERROR '.error() + ' Tried to load a mod with no metadata: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Load Error', error.message);
|
||||
|
||||
case MOD_METADATA_PARSE_FAILED:
|
||||
// Polymod tries to load the mod's metadata, but it could not be parsed.
|
||||
trace(' ERROR '.error() + ' Failed to parse mod metadata: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Metadata Parse Error', error.message);
|
||||
|
||||
case MOD_VERSION_PARSE_FAILED:
|
||||
// Polymod tries to load the mod's version, but it could not be parsed.
|
||||
trace(' ERROR '.error() + ' Failed to parse mod version: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Version Parse Error', error.message);
|
||||
|
||||
case MOD_API_VERSION_PARSE_FAILED:
|
||||
// Polymod tries to load the mod's API version, but it could not be parsed.
|
||||
trace(' ERROR '.error() + ' Failed to parse mod API version: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod API Version Parse Error', error.message);
|
||||
|
||||
case MOD_MISSING_ICON:
|
||||
// A mod is missing an icon.
|
||||
trace(' WARNING '.warning() + ' A mod is missing an icon: ${error.message}');
|
||||
|
||||
//
|
||||
// Mod Loading Errors
|
||||
//
|
||||
|
||||
case MOD_API_VERSION_MISMATCH:
|
||||
// Polymod tried to load a mod but failed because it has the wrong API version.
|
||||
// This is an important issue that requires user attention so it can be resolved by the mod developer.
|
||||
trace(' WARNING '.warning() + ' Failed to load mod - ${error.message}');
|
||||
|
||||
var regex:EReg = ~/Mod "([-_a-zA-Z0-9]+)" is not compatible with API version "(.*?)", got "(.*?)"/;
|
||||
if (regex.match(error.message))
|
||||
{
|
||||
// Notify the user via formatted popup.
|
||||
var modId:String = regex.matched(1);
|
||||
// var apiVersion:String = regex.matched(2);
|
||||
var modVersion:String = regex.matched(3);
|
||||
|
||||
var message:String = 'Installed mod "$modId" was built for modding version "v$modVersion". It is not compatible with game version ${Constants.GENERATED_BY}, and must be skipped.'
|
||||
+ '\n\nPlease inform the mod developer that "$modId" must be updated for compatibility.';
|
||||
|
||||
funkin.util.WindowUtil.showError('Mod Outdated', message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Notify the user via standard popup.
|
||||
funkin.util.WindowUtil.showError('Mod Outdated', error.message);
|
||||
}
|
||||
|
||||
case MOD_LOAD_FAILED:
|
||||
// Polymod failed to load a mod. A different error would have already been logged.
|
||||
trace(' WARNING '.warning() + ' Failed to load mod - ${error.message}');
|
||||
|
||||
case MOD_LOAD_DONE:
|
||||
trace(' INFO '.info() + ' Loaded mod - ${error.message}');
|
||||
|
||||
//
|
||||
// Mod Dependency Errors
|
||||
//
|
||||
|
||||
case MOD_OPTIONAL_DEPENDENCY_UNMET:
|
||||
// Polymod looked at the list of available mods, and one of them was missing an optional dependency.
|
||||
trace(' INFO '.info() + ' Installed mod is missing an optional dependency: ${error.message}');
|
||||
|
||||
case MOD_DEPENDENCY_UNMET:
|
||||
switch (error.origin)
|
||||
{
|
||||
case SCAN:
|
||||
// Polymod looked at the list of available mods, and one of them was missing a dependency.
|
||||
// This is an important warning that should be shown to the user.
|
||||
trace(' WARNING '.warning() + ' Installed mod is missing a dependency: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
|
||||
default:
|
||||
// Polymod tried to load a mod, but failed because it has a dependency that wasn't met.
|
||||
// This is a major error that should be shown to the user.
|
||||
trace(' ERROR '.error() + ' Failed to load mod due to missing dependency: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
|
||||
}
|
||||
|
||||
case MOD_DEPENDENCY_VERSION_MISMATCH:
|
||||
switch (error.origin)
|
||||
{
|
||||
case SCAN:
|
||||
// Polymod looked at the list of available mods, and one of them has a dependency on a different mod,
|
||||
// but the mod has the wrong version.
|
||||
// This is an important warning that should be shown to the user.
|
||||
trace(' WARNING '.warning() + ' Installed mod has a mismatched dependency: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
|
||||
default:
|
||||
// Polymod tried to load a mod, but failed because one of its dependencies was the wrong version.
|
||||
// This is a major error that should be shown to the user.
|
||||
trace(' ERROR '.error() + ' Failed to load mod due to mismatched dependency: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
|
||||
}
|
||||
|
||||
case MOD_DEPENDENCY_CYCLICAL:
|
||||
// Polymod tried to load a mod, but failed because one of its dependencies depends on that mod.
|
||||
// This is a major error that should be shown to the user.
|
||||
trace(' ERROR '.error() + ' Failed to load mod due to cyclical dependency: ${error.message}');
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Mod Dependency Error', error.message);
|
||||
|
||||
//
|
||||
// Script Parsing Errors
|
||||
//
|
||||
|
||||
case SCRIPT_PARSE_FAILED:
|
||||
// A syntax error when parsing a script.
|
||||
trace(' ERROR '.error() + ' ' + error.message);
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Script Parsing Error', error.message);
|
||||
|
||||
case SCRIPT_RUNTIME_EXCEPTION:
|
||||
// A runtime error when running a script.
|
||||
logError(error.message);
|
||||
trace(' ERROR '.error() + ' ' + error.message);
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Polymod Script Exception', error.message);
|
||||
case SCRIPT_CLASS_MODULE_NOT_FOUND:
|
||||
// A scripted class tried to reference an unknown class or module.
|
||||
logError(error.message);
|
||||
funkin.util.WindowUtil.showError('Script Exception', error.message);
|
||||
|
||||
// Last word is the class name.
|
||||
var msg:String = 'Import error in ${error.origin}';
|
||||
msg += '\n${error.message}';
|
||||
case SCRIPTED_CLASS_NOT_REGISTERED:
|
||||
// Polymod attempted to initialize a scripted class, but it wasn't registered.
|
||||
trace(' ERROR '.error() + ' ' + error.message);
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Script Parsing Error', error.message);
|
||||
|
||||
case SCRIPTED_CLASS_ALREADY_REGISTERED:
|
||||
// Polymod attempted to register a scripted class, but one with the same name and package already exists.
|
||||
trace(' ERROR '.error() + ' ' + error.message);
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Polymod Script Import Error', msg);
|
||||
case SCRIPT_CLASS_MODULE_BLACKLISTED:
|
||||
// A scripted class tried to reference a blacklisted class or module.
|
||||
logError(error.message);
|
||||
funkin.util.WindowUtil.showError('Script Parsing Error', error.message);
|
||||
|
||||
case SCRIPTED_CLASS_REDUNDANT_IMPORT:
|
||||
// A scripted class imported a module that's already imported.
|
||||
trace(' WARNING '.warning() + ' ' + error.message);
|
||||
|
||||
case SCRIPTED_CLASS_UNRESOLVED_IMPORT:
|
||||
// A scripted class tried to import a module that doesn't exist.
|
||||
trace(' ERROR '.error() + ' ' + error.message);
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Polymod Script Blacklist Violation', error.message);
|
||||
funkin.util.WindowUtil.showError('Script Import Error', error.message);
|
||||
|
||||
case SCRIPTED_CLASS_BLACKLISTED_MODULE:
|
||||
// A scripted class tried to import a module that's blacklisted.
|
||||
trace(' ERROR '.error() + ' ' + error.message);
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Script Blacklist Violation', error.message);
|
||||
|
||||
case SCRIPTED_CLASS_BLACKLISTED_FIELD:
|
||||
// A scripted class tried to access a field that's blacklisted.
|
||||
trace(' ERROR '.error() + ' ' + error.message);
|
||||
// Notify the user via popup.
|
||||
funkin.util.WindowUtil.showError('Script Blacklist Violation', error.message);
|
||||
|
||||
//
|
||||
// Other Errors
|
||||
//
|
||||
|
||||
case FRAMEWORK_INIT, MOD_DEPENDENCY_CHECK_SKIPPED, SCRIPT_PARSE_START, SCRIPT_PARSE_DONE:
|
||||
// Unimportant messages that we don't need to log.
|
||||
return;
|
||||
|
||||
default:
|
||||
// Log the message based on its severity.
|
||||
switch (error.severity)
|
||||
{
|
||||
case NOTICE:
|
||||
logInfo(error.message);
|
||||
case WARNING:
|
||||
logWarn(error.message);
|
||||
case ERROR:
|
||||
logError(error.message);
|
||||
trace(' ERROR '.error() + ' ' + error.message);
|
||||
case WARNING:
|
||||
trace(' WARNING '.warning() + ' ' + error.message);
|
||||
case INFO:
|
||||
trace(' INFO '.info() + ' ' + error.message);
|
||||
case DEBUG:
|
||||
// trace(' DEBUG '.debug() + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static function logInfo(message:String):Void
|
||||
{
|
||||
trace(' INFO '.bg_blue().bold() + ' ${message}');
|
||||
}
|
||||
|
||||
static function logError(message:String):Void
|
||||
{
|
||||
trace(' ERROR '.error() + ' ${message}');
|
||||
}
|
||||
|
||||
static function logWarn(message:String):Void
|
||||
{
|
||||
trace(' WARNING '.bg_yellow().bold() + ' ${message}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +46,13 @@ class PolymodHandler
|
||||
* Using more complex rules allows mods from older compatible versions to stay functioning,
|
||||
* while preventing mods made for future versions from being installed.
|
||||
*/
|
||||
public static final API_VERSION_RULE:String = ">=0.8.0 <0.9.0";
|
||||
public static final API_VERSION_RULE:String = '>=0.8.0 <0.9.0';
|
||||
|
||||
/**
|
||||
* Where relative to the executable that mods are located.
|
||||
*/
|
||||
static final MOD_FOLDER:String =
|
||||
#if (REDIRECT_ASSETS_FOLDER && macos)
|
||||
#if (REDIRECT_ASSETS_FOLDER && mac)
|
||||
'../../../../../../../example_mods'
|
||||
#elseif REDIRECT_ASSETS_FOLDER
|
||||
'../../../../example_mods'
|
||||
@@ -61,7 +61,7 @@ class PolymodHandler
|
||||
#end;
|
||||
|
||||
static final CORE_FOLDER:Null<String> =
|
||||
#if (REDIRECT_ASSETS_FOLDER && macos)
|
||||
#if (REDIRECT_ASSETS_FOLDER && mac)
|
||||
'../../../../../../../assets'
|
||||
#elseif REDIRECT_ASSETS_FOLDER
|
||||
'../../../../assets'
|
||||
@@ -69,6 +69,14 @@ class PolymodHandler
|
||||
null
|
||||
#end;
|
||||
|
||||
/**
|
||||
* Populated with the directories of mods once they're successfully loaded.
|
||||
*/
|
||||
public static var loadedModDirs:Array<String> = [];
|
||||
|
||||
/**
|
||||
* Populated with the IDs of mods once they're successfully loaded.
|
||||
*/
|
||||
public static var loadedModIds:Array<String> = [];
|
||||
|
||||
// Use SysZipFileSystem on native and MemoryZipFilesystem on web.
|
||||
@@ -92,7 +100,7 @@ class PolymodHandler
|
||||
createModRoot();
|
||||
#end
|
||||
trace('Initializing Polymod (using all mods)...');
|
||||
loadModsById(getAllModIds());
|
||||
loadModsByDir(getAllModDirs());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +113,7 @@ class PolymodHandler
|
||||
createModRoot();
|
||||
#end
|
||||
trace('Initializing Polymod (using configured mods)...');
|
||||
loadModsById(Save.instance.enabledModIds.value);
|
||||
loadModsByDir(Save.instance.enabledModDirs.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,22 +127,22 @@ class PolymodHandler
|
||||
#end
|
||||
// We still need to configure the debug print calls etc.
|
||||
trace('Initializing Polymod (using no mods)...');
|
||||
loadModsById([]);
|
||||
loadModsByDir([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all the mods with the given ids.
|
||||
* @param ids The ORDERED list of mod ids to load.
|
||||
* Load all the mods with the directories they're in.
|
||||
* @param dirs The ORDERED list of mod ids to load.
|
||||
*/
|
||||
public static function loadModsById(ids:Array<String>):Void
|
||||
public static function loadModsByDir(dirs:Array<String>):Void
|
||||
{
|
||||
if (ids.length == 0)
|
||||
if (dirs.length == 0)
|
||||
{
|
||||
trace('You attempted to load zero mods.');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Attempting to load ${ids.length} mods...');
|
||||
trace('Attempting to load ${dirs.length} mods...');
|
||||
}
|
||||
|
||||
buildImports();
|
||||
@@ -145,7 +153,7 @@ class PolymodHandler
|
||||
// Root directory for all mods.
|
||||
modRoot: MOD_FOLDER,
|
||||
// The directories for one or more mods to load.
|
||||
dirs: ids,
|
||||
dirs: dirs,
|
||||
// Framework being used to load assets.
|
||||
framework: OPENFL,
|
||||
// The current version of our API.
|
||||
@@ -182,18 +190,20 @@ class PolymodHandler
|
||||
{
|
||||
if (loadedModList.length == 0)
|
||||
{
|
||||
trace('Mod loading complete. We loaded no mods / ${ids.length} mods.');
|
||||
trace('Mod loading complete. We loaded no mods / ${dirs.length} mods.');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Mod loading complete. We loaded ${loadedModList.length} / ${ids.length} mods.');
|
||||
trace('Mod loading complete. We loaded ${loadedModList.length} / ${dirs.length} mods.');
|
||||
}
|
||||
}
|
||||
|
||||
loadedModIds = [];
|
||||
loadedModDirs = [];
|
||||
for (mod in loadedModList)
|
||||
{
|
||||
trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
|
||||
loadedModDirs.push(mod.dirName);
|
||||
loadedModIds.push(mod.id);
|
||||
}
|
||||
|
||||
@@ -247,25 +257,13 @@ class PolymodHandler
|
||||
static function buildImports():Void
|
||||
{
|
||||
// Add default imports for common classes.
|
||||
static final DEFAULT_IMPORTS:Array<Class<Dynamic>> = [
|
||||
funkin.Assets,
|
||||
funkin.Paths,
|
||||
funkin.Preferences,
|
||||
funkin.util.Constants,
|
||||
flixel.FlxG
|
||||
];
|
||||
static final DEFAULT_IMPORTS:Array<Class<Dynamic>> = [funkin.Assets, funkin.Paths, funkin.Preferences, funkin.util.Constants, flixel.FlxG];
|
||||
|
||||
for (cls in DEFAULT_IMPORTS)
|
||||
{
|
||||
Polymod.addDefaultImport(cls);
|
||||
}
|
||||
|
||||
// Add import aliases for certain classes.
|
||||
// NOTE: Scripted classes are automatically aliased to their parent class.
|
||||
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
|
||||
|
||||
Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
|
||||
|
||||
// `lime.utils.Assets` literally just has a private `resolveClass` function for some reason? so we replace it with our own.
|
||||
Polymod.addImportAlias('lime.utils.Assets', funkin.Assets);
|
||||
Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets);
|
||||
@@ -419,6 +417,11 @@ class PolymodHandler
|
||||
// Disallow direct manipulation of save data.
|
||||
Polymod.blacklistStaticFields(flixel.FlxG, ['save']);
|
||||
|
||||
// `haxe.Unserializer`
|
||||
// Just to be double-sure, lets blacklist some fields of the Unserializer to make it harder to use if you DO get one.
|
||||
Polymod.blacklistStaticFields(haxe.Unserializer, ['run']);
|
||||
Polymod.blacklistInstanceFields(haxe.Unserializer, ['unserialize']);
|
||||
|
||||
// `funkin.save.Save`
|
||||
// Direct access to save data is important for scripts (like checking unlocks),
|
||||
// but we don't want scripts to be able to perform operations like writing scores.
|
||||
@@ -427,6 +430,12 @@ class PolymodHandler
|
||||
'clearData', // No score manipulation please
|
||||
'setLevelScore', 'setSongScore', 'applySongRank']);
|
||||
|
||||
// `openfl.filesystem.FileStream`, `openfl.net.Socket`, `openfl.utils.ByteArray.ByteArrayData`
|
||||
// Returns `Unseralizer.run` if encoded in HXSF format, though it does have to be seralized correctly for the exploit to work.
|
||||
#if !html5 Polymod.blacklistInstanceFields(openfl.filesystem.FileStream, ['readObject']); #end
|
||||
Polymod.blacklistInstanceFields(openfl.net.Socket, ['readObject']);
|
||||
Polymod.blacklistInstanceFields(openfl.utils.ByteArray.ByteArrayData, ['readObject']);
|
||||
|
||||
// `funkin.api.*`
|
||||
// Contains functions which may allow for cheating and such.
|
||||
for (cls in ClassMacro.listClassesInPackage('funkin.api'))
|
||||
@@ -455,15 +464,6 @@ class PolymodHandler
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `funkin.api.newgrounds.*`
|
||||
// Contains functions which allow for cheating medals and leaderboards.
|
||||
for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `io.newgrounds.*`
|
||||
// Contains functions which allow for cheating medals and leaderboards.
|
||||
for (cls in ClassMacro.listClassesInPackage('io.newgrounds'))
|
||||
@@ -520,11 +520,6 @@ class PolymodHandler
|
||||
var output:polymod.format.ParseRules = polymod.format.ParseRules.getDefault();
|
||||
// Ensure TXT files have merge support.
|
||||
output.addType('txt', TextFileFormat.LINES);
|
||||
// Ensure script files have merge support.
|
||||
output.addType('hscript', TextFileFormat.PLAINTEXT);
|
||||
output.addType('hxs', TextFileFormat.PLAINTEXT);
|
||||
output.addType('hxc', TextFileFormat.PLAINTEXT);
|
||||
output.addType('hx', TextFileFormat.PLAINTEXT);
|
||||
|
||||
// You can specify the format of a specific file, with file extension.
|
||||
// output.addFile("data/introText.txt", TextFileFormat.LINES)
|
||||
@@ -569,18 +564,28 @@ class PolymodHandler
|
||||
return modIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of ALL mod directory names, including disabled mods.
|
||||
* @return An array of mod direcotry names
|
||||
*/
|
||||
public static function getAllModDirs():Array<String>
|
||||
{
|
||||
var modDirs:Array<String> = [for (i in getAllMods()) i.dirName];
|
||||
return modDirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of metadata for all enabled mods.
|
||||
* @return An array of mod metadata
|
||||
*/
|
||||
public static function getEnabledMods():Array<ModMetadata>
|
||||
{
|
||||
var modIds:Array<String> = Save.instance.enabledModIds.value;
|
||||
var modDirs:Array<String> = Save.instance.enabledModDirs.value;
|
||||
var modMetadata:Array<ModMetadata> = getAllMods();
|
||||
var enabledMods:Array<ModMetadata> = [];
|
||||
for (item in modMetadata)
|
||||
{
|
||||
if (modIds.indexOf(item.id) != -1)
|
||||
if (modDirs.indexOf(item.dirName) != -1)
|
||||
{
|
||||
enabledMods.push(item);
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class HitNoteScriptEvent extends NoteScriptEvent
|
||||
/**
|
||||
* The score the player received for hitting the note.
|
||||
*/
|
||||
public var score:Int;
|
||||
public var score:Float;
|
||||
|
||||
/**
|
||||
* If the hit causes a combo break.
|
||||
@@ -158,7 +158,7 @@ class HitNoteScriptEvent extends NoteScriptEvent
|
||||
*/
|
||||
public var doesNotesplash:Bool = false;
|
||||
|
||||
public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, isComboBreak:Bool, comboCount:Int = 0, hitDiff:Float = 0,
|
||||
public function new(note:NoteSprite, healthChange:Float, score:Float, judgement:String, isComboBreak:Bool, comboCount:Int = 0, hitDiff:Float = 0,
|
||||
doesNotesplash:Bool = false):Void
|
||||
{
|
||||
super(NOTE_HIT, note, healthChange, comboCount, true);
|
||||
@@ -200,7 +200,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
|
||||
/**
|
||||
* How much score should be lost when this ghost note is pressed.
|
||||
*/
|
||||
public var scoreChange(default, default):Int;
|
||||
public var scoreChange(default, default):Float;
|
||||
|
||||
/**
|
||||
* Whether to play the record scratch sound.
|
||||
@@ -212,7 +212,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
|
||||
*/
|
||||
public var playAnim(default, default):Bool;
|
||||
|
||||
public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
|
||||
public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Float):Void
|
||||
{
|
||||
super(NOTE_GHOST_MISS, true);
|
||||
this.dir = dir;
|
||||
@@ -239,7 +239,7 @@ class HoldNoteScriptEvent extends NoteScriptEvent
|
||||
/**
|
||||
* The score the player received for hitting the note.
|
||||
*/
|
||||
public var score:Int;
|
||||
public var score:Float;
|
||||
|
||||
/**
|
||||
* If the hit causes a combo break.
|
||||
@@ -257,7 +257,7 @@ class HoldNoteScriptEvent extends NoteScriptEvent
|
||||
*/
|
||||
public var doesNotesplash:Bool = false;
|
||||
|
||||
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, comboCount:Int = 0,
|
||||
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Float, isComboBreak:Bool, comboCount:Int = 0,
|
||||
cancelable:Bool = false):Void
|
||||
{
|
||||
super(type, null, healthChange, comboCount, true);
|
||||
|
||||
@@ -605,6 +605,9 @@ class PauseSubState extends MusicBeatSubState
|
||||
// ===============
|
||||
var fastOffset:Bool = false;
|
||||
var lastOffsetPress:Float = 0;
|
||||
#if !mobile
|
||||
var offset:Float = Preferences.globalOffset ?? 0;
|
||||
#end
|
||||
|
||||
/**
|
||||
* Process user inputs every frame.
|
||||
@@ -680,7 +683,6 @@ class PauseSubState extends MusicBeatSubState
|
||||
function handleModifyingOffsets():Bool
|
||||
{
|
||||
#if !mobile
|
||||
var offset:Int = Preferences.globalOffset ?? 0;
|
||||
if (FlxG.keys.pressed.SHIFT && (controls.UI_UP || controls.UI_DOWN))
|
||||
{
|
||||
lastOffsetPress += FlxG.elapsed;
|
||||
@@ -697,20 +699,20 @@ class PauseSubState extends MusicBeatSubState
|
||||
{
|
||||
offset += (controls.UI_UP_P || controls.UI_UP) ? 1 : -1;
|
||||
|
||||
offsetText.text = 'Global Offset: ${offset}ms';
|
||||
offsetText.text = 'Global Offset: ${Std.int(offset)}ms';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
offset += (controls.UI_UP_P || controls.UI_UP) ? 1 : -1;
|
||||
offset += ((controls.UI_UP_P || controls.UI_UP) ? 1 : -1) * (FlxG.elapsed * 30);
|
||||
|
||||
offsetText.text = 'Global Offset: ${offset}ms';
|
||||
offsetText.text = 'Global Offset: ${Std.int(offset)}ms';
|
||||
}
|
||||
|
||||
if (offset > 1500) offset = 1500;
|
||||
if (offset < -1500) offset = -1500;
|
||||
|
||||
Preferences.globalOffset = offset;
|
||||
Preferences.globalOffset = Std.int(offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import flixel.FlxSubState;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.sound.FlxSound;
|
||||
import flixel.text.FlxText;
|
||||
import flixel.text.FlxBitmapFont;
|
||||
import flixel.text.FlxBitmapText;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.ui.FlxBar;
|
||||
@@ -41,7 +42,6 @@ import funkin.play.components.HealthIcon;
|
||||
import funkin.play.components.PopUpStuff;
|
||||
import funkin.play.components.Subtitles;
|
||||
import funkin.play.cutscene.dialogue.Conversation;
|
||||
import funkin.play.cutscene.VanillaCutscenes;
|
||||
import funkin.play.cutscene.VideoCutscene;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
@@ -238,9 +238,10 @@ class PlayState extends MusicBeatSubState
|
||||
|
||||
/**
|
||||
* The player's current score.
|
||||
* TODO: Move this to its own class.
|
||||
* This needs to be a float because you gain partial points as you hold a hold note,
|
||||
* possibly less than one point each update depending on your framerate.
|
||||
*/
|
||||
public var songScore:Int = 0;
|
||||
public var songScore:Float = 0;
|
||||
|
||||
/**
|
||||
* Start at this point in the song once the countdown is done.
|
||||
@@ -548,9 +549,9 @@ class PlayState extends MusicBeatSubState
|
||||
* RENDER OBJECTS
|
||||
*/
|
||||
/**
|
||||
* The FlxText which displays the current score.
|
||||
* The FlxBitmapText which displays the current score.
|
||||
*/
|
||||
var scoreText:FlxText;
|
||||
var scoreText:FlxBitmapText;
|
||||
|
||||
/**
|
||||
* The bar which displays the player's health.
|
||||
@@ -625,6 +626,11 @@ class PlayState extends MusicBeatSubState
|
||||
*/
|
||||
public var camPause:FunkinCamera;
|
||||
|
||||
/**
|
||||
* The camera which contains, and controls visibility of, the fade out to the results screen.
|
||||
*/
|
||||
public var camTransition:FunkinCamera;
|
||||
|
||||
/**
|
||||
* The combo popups. Includes the real-time combo counter and the rating.
|
||||
*/
|
||||
@@ -766,8 +772,6 @@ class PlayState extends MusicBeatSubState
|
||||
previousCameraFollowPoint = params.cameraFollowPoint;
|
||||
mirrorSongData = params.mirrored ?? false;
|
||||
|
||||
trace("Params: " + mirrorSongData + ", " + isBotPlayMode);
|
||||
|
||||
// Basic object initialization
|
||||
|
||||
// TODO: Add something to toggle this on!
|
||||
@@ -793,6 +797,7 @@ class PlayState extends MusicBeatSubState
|
||||
camCutouts = new FunkinCamera('playStateCamCutouts');
|
||||
camSubtitles = new FunkinCamera('playStateCamSubtitles');
|
||||
camPause = new FunkinCamera('playStateCamPause');
|
||||
camTransition = new FunkinCamera('playStateCamTransition');
|
||||
|
||||
var currentChart = currentSong.getDifficulty(currentDifficulty, currentVariation);
|
||||
var noteStyleId:Null<String> = currentChart?.noteStyle;
|
||||
@@ -807,7 +812,7 @@ class PlayState extends MusicBeatSubState
|
||||
// Healthbar
|
||||
healthBarBG = FunkinSprite.create(0, 0, 'healthBar');
|
||||
healthBar = new FlxBar(0, 0, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), null, 0, 2);
|
||||
scoreText = new FlxText(0, 0, 0, '', 20);
|
||||
scoreText = new FlxBitmapText(0, 0, '', FlxBitmapFont.fromAngelCode(Paths.font("vcr-bmp.png"), Paths.font("vcr-bmp.fnt")));
|
||||
|
||||
// Combo & Pop Up
|
||||
comboPopUps = new PopUpStuff(noteStyle);
|
||||
@@ -941,19 +946,10 @@ class PlayState extends MusicBeatSubState
|
||||
// This gets set back to false when the chart actually starts.
|
||||
startingSong = true;
|
||||
|
||||
// TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead.
|
||||
if ((currentSong.id ?? '').toLowerCase() == 'winter-horrorland')
|
||||
{
|
||||
// VanillaCutscenes will call startCountdown later.
|
||||
VanillaCutscenes.playHorrorStartCutscene();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Call a script event to start the countdown.
|
||||
// Songs with cutscenes should call event.cancel().
|
||||
// As long as they call `PlayState.instance.startCountdown()` later, the countdown will start.
|
||||
startCountdown();
|
||||
}
|
||||
// Call a script event to start the countdown.
|
||||
// Songs with cutscenes should call event.cancel().
|
||||
// As long as they call `PlayState.instance.startCountdown()` later, the countdown will start.
|
||||
startCountdown();
|
||||
|
||||
// Create the pause button.
|
||||
#if mobile
|
||||
@@ -1118,7 +1114,7 @@ class PlayState extends MusicBeatSubState
|
||||
cameraZoomRate = Constants.DEFAULT_ZOOM_RATE;
|
||||
|
||||
health = Constants.HEALTH_STARTING;
|
||||
songScore = 0;
|
||||
songScore = 0.0;
|
||||
Highscore.tallies.combo = 0;
|
||||
|
||||
// so the song doesn't start too early :D
|
||||
@@ -1241,8 +1237,6 @@ class PlayState extends MusicBeatSubState
|
||||
FlxG.watch.addQuick('health', health);
|
||||
FlxG.watch.addQuick('cameraBopIntensity', cameraBopIntensity);
|
||||
|
||||
// TODO: Add a song event for Handle GF dance speed.
|
||||
|
||||
// Handle player death.
|
||||
if (!isInCutscene && !disableKeys)
|
||||
{
|
||||
@@ -1358,17 +1352,25 @@ class PlayState extends MusicBeatSubState
|
||||
*/
|
||||
function pause(mode:PauseMode = Standard, lostFocus:Bool = false):Void
|
||||
{
|
||||
if (!mayPauseGame || justUnpaused || isGamePaused || isPlayerDying) return;
|
||||
if (!mayPauseGame || justUnpaused || isGamePaused || isPlayerDying || isSongEnd) return;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case Conversation:
|
||||
preparePauseUI();
|
||||
openPauseSubState(Conversation, camPause, lostFocus, () -> currentConversation?.pauseMusic());
|
||||
|
||||
final event = new PauseScriptEvent(false);
|
||||
dispatchEvent(event);
|
||||
|
||||
if (!event.eventCanceled) openPauseSubState(Conversation, camPause, lostFocus, () -> currentConversation?.pauseMusic());
|
||||
|
||||
case Cutscene:
|
||||
preparePauseUI();
|
||||
openPauseSubState(Cutscene, camPause, lostFocus, () -> VideoCutscene.pauseVideo());
|
||||
|
||||
final event = new PauseScriptEvent(false);
|
||||
dispatchEvent(event);
|
||||
|
||||
if (!event.eventCanceled) openPauseSubState(Cutscene, camPause, lostFocus, () -> VideoCutscene.pauseVideo());
|
||||
|
||||
default: // also known as standard
|
||||
if (!isInCountdown || isInCutscene) return;
|
||||
@@ -1444,7 +1446,7 @@ class PlayState extends MusicBeatSubState
|
||||
|
||||
vwooshTimer.cancel();
|
||||
|
||||
songScore = 0;
|
||||
songScore = 0.0;
|
||||
updateScoreText();
|
||||
|
||||
health = Constants.HEALTH_STARTING;
|
||||
@@ -1500,7 +1502,7 @@ class PlayState extends MusicBeatSubState
|
||||
dispatchEvent(eventEvent);
|
||||
|
||||
// Calling event.cancelEvent() skips the event. Neat!
|
||||
if (!eventEvent.eventCanceled)
|
||||
if (!eventEvent.eventCanceled && !shouldSubstatePause)
|
||||
{
|
||||
SongEventRegistry.handleEvent(event);
|
||||
}
|
||||
@@ -1511,15 +1513,17 @@ class PlayState extends MusicBeatSubState
|
||||
|
||||
public override function dispatchEvent(event:ScriptEvent):Void
|
||||
{
|
||||
// ORDER: Module, Song, Note, Stage, Conversation, Character
|
||||
// ORDER: Module, Song, Events, Notes, Stage, Conversation, Characters
|
||||
// Modules should get the first chance to cancel the event.
|
||||
|
||||
// super.dispatchEvent(event) dispatches event to module scripts.
|
||||
super.dispatchEvent(event);
|
||||
|
||||
// Dispatch event to song script.
|
||||
ScriptEventDispatcher.callEvent(currentSong, event);
|
||||
|
||||
// Dispatch event to event notes
|
||||
if (songEvents != null && songEvents.length > 0) SongEventRegistry.callEvent(event);
|
||||
|
||||
// Dispatch event to note kind scripts
|
||||
NoteKindManager.callEvent(event);
|
||||
|
||||
@@ -1843,8 +1847,11 @@ class PlayState extends MusicBeatSubState
|
||||
iconP2?.onStepHit(Std.int(Conductor.instance.currentStep));
|
||||
|
||||
// Only bop camera if zoom level is below 135%
|
||||
// This is an arbitrary number chosen so that the camera doesn't move insanely far in when the bop speed is fast.
|
||||
final MAX_RELATIVE_CAM_ZOOM:Float = 1.35;
|
||||
|
||||
if (Preferences.zoomCamera
|
||||
&& FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
|
||||
&& camHUD.zoom < (MAX_RELATIVE_CAM_ZOOM * defaultHUDCameraZoom)
|
||||
&& cameraZoomRate > 0
|
||||
&& (Conductor.instance.currentStep + cameraZoomRateOffset * Constants.STEPS_PER_BEAT) % (cameraZoomRate * Constants.STEPS_PER_BEAT) == 0)
|
||||
{
|
||||
@@ -1960,12 +1967,14 @@ class PlayState extends MusicBeatSubState
|
||||
camCutouts.bgColor.alpha = 0; // Show the game scene behind the camera.
|
||||
if (Preferences.subtitles) camSubtitles.bgColor.alpha = 0; // Show the game scene behind the camera.
|
||||
camPause.bgColor.alpha = 0; // Show the game scene behind the camera.
|
||||
camTransition.bgColor.alpha = 0;
|
||||
|
||||
FlxG.cameras.reset(camGame);
|
||||
FlxG.cameras.add(camHUD, false);
|
||||
FlxG.cameras.add(camCutscene, false);
|
||||
FlxG.cameras.add(camCutouts, false);
|
||||
if (Preferences.subtitles) FlxG.cameras.add(camSubtitles, false);
|
||||
FlxG.cameras.add(camTransition, false);
|
||||
FlxG.cameras.add(camPause, false);
|
||||
|
||||
// Configure camera follow point.
|
||||
@@ -2006,7 +2015,10 @@ class PlayState extends MusicBeatSubState
|
||||
// The score text below the health bar.
|
||||
scoreText.x = healthBarBG.x + healthBarBG.width - 190;
|
||||
scoreText.y = healthBarBG.y + 30;
|
||||
scoreText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
|
||||
scoreText.alignment = RIGHT;
|
||||
scoreText.borderStyle = OUTLINE;
|
||||
scoreText.borderColor = FlxColor.BLACK;
|
||||
scoreText.letterSpacing = -1;
|
||||
scoreText.scrollFactor.set();
|
||||
scoreText.zIndex = 802;
|
||||
add(scoreText);
|
||||
@@ -2617,7 +2629,6 @@ class PlayState extends MusicBeatSubState
|
||||
|
||||
if (vocals != null)
|
||||
{
|
||||
trace('Playing vocals...');
|
||||
add(vocals);
|
||||
|
||||
vocals.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
||||
@@ -2697,9 +2708,9 @@ class PlayState extends MusicBeatSubState
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Add an option for this maybe?
|
||||
var commaSeparated:Bool = true;
|
||||
scoreText.text = 'Score: ${FlxStringUtil.formatMoney(songScore, false, commaSeparated)}';
|
||||
final SHOW_DECIMALS:Bool = false;
|
||||
final COMMA_SEPARATED:Bool = true;
|
||||
scoreText.text = 'Score: ${FlxStringUtil.formatMoney(songScore, SHOW_DECIMALS, COMMA_SEPARATED)}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2758,6 +2769,16 @@ class PlayState extends MusicBeatSubState
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
if (event.eventCanceled) continue;
|
||||
|
||||
if (vocals != null)
|
||||
{
|
||||
if (vocals.legacyVoiceSystem)
|
||||
{
|
||||
if (vocals.legacyVoiceUsesPlayer) vocals.playerVolume = playerVocalsVolume;
|
||||
else
|
||||
vocals.opponentVolume = opponentVocalsVolume;
|
||||
}
|
||||
}
|
||||
|
||||
// Command the opponent to hit the note on time.
|
||||
// NOTE: This is what handles the strumline and cleaning up the note itself!
|
||||
opponentStrumline.hitNote(note);
|
||||
@@ -2865,7 +2886,7 @@ class PlayState extends MusicBeatSubState
|
||||
if (!isBotPlayMode && holdNote.scoreable)
|
||||
{
|
||||
health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
|
||||
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
|
||||
songScore += Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed;
|
||||
}
|
||||
|
||||
// Make sure the player keeps singing while the note is held by the bot.
|
||||
@@ -2898,7 +2919,7 @@ class PlayState extends MusicBeatSubState
|
||||
// If the base note of the hold was missed, don't penalize them more on top of that.
|
||||
var healthChangeMax = Constants.HEALTH_HOLD_DROP_PENALTY_MAX - (holdNote.hitNote ? -Constants.HEALTH_MISS_PENALTY : 0);
|
||||
var healthChange = healthChangeUncapped.clamp(healthChangeMax, 0);
|
||||
var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec);
|
||||
var scoreChange:Float = Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec;
|
||||
|
||||
var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true, Highscore.tallies.combo);
|
||||
dispatchEvent(event);
|
||||
@@ -2912,7 +2933,11 @@ class PlayState extends MusicBeatSubState
|
||||
// Play the miss sound.
|
||||
if (event.playSound)
|
||||
{
|
||||
if (vocals != null) vocals.playerVolume = 0;
|
||||
if (vocals != null)
|
||||
{
|
||||
if (vocals.legacyVoiceSystem && !vocals.legacyVoiceUsesPlayer) vocals.opponentVolume = 0;
|
||||
vocals.playerVolume = 0;
|
||||
}
|
||||
FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.5, 0.6));
|
||||
}
|
||||
}
|
||||
@@ -3014,9 +3039,7 @@ class PlayState extends MusicBeatSubState
|
||||
if (targetNote == null) continue;
|
||||
|
||||
// Judge and hit the note.
|
||||
// trace('Hit note! ${targetNote.noteData}');
|
||||
goodNoteHit(targetNote, input);
|
||||
// trace('Score: ${songScore}');
|
||||
|
||||
notesInDirection.remove(targetNote);
|
||||
|
||||
@@ -3090,7 +3113,11 @@ class PlayState extends MusicBeatSubState
|
||||
playerStrumline.hitNote(note, !event.isComboBreak);
|
||||
if (event.doesNotesplash) playerStrumline.playNoteSplash(note.noteData.getDirection());
|
||||
if (note.isHoldNote && note.holdNoteSprite != null) playerStrumline.playNoteHoldCover(note.holdNoteSprite);
|
||||
if (vocals != null) vocals.playerVolume = playerVocalsVolume;
|
||||
if (vocals != null)
|
||||
{
|
||||
if (vocals.legacyVoiceSystem && !vocals.legacyVoiceUsesPlayer) vocals.opponentVolume = opponentVocalsVolume;
|
||||
vocals.playerVolume = playerVocalsVolume;
|
||||
}
|
||||
|
||||
// Display the combo meter and add the calculation to the score.
|
||||
if (note.scoreable)
|
||||
@@ -3143,7 +3170,7 @@ class PlayState extends MusicBeatSubState
|
||||
var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
|
||||
hasPossibleNotes, // Whether there was a note you could have hit.
|
||||
Constants.HEALTH_GHOST_MISS_PENALTY, // How much health to add (negative).
|
||||
- 10 // Amount of score to add (negative).
|
||||
Constants.SCORE_GHOST_MISS_PENALTY // Amount of score to add (negative).
|
||||
);
|
||||
dispatchEvent(event);
|
||||
|
||||
@@ -3262,7 +3289,7 @@ class PlayState extends MusicBeatSubState
|
||||
/**
|
||||
* Handles applying health, score, and ratings.
|
||||
*/
|
||||
function applyScore(score:Int, daRating:String, healthChange:Float, isComboBreak:Bool)
|
||||
function applyScore(score:Float, daRating:String, healthChange:Float, isComboBreak:Bool)
|
||||
{
|
||||
switch (daRating)
|
||||
{
|
||||
@@ -3419,7 +3446,7 @@ class PlayState extends MusicBeatSubState
|
||||
{
|
||||
// crackhead double thingie, sets whether was new highscore, AND saves the song!
|
||||
var data = {
|
||||
score: songScore,
|
||||
score: Std.int(songScore),
|
||||
tallies: {
|
||||
sick: Highscore.tallies.sick,
|
||||
good: Highscore.tallies.good,
|
||||
@@ -3437,7 +3464,7 @@ class PlayState extends MusicBeatSubState
|
||||
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
|
||||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, songScore);
|
||||
Leaderboards.submitSongScore(currentSong.id, suffixedDifficulty, Std.int(songScore));
|
||||
#end
|
||||
|
||||
if (!isPracticeMode && !isBotPlayMode)
|
||||
@@ -3468,7 +3495,7 @@ class PlayState extends MusicBeatSubState
|
||||
|
||||
// Determine the score rank for this song we just finished.
|
||||
var scoreRank:Null<ScoringRank> = Scoring.calculateRank({
|
||||
score: songScore,
|
||||
score: Std.int(songScore),
|
||||
tallies: {
|
||||
sick: Highscore.tallies.sick,
|
||||
good: Highscore.tallies.good,
|
||||
@@ -3503,7 +3530,7 @@ class PlayState extends MusicBeatSubState
|
||||
{
|
||||
isNewHighscore = false;
|
||||
|
||||
PlayStatePlaylist.campaignScore += songScore;
|
||||
PlayStatePlaylist.campaignScore += Std.int(songScore);
|
||||
|
||||
// Pop the next song ID from the list.
|
||||
// Returns null if the list is empty.
|
||||
@@ -3627,7 +3654,7 @@ class PlayState extends MusicBeatSubState
|
||||
{
|
||||
if (isSubState)
|
||||
{
|
||||
if (isPlaytestResults)
|
||||
if (isPlaytestResults && !isBotPlayMode)
|
||||
{
|
||||
moveToResultsScreen(false, prevScoreData);
|
||||
}
|
||||
@@ -3769,13 +3796,11 @@ class PlayState extends MusicBeatSubState
|
||||
FlxG.camera.targetOffset.x += 20;
|
||||
|
||||
// Replace zoom animation with a fade out for now.
|
||||
FlxG.camera.fade(FlxColor.BLACK, 0.6);
|
||||
FlxTween.tween(camHUD, {alpha: 0}, 0.6);
|
||||
|
||||
FlxTween.tween(camHUD, {alpha: 0}, 0.6, {
|
||||
onComplete: function(_)
|
||||
{
|
||||
moveToResultsScreen(isNewHighscore, prevScoreData);
|
||||
}
|
||||
camTransition.fade(FlxColor.BLACK, 0.6, false, function()
|
||||
{
|
||||
moveToResultsScreen(isNewHighscore, prevScoreData);
|
||||
});
|
||||
|
||||
// Zoom in on Girlfriend (or BF if no GF)
|
||||
@@ -3829,7 +3854,7 @@ class PlayState extends MusicBeatSubState
|
||||
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
|
||||
prevScoreData: prevScoreData,
|
||||
scoreData: {
|
||||
score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
|
||||
score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : Std.int(songScore),
|
||||
tallies: {
|
||||
sick: talliesToUse.sick,
|
||||
good: talliesToUse.good,
|
||||
@@ -3910,9 +3935,11 @@ class PlayState extends MusicBeatSubState
|
||||
@:nullSafety(Off)
|
||||
FlxG.camera.target = null;
|
||||
|
||||
var adjustedDuration:Float = duration / playbackRate;
|
||||
|
||||
// Follow tween! Caching it so we can cancel/pause it later if needed.
|
||||
var followPos:FlxPoint = cameraFollowPoint.getPosition() - FlxPoint.weak(FlxG.camera.width * 0.5, FlxG.camera.height * 0.5);
|
||||
cameraFollowTween = FlxTween.tween(FlxG.camera.scroll, {x: followPos.x, y: followPos.y}, duration, {
|
||||
cameraFollowTween = FlxTween.tween(FlxG.camera.scroll, {x: followPos.x, y: followPos.y}, adjustedDuration, {
|
||||
ease: ease,
|
||||
onComplete: function(_)
|
||||
{
|
||||
@@ -3950,7 +3977,8 @@ class PlayState extends MusicBeatSubState
|
||||
else
|
||||
{
|
||||
// Zoom tween! Caching it so we can cancel/pause it later if needed.
|
||||
cameraZoomTween = FlxTween.tween(this, {currentCameraZoom: targetZoom}, duration, {ease: ease});
|
||||
var adjustedDuration:Float = duration / playbackRate;
|
||||
cameraZoomTween = FlxTween.tween(this, {currentCameraZoom: targetZoom}, adjustedDuration, {ease: ease});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3976,7 +4004,7 @@ class PlayState extends MusicBeatSubState
|
||||
/**
|
||||
* The magical function that shall tween the scroll speed.
|
||||
*/
|
||||
public function tweenScrollSpeed(?speed:Float, ?duration:Float, ?ease:Null<Float->Float>, strumlines:Array<String>):Void
|
||||
public function tweenScrollSpeed(?speed:Float, duration:Float = 0, ?ease:Null<Float->Float>, strumlines:Array<String>):Void
|
||||
{
|
||||
// Cancel the current tween if it's active.
|
||||
cancelScrollSpeedTweens();
|
||||
@@ -4003,9 +4031,11 @@ class PlayState extends MusicBeatSubState
|
||||
}
|
||||
else
|
||||
{
|
||||
var adjustedDuration:Float = duration / playbackRate;
|
||||
|
||||
scrollSpeedTweens.push(FlxTween.tween(strum, {
|
||||
'scrollSpeed': value
|
||||
}, duration, {ease: ease}));
|
||||
}, adjustedDuration, {ease: ease}));
|
||||
}
|
||||
// make sure charts dont break if the charter is dumb and stupid
|
||||
prevScrollTargets.push([value, i]);
|
||||
|
||||
@@ -941,7 +941,7 @@ class ResultState extends MusicBeatSubState
|
||||
var isScoreValid = !(params?.isPracticeMode ?? false) && !(params?.isBotPlayMode ?? false);
|
||||
var isPersonalBest = rank > Scoring.calculateRank(params?.prevScoreData);
|
||||
|
||||
if (isScoreValid && isPersonalBest)
|
||||
if ((isScoreValid && isPersonalBest) || params.forceRankSlam)
|
||||
{
|
||||
trace('THE RANK IS Higher.....');
|
||||
|
||||
@@ -1158,4 +1158,9 @@ typedef ResultsStateParams =
|
||||
* The previous score data, used for rank comparision.
|
||||
*/
|
||||
var ?prevScoreData:SaveScoreData;
|
||||
|
||||
/**
|
||||
* Forces to do the rank slamming animation in freeplay for debug purposes
|
||||
*/
|
||||
var ?forceRankSlam:Bool;
|
||||
};
|
||||
|
||||
@@ -253,6 +253,9 @@ class BaseCharacter extends Bopper
|
||||
// Set the x and y to be their original values.
|
||||
this.resetPosition();
|
||||
|
||||
// Resets danceEvery value to the original one.
|
||||
this.danceEvery = _data.danceEvery;
|
||||
|
||||
this.dance(true); // Force to avoid the old animation playing with the wrong offset at the start of the song.
|
||||
// Make sure we are playing the idle animation
|
||||
// ...then update the hitbox so that this.width and this.height are correct.
|
||||
|
||||
@@ -52,6 +52,8 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
|
||||
rating.velocity.y -= FlxG.random.int(140, 175);
|
||||
rating.velocity.x -= FlxG.random.int(0, 10);
|
||||
|
||||
rating.graphic.destroyOnNoUse = false;
|
||||
|
||||
add(rating);
|
||||
|
||||
var fadeEase = noteStyle.isJudgementSpritePixel(daRating) ? EaseUtil.stepped(2) : null;
|
||||
@@ -101,6 +103,8 @@ class PopUpStuff extends FlxTypedGroup<FunkinSprite>
|
||||
numScore.velocity.y -= FlxG.random.int(130, 150);
|
||||
numScore.velocity.x = FlxG.random.float(-5, 5);
|
||||
|
||||
numScore.graphic.destroyOnNoUse = false;
|
||||
|
||||
add(numScore);
|
||||
|
||||
var fadeEase = noteStyle.isComboNumSpritePixel(digit) ? EaseUtil.stepped(2) : null;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package funkin.play.cutscene;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.audio.FunkinSound;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.util.HapticUtil;
|
||||
|
||||
/**
|
||||
* Static methods for playing cutscenes in the PlayState.
|
||||
* TODO: Un-hardcode this shit!!!!!1!
|
||||
*/
|
||||
class VanillaCutscenes
|
||||
{
|
||||
static var blackScreen:FlxSprite;
|
||||
|
||||
static final TWEEN_DURATION:Float = 2.0;
|
||||
|
||||
/**
|
||||
* Plays the cutscene that appears at the start of Winter Horrorland.
|
||||
* TODO: Move this to `winter-horrorland.hxc`
|
||||
*/
|
||||
public static function playHorrorStartCutscene():Void
|
||||
{
|
||||
PlayState.instance.isInCutscene = true;
|
||||
PlayState.instance.camHUD.visible = false;
|
||||
|
||||
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
||||
blackScreen.scrollFactor.set(0, 0);
|
||||
blackScreen.zIndex = 1000000;
|
||||
PlayState.instance.add(blackScreen);
|
||||
|
||||
new FlxTimer().start(0.1, function(_)
|
||||
{
|
||||
trace('Playing horrorland cutscene...');
|
||||
PlayState.instance.remove(blackScreen);
|
||||
|
||||
// Force set the camera position and zoom.
|
||||
PlayState.instance.cameraFollowPoint.setPosition(400, -2050);
|
||||
PlayState.instance.resetCamera();
|
||||
FlxG.camera.zoom = 2.5;
|
||||
|
||||
// Play the Sound effect.
|
||||
HapticUtil.vibrate(0.1, 0.5, 1, 1);
|
||||
FunkinSound.playOnce(Paths.sound('Lights_Turn_On'), function()
|
||||
{
|
||||
// Fade in the HUD.
|
||||
trace('SFX done...');
|
||||
PlayState.instance.camHUD.visible = true;
|
||||
PlayState.instance.camHUD.alpha = 0.0; // Use alpha rather than visible to let us fade it in.
|
||||
FlxTween.tween(PlayState.instance.camHUD, {alpha: 1.0}, TWEEN_DURATION, {ease: FlxEase.quadInOut});
|
||||
|
||||
// Start the countdown.
|
||||
trace('Zoom out done...');
|
||||
trace('Begin countdown (ends cutscene)');
|
||||
PlayState.instance.startCountdown();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ class VideoCutscene
|
||||
{
|
||||
if (PlayState.instance == null) return;
|
||||
|
||||
#if FEATURE_VIDEO_PLAYBACK
|
||||
if (!openfl.Assets.exists(filePath))
|
||||
{
|
||||
// Display a popup.
|
||||
@@ -78,8 +79,7 @@ class VideoCutscene
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var rawFilePath = Paths.stripLibrary(filePath);
|
||||
#end
|
||||
|
||||
// Trigger the cutscene. Don't play the song in the background.
|
||||
PlayState.instance.isInCutscene = true;
|
||||
@@ -100,13 +100,18 @@ class VideoCutscene
|
||||
}
|
||||
#end
|
||||
|
||||
#if NO_FEATURE_VIDEO_PLAYBACK
|
||||
trace(" WARNING ".bold().bg_yellow() + " Video playback is not enabled. Calling video end callback instead.");
|
||||
finishVideo();
|
||||
#else
|
||||
#if html5
|
||||
playVideoHTML5(rawFilePath);
|
||||
playVideoHTML5(Paths.stripLibrary(filePath));
|
||||
#elseif hxvlc
|
||||
playVideoNative(filePath);
|
||||
#else
|
||||
throw "No video support for this platform!";
|
||||
#end
|
||||
#end
|
||||
}
|
||||
|
||||
public static function isPlaying():Bool
|
||||
|
||||
@@ -6,6 +6,7 @@ import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
|
||||
|
||||
/**
|
||||
* This class handles song events which change the camera focus.
|
||||
|
||||
@@ -139,16 +139,22 @@ class ScrollSpeedEvent extends SongEvent
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: ['In' => 'In', 'Out' => 'Out', 'In/Out' => 'InOut']
|
||||
}, {
|
||||
name: 'strumline',
|
||||
title: 'Target Strumline',
|
||||
defaultValue: DEFAULT_STRUMLINE,
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: ['Both' => 'both', 'Player' => 'player', 'Opponent' => 'opponent']
|
||||
}, {
|
||||
name: 'absolute',
|
||||
title: 'Absolute',
|
||||
defaultValue: DEFAULT_ABSOLUTE,
|
||||
type: SongEventFieldType.BOOL,
|
||||
name: 'advanced',
|
||||
title: 'Advanced',
|
||||
type: SongEventFieldType.FRAME,
|
||||
collapsible: true,
|
||||
children: [{
|
||||
name: 'strumline',
|
||||
title: 'Target Strumline',
|
||||
defaultValue: DEFAULT_STRUMLINE,
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: ['Both' => 'both', 'Player' => 'player', 'Opponent' => 'opponent']
|
||||
}, {
|
||||
name: 'absolute',
|
||||
title: 'Absolute',
|
||||
defaultValue: DEFAULT_ABSOLUTE,
|
||||
type: SongEventFieldType.BOOL,
|
||||
}]
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,20 +109,26 @@ class SetHealthIconSongEvent extends SongEvent
|
||||
defaultValue: DEFAULT_FLIPX,
|
||||
type: SongEventFieldType.BOOL,
|
||||
}, {
|
||||
name: 'isPixel',
|
||||
title: 'Is Pixel?',
|
||||
defaultValue: DEFAULT_ISPIXEL,
|
||||
type: SongEventFieldType.BOOL,
|
||||
}, {
|
||||
name: 'offsetX',
|
||||
title: 'X Offset',
|
||||
defaultValue: DEFAULT_X_OFFSET,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
}, {
|
||||
name: 'offsetY',
|
||||
title: 'Y Offset',
|
||||
defaultValue: DEFAULT_Y_OFFSET,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
name: 'advanced',
|
||||
title: 'Advanced',
|
||||
type: SongEventFieldType.FRAME,
|
||||
collapsible: true,
|
||||
children: [{
|
||||
name: 'isPixel',
|
||||
title: 'Is Pixel?',
|
||||
defaultValue: DEFAULT_ISPIXEL,
|
||||
type: SongEventFieldType.BOOL,
|
||||
}, {
|
||||
name: 'offsetX',
|
||||
title: 'X Offset',
|
||||
defaultValue: DEFAULT_X_OFFSET,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
}, {
|
||||
name: 'offsetY',
|
||||
title: 'Y Offset',
|
||||
defaultValue: DEFAULT_Y_OFFSET,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
}]
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package funkin.play.event;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.stage.Bopper;
|
||||
// Data from the chart
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class handles song events which changes dance speed of specific character or stage prop.
|
||||
*/
|
||||
class SetTargetBopSpeedSongEvent extends SongEvent
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super('SetTargetBopSpeed');
|
||||
}
|
||||
|
||||
static final DEFAULT_TARGET:String = 'boyfriend';
|
||||
|
||||
public override function handleEvent(data:SongEventData):Void
|
||||
{
|
||||
// Does nothing if there is no PlayState camera or stage.
|
||||
if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
|
||||
|
||||
var targetName:Null<String> = data.getString('target');
|
||||
if (targetName == null) targetName = DEFAULT_TARGET;
|
||||
|
||||
var rate = data.getFloat('rate');
|
||||
if (rate == null) rate = Constants.DEFAULT_PROP_RATE;
|
||||
|
||||
var target:FlxSprite = null;
|
||||
|
||||
switch (targetName)
|
||||
{
|
||||
case 'boyfriend' | 'bf' | 'player':
|
||||
trace('Set dance rate to $rate on boyfriend.');
|
||||
target = PlayState.instance.currentStage.getBoyfriend();
|
||||
case 'dad' | 'opponent':
|
||||
trace('Set dance rate to $rate on dad.');
|
||||
target = PlayState.instance.currentStage.getDad();
|
||||
case 'girlfriend' | 'gf':
|
||||
trace('Set dance rate to $rate on girlfriend.');
|
||||
target = PlayState.instance.currentStage.getGirlfriend();
|
||||
default:
|
||||
target = PlayState.instance.currentStage.getNamedProp(targetName);
|
||||
if (target == null) trace('Unknown target to set dance rate: $targetName');
|
||||
else
|
||||
trace('Set dance rate to $targetName from stage.');
|
||||
}
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
if (Std.isOfType(target, BaseCharacter))
|
||||
{
|
||||
var targetChar:BaseCharacter = cast target;
|
||||
targetChar.danceEvery = rate;
|
||||
}
|
||||
else if (Std.isOfType(target, Bopper))
|
||||
{
|
||||
var targetProp:Bopper = cast target;
|
||||
targetProp.danceEvery = rate;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Unknown SetTargetBopSpeed target: $targetName');
|
||||
}
|
||||
}
|
||||
|
||||
public override function getTitle():String
|
||||
{
|
||||
return "Set Target Bop Speed";
|
||||
}
|
||||
|
||||
/**
|
||||
* ```
|
||||
* {
|
||||
* "target": STRING, // Name of character or prop to point to.
|
||||
* "anim": STRING, // Name of animation to play.
|
||||
* }
|
||||
* ```
|
||||
* @return SongEventSchema
|
||||
*/
|
||||
public override function getEventSchema():SongEventSchema
|
||||
{
|
||||
return new SongEventSchema([{
|
||||
name: 'target',
|
||||
title: 'Target',
|
||||
type: SongEventFieldType.STRING,
|
||||
defaultValue: DEFAULT_TARGET,
|
||||
}, {
|
||||
name: 'rate',
|
||||
title: 'Rate',
|
||||
defaultValue: Constants.DEFAULT_PROP_RATE,
|
||||
min: 0,
|
||||
step: 0.25,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'beats/dance'
|
||||
}]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package funkin.play.event;
|
||||
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
|
||||
/**
|
||||
* Parameters used to initialize a song event.
|
||||
@@ -22,7 +24,7 @@ typedef SongEventParams =
|
||||
* It is used by the ScriptedSongEvent class to handle user-defined events,
|
||||
* and also used by other classes in this package to provide default behavior for built-in events.
|
||||
*/
|
||||
class SongEvent
|
||||
class SongEvent implements IPlayStateScriptedClass
|
||||
{
|
||||
/**
|
||||
* These variables are used in two different events (and may be in more), and in order not to create unnecessary variables, we store them here
|
||||
@@ -99,4 +101,92 @@ class SongEvent
|
||||
{
|
||||
return 'SongEvent(${this.id})';
|
||||
}
|
||||
|
||||
public function onScriptEvent(event:ScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onCreate(event:ScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onDestroy(event:ScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onUpdate(event:UpdateScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onStepHit(event:SongTimeScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onBeatHit(event:SongTimeScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onPause(event:PauseScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onResume(event:ScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onSongStart(event:ScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onSongEnd(event:ScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onGameOver(event:ScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onNoteIncoming(event:NoteScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onNoteHit(event:HitNoteScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onNoteMiss(event:NoteScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onNoteHoldDrop(event:HoldNoteScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onSongEvent(event:SongEventScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onCountdownStart(event:CountdownScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onCountdownStep(event:CountdownScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onCountdownEnd(event:CountdownScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onSongLoaded(event:SongLoadScriptEvent)
|
||||
{
|
||||
}
|
||||
|
||||
public function onSongRetry(event:SongRetryEvent)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,49 @@ class SongEventHelper
|
||||
return easeBitmapMap.get(key);
|
||||
}
|
||||
|
||||
static function getEaseRange(func:Dynamic, samples:Int):{min:Float, max:Float}
|
||||
{
|
||||
if (func == null || samples <= 0) return {min: 0.0, max: 1.0};
|
||||
|
||||
var min:Float = 0.0;
|
||||
var max:Float = 1.0;
|
||||
var hasValue:Bool = false;
|
||||
|
||||
for (i in 0...samples)
|
||||
{
|
||||
var t:Float = if (samples > 1) (i / (samples - 1)) else 0.0;
|
||||
var raw = func(t);
|
||||
if (Math.isNaN(raw)) continue;
|
||||
|
||||
if (!hasValue)
|
||||
{
|
||||
min = raw;
|
||||
max = raw;
|
||||
hasValue = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (raw < min) min = raw;
|
||||
if (raw > max) max = raw;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValue) return {min: 0.0, max: 1.0};
|
||||
return {min: min, max: max};
|
||||
}
|
||||
|
||||
static function getEaseY(raw:Float, size:Int, range:{min:Float, max:Float}):Int
|
||||
{
|
||||
var v:Float = raw;
|
||||
var span = range.max - range.min;
|
||||
|
||||
if (span > 0)
|
||||
v = (raw - range.min) / span;
|
||||
|
||||
v = if (v < 0) 0 else if (v > 1) 1 else v;
|
||||
return Std.int((1 - v) * (size - 1));
|
||||
}
|
||||
|
||||
static function createBitmapFromFunc(func:Dynamic, key:String, thickness:Int = 2):BitmapData
|
||||
{
|
||||
try
|
||||
@@ -62,6 +105,14 @@ class SongEventHelper
|
||||
if (key.toLowerCase() == "instant") return bd;
|
||||
if (thickness < 1) thickness = 1;
|
||||
var half = Std.int(thickness / 2);
|
||||
var range = getEaseRange(func, size);
|
||||
|
||||
// Draw horizontal lines at y=1 and y=0 for reference.
|
||||
var y1 = getEaseY(1, size, range);
|
||||
var y0 = getEaseY(0, size, range);
|
||||
bd.fillRect(new openfl.geom.Rectangle(0, y1 - half, size, thickness), 0xFF404040);
|
||||
bd.fillRect(new openfl.geom.Rectangle(0, y0 - half, size, thickness), 0xFF404040);
|
||||
|
||||
var lastY:Int = -1;
|
||||
for (i in 0...size)
|
||||
{
|
||||
@@ -69,21 +120,20 @@ class SongEventHelper
|
||||
var raw = func(t);
|
||||
if (!Math.isNaN(raw))
|
||||
{
|
||||
var v:Float = raw;
|
||||
if (v < 0) v = 0;
|
||||
if (v > 1) v = 1;
|
||||
var y:Int = Std.int((1 - v) * (size - 1));
|
||||
var y:Int = getEaseY(raw, size, range);
|
||||
if (lastY == -1)
|
||||
{
|
||||
for (yy in (y - half)...(y + half + 1))
|
||||
if (yy >= 0 && yy < size) bd.setPixel32(i, yy, 0xFFFFFFFF);
|
||||
for (xx in (i - half)...(i + half + 1))
|
||||
if (xx >= 0 && xx < size) for (yy in (y - half)...(y + half + 1))
|
||||
if (yy >= 0 && yy < size) bd.setPixel32(xx, yy, 0xFFFFFFFF);
|
||||
}
|
||||
else
|
||||
{
|
||||
var a = Std.int(Math.min(y, lastY));
|
||||
var b = Std.int(Math.max(y, lastY));
|
||||
for (yy in a - half...b + half + 1)
|
||||
if (yy >= 0 && yy < size) bd.setPixel32(i, yy, 0xFFFFFFFF);
|
||||
for (xx in (i - half)...(i + half + 1))
|
||||
if (xx >= 0 && xx < size) for (yy in a - half...b + half + 1)
|
||||
if (yy >= 0 && yy < size) bd.setPixel32(xx, yy, 0xFFFFFFFF);
|
||||
}
|
||||
lastY = y;
|
||||
}
|
||||
@@ -122,6 +172,7 @@ class SongEventHelper
|
||||
if (baseBd == null) return null;
|
||||
var easeFunc:Dynamic = resolveEaseFuncForKey(key);
|
||||
var sizeH:Int = baseBd.height;
|
||||
var range = getEaseRange(easeFunc, sizeH);
|
||||
var sprites:Array<FlxSprite> = [];
|
||||
for (f in 0...frameCount)
|
||||
{
|
||||
@@ -136,10 +187,7 @@ class SongEventHelper
|
||||
raw = FlxEase.linear(t);
|
||||
}
|
||||
if (Math.isNaN(raw)) raw = 0.0;
|
||||
var v:Float = raw;
|
||||
if (v < 0) v = 0;
|
||||
if (v > 1) v = 1;
|
||||
var y:Int = Std.int((1 - v) * (sizeH - 1));
|
||||
var y:Int = getEaseY(raw, sizeH, range);
|
||||
var bd:BitmapData = new BitmapData(dotWidth, sizeH, false, 0xFF202223);
|
||||
var centerX:Int = Std.int(dotWidth / 2);
|
||||
for (dx in -dotRadius...dotRadius + 1)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package funkin.play.event;
|
||||
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.ui.FullScreenScaleMode;
|
||||
// Data from the chart
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
@@ -27,9 +29,10 @@ class ZoomCameraSongEvent extends SongEvent
|
||||
});
|
||||
}
|
||||
|
||||
static final DEFAULT_ZOOM:Float = 1.0;
|
||||
static final DEFAULT_DURATION:Float = 4.0;
|
||||
static final DEFAULT_MODE:String = 'direct';
|
||||
public static final DEFAULT_ZOOM:Float = 1.0;
|
||||
public static final DEFAULT_WIDESCREEN_SCALE:Float = 0.0;
|
||||
public static final DEFAULT_DURATION:Float = 4.0;
|
||||
public static final DEFAULT_MODE:String = 'direct';
|
||||
|
||||
public override function handleEvent(data:SongEventData):Void
|
||||
{
|
||||
@@ -41,6 +44,11 @@ class ZoomCameraSongEvent extends SongEvent
|
||||
|
||||
var zoom:Float = data.getFloat('zoom') ?? DEFAULT_ZOOM;
|
||||
|
||||
var widescreenScaleX:Float = data.getFloat('widescreenScaleX') ?? DEFAULT_WIDESCREEN_SCALE;
|
||||
var widescreenScaleY:Float = data.getFloat('widescreenScaleY') ?? DEFAULT_WIDESCREEN_SCALE;
|
||||
|
||||
var scaledZoom:Float = zoom + (zoom * calculateScale(FullScreenScaleMode.wideScale, FlxPoint.get(widescreenScaleX, widescreenScaleY)));
|
||||
|
||||
var duration:Float = data.getFloat('duration') ?? DEFAULT_DURATION;
|
||||
|
||||
var mode:String = data.getString('mode') ?? DEFAULT_MODE;
|
||||
@@ -55,7 +63,7 @@ class ZoomCameraSongEvent extends SongEvent
|
||||
switch (ease)
|
||||
{
|
||||
case 'INSTANT':
|
||||
PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode);
|
||||
PlayState.instance.tweenCameraZoom(scaledZoom, 0, isDirectMode);
|
||||
default:
|
||||
var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
|
||||
var easeFunctionName = '$ease$easeDir';
|
||||
@@ -66,10 +74,15 @@ class ZoomCameraSongEvent extends SongEvent
|
||||
return;
|
||||
}
|
||||
|
||||
PlayState.instance.tweenCameraZoom(zoom, durSeconds, isDirectMode, easeFunction);
|
||||
PlayState.instance.tweenCameraZoom(scaledZoom, durSeconds, isDirectMode, easeFunction);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateScale(wideScale:FlxPoint, scale:FlxPoint)
|
||||
{
|
||||
return (wideScale.x - 1) * scale.x + (wideScale.y - 1) * scale.y;
|
||||
}
|
||||
|
||||
public override function getTitle():String
|
||||
{
|
||||
return 'Zoom Camera';
|
||||
@@ -104,12 +117,6 @@ class ZoomCameraSongEvent extends SongEvent
|
||||
step: 0.5,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'steps'
|
||||
}, {
|
||||
name: 'mode',
|
||||
title: 'Mode',
|
||||
defaultValue: DEFAULT_MODE,
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: ['Stage zoom' => 'stage', 'Absolute zoom' => 'direct']
|
||||
}, {
|
||||
name: 'ease',
|
||||
title: 'Easing Type',
|
||||
@@ -122,6 +129,34 @@ class ZoomCameraSongEvent extends SongEvent
|
||||
defaultValue: SongEvent.DEFAULT_EASE_DIR,
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: ['In' => 'In', 'Out' => 'Out', 'In/Out' => 'InOut']
|
||||
}, {
|
||||
name: 'advanced',
|
||||
title: 'Advanced',
|
||||
type: SongEventFieldType.FRAME,
|
||||
collapsible: true,
|
||||
children: [{
|
||||
name: 'mode',
|
||||
title: 'Mode',
|
||||
defaultValue: DEFAULT_MODE,
|
||||
type: SongEventFieldType.ENUM,
|
||||
keys: ['Stage zoom' => 'stage', 'Absolute zoom' => 'direct']
|
||||
}, {
|
||||
name: 'widescreenScaleX',
|
||||
title: 'Widescreen Scale X',
|
||||
defaultValue: DEFAULT_WIDESCREEN_SCALE,
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'x'
|
||||
}, {
|
||||
name: 'widescreenScaleY',
|
||||
title: 'Widescreen Scale Y',
|
||||
defaultValue: DEFAULT_WIDESCREEN_SCALE,
|
||||
min: 0,
|
||||
max: 1,
|
||||
type: SongEventFieldType.FLOAT,
|
||||
units: 'x'
|
||||
}]
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -972,7 +972,8 @@ class Strumline extends FlxSpriteGroup
|
||||
note.holdNoteSprite.hitNote = true;
|
||||
note.holdNoteSprite.missedNote = false;
|
||||
|
||||
note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
|
||||
note.holdNoteSprite.sustainLength = Math.min(note.holdNoteSprite.fullSustainLength,
|
||||
(note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition);
|
||||
}
|
||||
|
||||
#if FEATURE_GHOST_TAPPING
|
||||
@@ -1094,6 +1095,8 @@ class Strumline extends FlxSpriteGroup
|
||||
splash.y = this.y;
|
||||
splash.y -= INITIAL_OFFSET;
|
||||
splash.y += noteStyle.getSplashOffsets()[1] * splash.scale.y;
|
||||
|
||||
splash.graphic.destroyOnNoUse = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1171,6 +1174,8 @@ class Strumline extends FlxSpriteGroup
|
||||
noteSprite.x -= NUDGE;
|
||||
noteSprite.y = -9999;
|
||||
|
||||
noteSprite.graphic.destroyOnNoUse = false;
|
||||
|
||||
if (noteKind != null) noteSprite.scoreable = noteKind.scoreable;
|
||||
}
|
||||
|
||||
@@ -1212,6 +1217,8 @@ class Strumline extends FlxSpriteGroup
|
||||
holdNoteSprite.x -= holdNoteSprite.width / 2;
|
||||
holdNoteSprite.y = -9999;
|
||||
|
||||
holdNoteSprite.graphic.destroyOnNoUse = false;
|
||||
|
||||
if (noteKind != null) holdNoteSprite.scoreable = noteKind.scoreable;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,14 +120,14 @@ class SustainTrail extends FlxSprite
|
||||
{
|
||||
super(0, 0);
|
||||
|
||||
setupHoldNoteGraphic(noteStyle);
|
||||
noteStyleOffsets = noteStyle.getHoldNoteOffsets();
|
||||
|
||||
// BASIC SETUP
|
||||
this.sustainLength = sustainLength;
|
||||
this.fullSustainLength = sustainLength;
|
||||
this.noteDirection = noteDirection;
|
||||
|
||||
setupHoldNoteGraphic(noteStyle);
|
||||
noteStyleOffsets = noteStyle.getHoldNoteOffsets();
|
||||
|
||||
setIndices(TRIANGLE_VERTEX_INDICES);
|
||||
|
||||
this.active = true; // This NEEDS to be true for the note to be drawn!
|
||||
@@ -262,7 +262,6 @@ class SustainTrail extends FlxSprite
|
||||
{
|
||||
if (s < 0.0) s = 0.0;
|
||||
|
||||
if (sustainLength == s) return s;
|
||||
this.sustainLength = s;
|
||||
triggerRedraw();
|
||||
return this.sustainLength;
|
||||
|
||||
@@ -9,6 +9,6 @@ class NoAnimNoteKind extends NoteKind
|
||||
|
||||
public function new()
|
||||
{
|
||||
super("noanim", "Disables singing animations", null, [], DISABLE_ANIMATIONS, null);
|
||||
super('noanim', 'No Animation', null, [], DISABLE_ANIMATIONS, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package funkin.play.notes.notekind;
|
||||
|
||||
import funkin.modding.events.ScriptEvent.HitNoteScriptEvent;
|
||||
import funkin.modding.events.ScriptEvent.NoteScriptEvent;
|
||||
|
||||
/**
|
||||
@@ -10,7 +9,7 @@ class NonScoreableNoteKind extends NoteKind
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super("non_scoreable", "Non-scoreable");
|
||||
super('non_scoreable', 'Non-scoreable');
|
||||
scoreable = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,15 @@ class NoteKindManager
|
||||
return noteKinds.get(noteKind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of known valid note kinds.
|
||||
* @return A list of note kinds
|
||||
*/
|
||||
public static function listNoteKinds():Array<String>
|
||||
{
|
||||
return noteKinds.keyValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize custom behavior for note kinds.
|
||||
*/
|
||||
@@ -61,7 +70,7 @@ class NoteKindManager
|
||||
if (noteKindClsName == 'funkin.play.notes.notekind.NoteKind'
|
||||
|| noteKindClsName == 'funkin.play.notes.notekind.ScriptedNoteKind') continue;
|
||||
|
||||
var kind:NoteKind = Type.createInstance(noteKindCls, ["UNKNOWN"]);
|
||||
var kind:NoteKind = Type.createInstance(noteKindCls, ['UNKNOWN']);
|
||||
|
||||
if (kind != null)
|
||||
{
|
||||
@@ -88,12 +97,9 @@ class NoteKindManager
|
||||
{
|
||||
try
|
||||
{
|
||||
var script:NoteKind = ScriptedNoteKind.scriptInit(scriptedClass, "unknown");
|
||||
var script:NoteKind = ScriptedNoteKind.scriptInit(scriptedClass, 'unknown');
|
||||
trace(' Initialized scripted note kind: ${script.noteKind}');
|
||||
noteKinds.set(script.noteKind, script);
|
||||
#if FEATURE_CHART_EDITOR
|
||||
ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description);
|
||||
#end
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
|
||||
@@ -176,15 +176,15 @@ class Scoring
|
||||
|
||||
return switch (absTiming)
|
||||
{
|
||||
// case(_ < PBOT1_KILLER_THRESHOLD) => true:
|
||||
// case(_ <= PBOT1_KILLER_THRESHOLD) => true:
|
||||
// 'killer';
|
||||
case(_ < PBOT1_SICK_THRESHOLD) => true:
|
||||
case(_ <= PBOT1_SICK_THRESHOLD) => true:
|
||||
'sick';
|
||||
case(_ < PBOT1_GOOD_THRESHOLD) => true:
|
||||
case(_ <= PBOT1_GOOD_THRESHOLD) => true:
|
||||
'good';
|
||||
case(_ < PBOT1_BAD_THRESHOLD) => true:
|
||||
case(_ <= PBOT1_BAD_THRESHOLD) => true:
|
||||
'bad';
|
||||
case(_ < PBOT1_SHIT_THRESHOLD) => true:
|
||||
case(_ <= PBOT1_SHIT_THRESHOLD) => true:
|
||||
'shit';
|
||||
default:
|
||||
FlxG.log.warn('Missed note: Bad timing ($absTiming < $PBOT1_SHIT_THRESHOLD)');
|
||||
@@ -272,13 +272,13 @@ class Scoring
|
||||
|
||||
return switch (absTiming)
|
||||
{
|
||||
case(_ < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) => true:
|
||||
case(_ <= LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) => true:
|
||||
'sick';
|
||||
case(_ < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) => true:
|
||||
case(_ <= LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) => true:
|
||||
'good';
|
||||
case(_ < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) => true:
|
||||
case(_ <= LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) => true:
|
||||
'bad';
|
||||
case(_ < LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true:
|
||||
case(_ <= LEGACY_HIT_WINDOW * LEGACY_SHIT_THRESHOLD) => true:
|
||||
'shit';
|
||||
default:
|
||||
FlxG.log.warn('Missed note: Bad timing ($absTiming < $LEGACY_SHIT_THRESHOLD)');
|
||||
@@ -344,19 +344,19 @@ class Scoring
|
||||
static function judgeNoteWEEK7(msTiming:Float):String
|
||||
{
|
||||
var absTiming = Math.abs(msTiming);
|
||||
if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD)
|
||||
if (absTiming <= WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD)
|
||||
{
|
||||
return 'sick';
|
||||
}
|
||||
else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD)
|
||||
else if (absTiming <= WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD)
|
||||
{
|
||||
return 'good';
|
||||
}
|
||||
else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD)
|
||||
else if (absTiming <= WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD)
|
||||
{
|
||||
return 'bad';
|
||||
}
|
||||
else if (absTiming < WEEK7_HIT_WINDOW)
|
||||
else if (absTiming <= WEEK7_HIT_WINDOW)
|
||||
{
|
||||
return 'shit';
|
||||
}
|
||||
@@ -529,7 +529,7 @@ enum abstract ScoringRank(String)
|
||||
case SHIT:
|
||||
return 2 / 24;
|
||||
default:
|
||||
return 3.5;
|
||||
return 2 / 24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,7 +549,7 @@ enum abstract ScoringRank(String)
|
||||
case SHIT:
|
||||
return 95 / 24;
|
||||
default:
|
||||
return 3.5;
|
||||
return 95 / 24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,7 +569,7 @@ enum abstract ScoringRank(String)
|
||||
case SHIT:
|
||||
return 186 / 24;
|
||||
default:
|
||||
return 3.5;
|
||||
return 186 / 24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +589,7 @@ enum abstract ScoringRank(String)
|
||||
case SHIT:
|
||||
return 207 / 24;
|
||||
default:
|
||||
return 3.5;
|
||||
return 207 / 24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,7 +631,7 @@ enum abstract ScoringRank(String)
|
||||
case SHIT:
|
||||
return 'resultScreen/rankText/rankScrollLOSS';
|
||||
default:
|
||||
return 'resultScreen/rankText/rankScrollGOOD';
|
||||
return 'resultScreen/rankText/rankScrollLOSS';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,7 +652,7 @@ enum abstract ScoringRank(String)
|
||||
case SHIT:
|
||||
return 'resultScreen/rankText/rankTextLOSS';
|
||||
default:
|
||||
return 'resultScreen/rankText/rankTextGOOD';
|
||||
return 'resultScreen/rankText/rankTextLOSS';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,6 +672,8 @@ enum abstract ScoringRank(String)
|
||||
0xFFFF58B4;
|
||||
case PERFECT_GOLD:
|
||||
0xFFFFB619;
|
||||
default:
|
||||
0xFF6044FF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1018,11 +1018,25 @@ class SongDifficulty
|
||||
result.addOpponentVoice(FunkinSound.load(opponentVoice, 1.0, false, false, false, false, null, null, true));
|
||||
}
|
||||
|
||||
// Sometimes the sounds don't set their important value to true, so we have to do this manually.
|
||||
result.forEach(function(snd:FunkinSound)
|
||||
if (result.members.length == 0)
|
||||
{
|
||||
snd.important = true;
|
||||
});
|
||||
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
|
||||
// Try to use `Voices.ogg` if no other voices are found.
|
||||
var legacyPath = Paths.voices(this.song.id, '$suffix');
|
||||
if (Assets.exists(legacyPath))
|
||||
{
|
||||
result.addPlayerVoice(FunkinSound.load(legacyPath, 1.0, false, false, false, false, null, null, true));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.members.length == 1) // It's legacy'ing somewhere and i can prove it
|
||||
{
|
||||
result.legacyVoiceSystem = true;
|
||||
result.legacyVoiceUsesPlayer = result.getPlayerVoice(0) != null;
|
||||
}
|
||||
|
||||
// Sometimes the sounds don't set their important value to true, so we have to do this manually.
|
||||
result.forEach((snd:FunkinSound) -> snd.important = true);
|
||||
|
||||
result.playerVoicesOffset = offsets.getVocalOffset(characters.player, instId);
|
||||
result.opponentVoicesOffset = offsets.getVocalOffset(characters.opponent, instId);
|
||||
|
||||
@@ -145,6 +145,12 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
||||
prop.x = dataProp.position[0];
|
||||
prop.y = dataProp.position[1];
|
||||
prop.zIndex = dataProp.zIndex;
|
||||
|
||||
// Reset danceEvery value.
|
||||
if (Std.isOfType(prop, Bopper))
|
||||
{
|
||||
cast(prop, Bopper).danceEvery = dataProp.danceEvery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ class Save implements ConsoleClass
|
||||
/// MODS
|
||||
///
|
||||
@:saveProperty(data.mods.enabledMods)
|
||||
public var enabledModIds:SaveProperty<Array<String>>;
|
||||
public var enabledModDirs:SaveProperty<Array<String>>;
|
||||
|
||||
///
|
||||
/// CHART EDITOR OPTIONS
|
||||
@@ -1223,7 +1223,7 @@ typedef SaveDataOptions =
|
||||
var audioVisualOffset:Int;
|
||||
|
||||
/**
|
||||
* If we want the framerate to be unlocked on HTML5.
|
||||
* If we want the framerate to be unlocked.
|
||||
* @default `false`
|
||||
*/
|
||||
var unlockedFramerate:Bool;
|
||||
|
||||
@@ -31,8 +31,6 @@ class SaveProperty<T>
|
||||
|
||||
if (_autoFlush) Save.system.flush();
|
||||
|
||||
trace('[Save Property]: Changing value of $oldValue TO $newValue');
|
||||
|
||||
onChange.dispatch(newValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
||||
}
|
||||
|
||||
#if mobile
|
||||
private function updateDeviceNotch(notch:lime.math.Rectangle):Void
|
||||
private function updateDeviceNotch(notch:openfl.geom.Rectangle):Void
|
||||
{
|
||||
notchPosition.set(enabled ? notch.x : 0, enabled ? notch.y : 0);
|
||||
notchSize.set(enabled ? notch.width : 0, enabled ? notch.height : 0);
|
||||
@@ -403,11 +403,6 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
|
||||
gameNotchPosition *= scale;
|
||||
gameNotchSize *= scale;
|
||||
}
|
||||
|
||||
#if ios
|
||||
gameNotchPosition /= 2;
|
||||
gameNotchSize /= 2;
|
||||
#end
|
||||
}
|
||||
#end
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package funkin.ui;
|
||||
|
||||
import funkin.graphics.FlxFilteredSprite;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
|
||||
/**
|
||||
* The icon that gets used for Freeplay capsules and char select
|
||||
* NOT to be confused with the CharIcon class, which is for the in-game icons
|
||||
* NOT to be confused with the HealthIcon class, which is for the in-game icons
|
||||
*/
|
||||
@:nullSafety
|
||||
class PixelatedIcon extends FlxFilteredSprite
|
||||
class PixelatedIcon extends FunkinSprite
|
||||
{
|
||||
public var char:String;
|
||||
|
||||
@@ -25,7 +25,6 @@ class PixelatedIcon extends FlxFilteredSprite
|
||||
public function setCharacter(char:String):Void
|
||||
{
|
||||
if (this.char == char) return;
|
||||
this.char = char;
|
||||
|
||||
var charPath:String = "freeplay/icons/";
|
||||
|
||||
@@ -52,10 +51,9 @@ class PixelatedIcon extends FlxFilteredSprite
|
||||
this.visible = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
this.char = char; // if we went past this its safe to assume the icon exists so we can assign it
|
||||
|
||||
var isAnimated = Assets.exists(Paths.file('images/$charPath.xml'));
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ class IntroSubState extends MusicBeatSubState
|
||||
playVideoNative(LIGHTS_VIDEO_PATH);
|
||||
#end
|
||||
|
||||
#if NO_FEATURE_VIDEO_PLAYBACK
|
||||
onLightsEnd();
|
||||
#end
|
||||
|
||||
// // Im TOO lazy to even care, so uh, yep
|
||||
// FlxG.camera.zoom = 0.66666666666666666666666666666667;
|
||||
// vid.x = -(FlxG.width - (FlxG.width * FlxG.camera.zoom));
|
||||
|
||||
@@ -41,29 +41,37 @@ class Nametag extends FlxSprite
|
||||
|
||||
public function switchChar(str:String, playMosaicSequence:Bool = true):Void
|
||||
{
|
||||
if (playMosaicSequence) shaderEffect();
|
||||
var path:String = (str == "bf") ? "boyfriend" : "bf";
|
||||
if (str != "bf") path = str;
|
||||
|
||||
new FlxTimer().start(4 / 30, _ ->
|
||||
loadGraphic(Paths.image("charSelect/" + path + "Nametag"));
|
||||
updateHitbox();
|
||||
scale.set(0.77, 0.77);
|
||||
updatePosition();
|
||||
|
||||
// Reset shader to ensure the nametag doesn't get stuck
|
||||
if (playMosaicSequence)
|
||||
{
|
||||
var path:String = str;
|
||||
switch str
|
||||
mosaicShader.setBlockSize(1, 1);
|
||||
shaderEffect();
|
||||
|
||||
// Delay the shader effect by a bit to prevent lag.
|
||||
new FlxTimer().start(2 / 30, _ ->
|
||||
{
|
||||
case "bf":
|
||||
path = "boyfriend";
|
||||
}
|
||||
|
||||
loadGraphic(Paths.image('charSelect/' + path + "Nametag"));
|
||||
updateHitbox();
|
||||
scale.x = scale.y = 0.77;
|
||||
|
||||
updatePosition();
|
||||
|
||||
if (playMosaicSequence) shaderEffect(true);
|
||||
});
|
||||
shaderEffect(true);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mosaicShader.setBlockSize(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function shaderEffect(fadeOut:Bool = false):Void
|
||||
{
|
||||
// Skip the shader effect if the width is too small.
|
||||
if (width <= 1) return;
|
||||
|
||||
if (currentMosaicSequence != null)
|
||||
{
|
||||
// Forcibly reset the shader to prevent overlapping blur sequences
|
||||
|
||||
@@ -7,13 +7,11 @@ import openfl.display.Shape;
|
||||
import openfl.display.Sprite;
|
||||
import openfl.text.TextField;
|
||||
import openfl.text.TextFormat;
|
||||
import openfl.Lib;
|
||||
|
||||
/**
|
||||
* A debug overlay showing useful info.
|
||||
*/
|
||||
#if cpp
|
||||
@:access(lime._internal.backend.native.NativeCFFI)
|
||||
#end
|
||||
class FunkinDebugDisplay extends Sprite
|
||||
{
|
||||
static final UPDATE_DELAY:Int = 100;
|
||||
@@ -31,18 +29,16 @@ class FunkinDebugDisplay extends Sprite
|
||||
*/
|
||||
public var backgroundOpacity(default, set):Float = 0.5;
|
||||
|
||||
var currentFPS:Int;
|
||||
var deltaTimeout:Float;
|
||||
var times:Array<Float>;
|
||||
var color:Int;
|
||||
|
||||
#if !html5
|
||||
var fps:Int;
|
||||
var fpsPeak:Int;
|
||||
var gcMem:Float;
|
||||
var gcMemPeak:Float;
|
||||
|
||||
var taskMem:Float;
|
||||
var taskMemPeak:Float;
|
||||
#end
|
||||
|
||||
var background:Shape;
|
||||
|
||||
@@ -58,16 +54,18 @@ class FunkinDebugDisplay extends Sprite
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.currentFPS = 0;
|
||||
|
||||
this.deltaTimeout = 0.0;
|
||||
#if !html5
|
||||
this.times = [];
|
||||
this.color = color;
|
||||
|
||||
this.fps = 0;
|
||||
this.fpsPeak = 0;
|
||||
this.gcMem = 0.0;
|
||||
this.gcMemPeak = 0.0;
|
||||
this.taskMem = 0.0;
|
||||
this.taskMemPeak = 0.0;
|
||||
#end
|
||||
this.times = [];
|
||||
this.color = color;
|
||||
|
||||
this.backgroundOpacity = 0;
|
||||
this.isAdvanced = false;
|
||||
}
|
||||
@@ -76,13 +74,23 @@ class FunkinDebugDisplay extends Sprite
|
||||
{
|
||||
removeChildren(0, numChildren);
|
||||
|
||||
final BG_WIDTH_MULTIPLIER:Float = #if html5 advanced ? 1 : 0.3 #else 1 #end;
|
||||
var BG_WIDTH_MULTIPLIER:Float = advanced ? 1 : 0.3;
|
||||
|
||||
#if html5
|
||||
final BG_HEIGHT_MULTIPLIER:Float = advanced ? 0.45 : 0.15;
|
||||
#else
|
||||
final BG_HEIGHT_MULTIPLIER:Float = advanced ? 1 : (MemoryUtil.supportsTaskMem()) ? 0.3 : 0.2;
|
||||
#end
|
||||
if (MemoryUtil.supportsGCMem() || MemoryUtil.supportsTaskMem())
|
||||
{
|
||||
BG_WIDTH_MULTIPLIER = 1;
|
||||
}
|
||||
|
||||
var BG_HEIGHT_MULTIPLIER:Float = advanced ? 0.45 : 0.15;
|
||||
|
||||
if (MemoryUtil.supportsGCMem() && MemoryUtil.supportsTaskMem())
|
||||
{
|
||||
BG_HEIGHT_MULTIPLIER = advanced ? 1 : 0.3;
|
||||
}
|
||||
else if (MemoryUtil.supportsGCMem() || MemoryUtil.supportsTaskMem())
|
||||
{
|
||||
BG_HEIGHT_MULTIPLIER = advanced ? 0.7 : 0.2;
|
||||
}
|
||||
|
||||
background = new Shape();
|
||||
background.graphics.beginFill(0x3d3f41, 1);
|
||||
@@ -118,10 +126,12 @@ class FunkinDebugDisplay extends Sprite
|
||||
fpsGraph.minValue = 0;
|
||||
addChild(fpsGraph);
|
||||
|
||||
#if !html5
|
||||
gcMemGraph = new FunkinStatsGraph(OTHERS_OFFSET, Math.floor(OTHERS_OFFSET + (fpsGraph.y + fpsGraph.axisHeight) + 22), graphsWidth, graphsHeight, color);
|
||||
gcMemGraph.minValue = 0;
|
||||
addChild(gcMemGraph);
|
||||
if (MemoryUtil.supportsGCMem())
|
||||
{
|
||||
gcMemGraph = new FunkinStatsGraph(OTHERS_OFFSET, Math.floor(OTHERS_OFFSET + (fpsGraph.y + fpsGraph.axisHeight) + 22), graphsWidth, graphsHeight, color);
|
||||
gcMemGraph.minValue = 0;
|
||||
addChild(gcMemGraph);
|
||||
}
|
||||
|
||||
if (MemoryUtil.supportsTaskMem())
|
||||
{
|
||||
@@ -130,7 +140,6 @@ class FunkinDebugDisplay extends Sprite
|
||||
taskMemGraph.minValue = 0;
|
||||
addChild(taskMemGraph);
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
function createSimpleElements():Void
|
||||
@@ -147,15 +156,9 @@ class FunkinDebugDisplay extends Sprite
|
||||
addChild(infoDisplay);
|
||||
}
|
||||
|
||||
override function __enterFrame(deltaTime:Int):Void
|
||||
override function __enterFrame(deltaTime:Float):Void
|
||||
{
|
||||
#if cpp
|
||||
final currentTime:Float = lime._internal.backend.native.NativeCFFI.lime_sdl_get_ticks();
|
||||
#elseif html5
|
||||
final currentTime:Float = js.Browser.window.performance.now();
|
||||
#else
|
||||
final currentTime:Float = haxe.Timer.stamp() * 1000;
|
||||
#end
|
||||
final currentTime:Float = Lib.getTimer();
|
||||
|
||||
times.push(currentTime);
|
||||
|
||||
@@ -170,12 +173,16 @@ class FunkinDebugDisplay extends Sprite
|
||||
return;
|
||||
}
|
||||
|
||||
currentFPS = times.length;
|
||||
fps = times.length;
|
||||
|
||||
#if !html5
|
||||
gcMem = MemoryUtil.getGCMemory();
|
||||
if (fps > fpsPeak) fpsPeak = fps;
|
||||
|
||||
if (gcMem > gcMemPeak) gcMemPeak = gcMem;
|
||||
if (MemoryUtil.supportsGCMem())
|
||||
{
|
||||
gcMem = MemoryUtil.getGCMemory();
|
||||
|
||||
if (gcMem > gcMemPeak) gcMemPeak = gcMem;
|
||||
}
|
||||
|
||||
if (MemoryUtil.supportsTaskMem())
|
||||
{
|
||||
@@ -183,7 +190,6 @@ class FunkinDebugDisplay extends Sprite
|
||||
|
||||
if (taskMem > taskMemPeak) taskMemPeak = taskMem;
|
||||
}
|
||||
#end
|
||||
|
||||
if (isAdvanced)
|
||||
{
|
||||
@@ -200,25 +206,24 @@ class FunkinDebugDisplay extends Sprite
|
||||
function updateAdvancedDisplay():Void
|
||||
{
|
||||
updateFPSGraph();
|
||||
#if !html5
|
||||
updateGcMemGraph();
|
||||
updateTaskMemGraph();
|
||||
#end
|
||||
|
||||
final info:Array<String> = [];
|
||||
info.push('FPS: $currentFPS');
|
||||
info.push('FPS: $fps');
|
||||
info.push('AVG FPS: ${Math.floor(fpsGraph.average())}');
|
||||
info.push('1% LOW FPS: ${Math.floor(fpsGraph.lowest())}');
|
||||
fpsGraph.textDisplay.text = info.join('\n');
|
||||
|
||||
#if !html5
|
||||
gcMemGraph.textDisplay.text = 'GC MEM: ${FlxStringUtil.formatBytes(gcMem).toLowerCase()} / ${FlxStringUtil.formatBytes(gcMemPeak).toLowerCase()}';
|
||||
if (gcMemGraph != null)
|
||||
{
|
||||
gcMemGraph.textDisplay.text = 'GC MEM: ${FlxStringUtil.formatBytes(gcMem).toLowerCase()} / ${FlxStringUtil.formatBytes(gcMemPeak).toLowerCase()}';
|
||||
}
|
||||
|
||||
if (taskMemGraph != null)
|
||||
{
|
||||
taskMemGraph.textDisplay.text = 'TASK MEM: ${FlxStringUtil.formatBytes(taskMem).toLowerCase()} / ${FlxStringUtil.formatBytes(taskMemPeak).toLowerCase()}';
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
function updateSimpleDisplay():Void
|
||||
@@ -227,33 +232,38 @@ class FunkinDebugDisplay extends Sprite
|
||||
{
|
||||
final info:Array<String> = [];
|
||||
|
||||
info.push('FPS: $currentFPS');
|
||||
info.push('FPS: $fps');
|
||||
|
||||
#if !html5
|
||||
info.push('GC MEM: ${FlxStringUtil.formatBytes(gcMem).toLowerCase()} / ${FlxStringUtil.formatBytes(gcMemPeak).toLowerCase()}');
|
||||
if (MemoryUtil.supportsGCMem())
|
||||
{
|
||||
info.push('GC MEM: ${FlxStringUtil.formatBytes(gcMem).toLowerCase()} / ${FlxStringUtil.formatBytes(gcMemPeak).toLowerCase()}');
|
||||
}
|
||||
|
||||
if (MemoryUtil.supportsTaskMem())
|
||||
{
|
||||
info.push('TASK MEM: ${FlxStringUtil.formatBytes(taskMem).toLowerCase()} / ${FlxStringUtil.formatBytes(taskMemPeak).toLowerCase()}');
|
||||
#end
|
||||
}
|
||||
|
||||
infoDisplay.text = info.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFPSGraph(?currentFPS:Int = 0):Void
|
||||
function updateFPSGraph():Void
|
||||
{
|
||||
fpsGraph.maxValue = FlxG.drawFramerate;
|
||||
fpsGraph.update(times.length);
|
||||
fpsGraph.maxValue = fpsPeak;
|
||||
fpsGraph.update(fps);
|
||||
}
|
||||
|
||||
#if !html5
|
||||
function updateGcMemGraph(?currentFPS:Int = 0):Void
|
||||
function updateGcMemGraph():Void
|
||||
{
|
||||
gcMemGraph.maxValue = gcMemPeak;
|
||||
gcMemGraph.update(gcMem);
|
||||
if (gcMemGraph != null)
|
||||
{
|
||||
gcMemGraph.maxValue = gcMemPeak;
|
||||
gcMemGraph.update(gcMem);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTaskMemGraph(?currentFPS:Int = 0):Void
|
||||
function updateTaskMemGraph():Void
|
||||
{
|
||||
if (taskMemGraph != null)
|
||||
{
|
||||
@@ -261,7 +271,6 @@ class FunkinDebugDisplay extends Sprite
|
||||
taskMemGraph.update(taskMem);
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
function set_isAdvanced(value:Bool):Bool
|
||||
{
|
||||
|
||||
@@ -93,10 +93,7 @@ class DebugBoundingState extends FlxState
|
||||
var viewDropdown:DropDown = offsetEditorDialog.findComponent("swapper", DropDown);
|
||||
viewDropdown.onChange = function(e:UIEvent)
|
||||
{
|
||||
trace(e.type);
|
||||
curView = cast e?.data?.curView;
|
||||
trace(e.data);
|
||||
// trace(e.data);
|
||||
};
|
||||
|
||||
offsetAnimationDropdown = offsetEditorDialog.findComponent("animationDropdown", DropDown);
|
||||
@@ -451,7 +448,8 @@ class DebugBoundingState extends FlxState
|
||||
// Plays the idle animation
|
||||
if (FlxG.keys.justPressed.SPACE)
|
||||
{
|
||||
offsetAnimationDropdown.value = {id: 'idle', text: 'idle'};
|
||||
if (swagChar?.hasAnimation('danceLeft')) offsetAnimationDropdown.value = {id: 'danceLeft', text: 'danceLeft'};
|
||||
else offsetAnimationDropdown.value = {id: 'idle', text: 'idle'};
|
||||
|
||||
playCharacterAnimation(currentAnimationName, true);
|
||||
}
|
||||
@@ -568,21 +566,18 @@ class DebugBoundingState extends FlxState
|
||||
for (i in swagChar.animationOffsets.keys())
|
||||
{
|
||||
characterAnimNames.push(i);
|
||||
trace(i);
|
||||
trace(swagChar.animationOffsets[i]);
|
||||
}
|
||||
|
||||
offsetAnimationDropdown.dataSource.clear();
|
||||
|
||||
for (charAnim in characterAnimNames)
|
||||
{
|
||||
trace('Adding ${charAnim} to HaxeUI dropdown');
|
||||
offsetAnimationDropdown.dataSource.add({id: charAnim, text: charAnim});
|
||||
}
|
||||
|
||||
offsetAnimationDropdown.selectedIndex = 0;
|
||||
|
||||
trace('Added ${offsetAnimationDropdown.dataSource.size} to HaxeUI dropdown');
|
||||
trace('Added ${offsetAnimationDropdown.dataSource.size} animations to HaxeUI dropdown');
|
||||
|
||||
offsetAnimationDropdown.onChange = function(event:UIEvent)
|
||||
{
|
||||
@@ -607,7 +602,6 @@ class DebugBoundingState extends FlxState
|
||||
// var animName = characterAnimNames[Std.parseInt(str)];
|
||||
var animName = str;
|
||||
swagChar.playAnimation(animName, true); // trace();
|
||||
trace(swagChar.animationOffsets.get(animName));
|
||||
|
||||
txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
|
||||
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
|
||||
|
||||
@@ -58,6 +58,7 @@ import funkin.ui.debug.charting.commands.ChartEditorCommand;
|
||||
import funkin.ui.debug.charting.commands.CopyItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.CutItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.DeselectAllItemsBetweenTimeCommand;
|
||||
import funkin.ui.debug.charting.commands.DeselectItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
|
||||
import funkin.ui.debug.charting.commands.FlipNotesCommand;
|
||||
@@ -72,6 +73,7 @@ import funkin.ui.debug.charting.commands.RemoveItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveStackedNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.SelectAllItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.SelectAllItemsBetweenTimeCommand;
|
||||
import funkin.ui.debug.charting.commands.SelectItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
|
||||
import funkin.ui.debug.charting.commands.SwitchDifficultyCommand;
|
||||
@@ -292,6 +294,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
{
|
||||
this.songLengthInMs = value;
|
||||
|
||||
resetPreviewTimes();
|
||||
updateGridHeight();
|
||||
|
||||
return this.songLengthInMs;
|
||||
@@ -789,6 +792,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
return hitsoundVolumePlayer + hitsoundVolumeOpponent > 0;
|
||||
}
|
||||
|
||||
var stretchySound1:Null<FunkinSound> = null;
|
||||
var stretchySound2:Null<FunkinSound> = null;
|
||||
|
||||
// Auto-save
|
||||
|
||||
/**
|
||||
@@ -1144,6 +1150,22 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
*/
|
||||
var commandHistoryDirty:Bool = true;
|
||||
|
||||
/**
|
||||
* Whether the selection has changed and the edit buttons need to be updated.
|
||||
*/
|
||||
var editButtonsDirty:Bool = true;
|
||||
|
||||
/**
|
||||
* Whether the clipboard has changed and the paste buttons need to be updated.
|
||||
*/
|
||||
var clipboardDirty:Bool = true;
|
||||
|
||||
/**
|
||||
* Whether the clipboard is valid and contains a json of notes and events.
|
||||
*/
|
||||
var clipboardValid:Bool = true;
|
||||
|
||||
|
||||
/**
|
||||
* If true, we are currently in the process of quitting the chart editor.
|
||||
* Skip any update functions as most of them will call a crash.
|
||||
@@ -1413,6 +1435,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
function set_currentSongMetadata(value:SongMetadata):SongMetadata
|
||||
{
|
||||
songMetadata.set(selectedVariation, value);
|
||||
|
||||
resetPreviewTimes();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1558,28 +1583,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
return currentSongMetadata.playData.noteStyle = value;
|
||||
}
|
||||
|
||||
var currentSongFreeplayPreviewStart(get, set):Int;
|
||||
var currentSongFreeplayPreviewStart(get, set):Float;
|
||||
|
||||
function get_currentSongFreeplayPreviewStart():Int
|
||||
function get_currentSongFreeplayPreviewStart():Float
|
||||
{
|
||||
return currentSongMetadata.playData.previewStart;
|
||||
}
|
||||
|
||||
function set_currentSongFreeplayPreviewStart(value:Int):Int
|
||||
function set_currentSongFreeplayPreviewStart(value:Float):Float
|
||||
{
|
||||
return currentSongMetadata.playData.previewStart = value;
|
||||
return currentSongMetadata.playData.previewStart = value * (value < 1 ? songLengthInMs : 1);
|
||||
}
|
||||
|
||||
var currentSongFreeplayPreviewEnd(get, set):Int;
|
||||
var currentSongFreeplayPreviewEnd(get, set):Float;
|
||||
|
||||
function get_currentSongFreeplayPreviewEnd():Int
|
||||
function get_currentSongFreeplayPreviewEnd():Float
|
||||
{
|
||||
return currentSongMetadata.playData.previewEnd;
|
||||
}
|
||||
|
||||
function set_currentSongFreeplayPreviewEnd(value:Int):Int
|
||||
function set_currentSongFreeplayPreviewEnd(value:Float):Float
|
||||
{
|
||||
return currentSongMetadata.playData.previewEnd = value;
|
||||
return currentSongMetadata.playData.previewEnd = value * (value < 1 ? songLengthInMs : 1);
|
||||
}
|
||||
|
||||
var currentSongStage(get, set):String;
|
||||
@@ -1959,14 +1984,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
var menubarItemSelectRegion:MenuItem;
|
||||
|
||||
/**
|
||||
* The `Edit -> Select Before Cursor` menu item.
|
||||
* The `Edit -> Select Before Playhead` menu item.
|
||||
*/
|
||||
var menubarItemSelectBeforeCursor:MenuItem;
|
||||
var menubarItemSelectBeforePlayhead:MenuItem;
|
||||
|
||||
/**
|
||||
* The `Edit -> Select After Cursor` menu item.
|
||||
* The `Edit -> Select After Playhead` menu item.
|
||||
*/
|
||||
var menubarItemSelectAfterCursor:MenuItem;
|
||||
var menubarItemSelectAfterPlayhead:MenuItem;
|
||||
|
||||
/**
|
||||
* The `Edit -> Decrease Note Snap Precision` menu item.
|
||||
@@ -2525,6 +2550,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
this.welcomeMusic.looped = true;
|
||||
}
|
||||
|
||||
public function resetPreviewTimes() {
|
||||
currentSongFreeplayPreviewStart = (currentSongMetadata?.playData?.previewStart ?? Constants.DEFAULT_PREVIEW_START_TIME);
|
||||
currentSongFreeplayPreviewEnd = (currentSongMetadata?.playData?.previewEnd ?? Constants.DEFAULT_PREVIEW_END_TIME);
|
||||
}
|
||||
|
||||
public function loadPreferences():Void
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
@@ -2555,7 +2585,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
menubarItemVolumeInstrumental.value = Std.int(save.chartEditorInstVolume.value * 100);
|
||||
menubarItemVolumeVocalsPlayer.value = Std.int(save.chartEditorPlayerVoiceVolume.value * 100);
|
||||
menubarItemVolumeVocalsOpponent.value = Std.int(save.chartEditorOpponentVoiceVolume.value * 100);
|
||||
menubarItemPlaybackSpeed.value = Std.int(save.chartEditorPlaybackSpeed.value * 100.0);
|
||||
menubarItemPlaybackSpeed.value = Math.round(save.chartEditorPlaybackSpeed.value * 100.0);
|
||||
}
|
||||
|
||||
public function writePreferences(hasBackup:Bool):Void
|
||||
@@ -3027,7 +3057,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
{
|
||||
if (!isCursorOverHaxeUI && !isPlaytesting)
|
||||
{
|
||||
this.openCharacterDropdown(CharacterType.DAD, true);
|
||||
if (pressingControl())
|
||||
{
|
||||
this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.openCharacterDropdown(CharacterType.DAD, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3035,7 +3072,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
{
|
||||
if (!isCursorOverHaxeUI && !isPlaytesting)
|
||||
{
|
||||
this.openCharacterDropdown(CharacterType.BF, true);
|
||||
if (pressingControl())
|
||||
{
|
||||
this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.openCharacterDropdown(CharacterType.BF, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3287,6 +3331,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
|
||||
menubarItemSelectNone.onClick = _ -> performCommand(new DeselectAllItemsCommand());
|
||||
|
||||
menubarItemSelectBeforePlayhead.onClick = _ -> performCommand(new SelectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, true, true, true));
|
||||
|
||||
menubarItemSelectAfterPlayhead.onClick = _ -> performCommand(new SelectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, false, true, true));
|
||||
|
||||
menubarItemPlaytestFull.onClick = _ -> testSongInPlayState(false);
|
||||
menubarItemPlaytestMinimal.onClick = _ -> testSongInPlayState(true);
|
||||
|
||||
@@ -3442,7 +3490,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
if (audioInstTrack != null) audioInstTrack.volume = volume;
|
||||
menubarLabelVolumeInstrumental.text = 'Instrumental - ${Std.int(event.value)}%';
|
||||
};
|
||||
previousAudioVolumes[3] = menubarLabelVolumeInstrumental.value;
|
||||
previousAudioVolumes[3] = menubarItemVolumeInstrumental.value;
|
||||
|
||||
menubarItemVolumeVocalsPlayer.onChange = event ->
|
||||
{
|
||||
@@ -3450,7 +3498,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
audioVocalTrackGroup.playerVolume = volume;
|
||||
menubarLabelVolumeVocalsPlayer.text = 'Player - ${Std.int(event.value)}%';
|
||||
};
|
||||
previousAudioVolumes[4] = menubarLabelVolumeVocalsPlayer.value;
|
||||
previousAudioVolumes[4] = menubarItemVolumeVocalsPlayer.value;
|
||||
|
||||
menubarItemVolumeVocalsOpponent.onChange = event ->
|
||||
{
|
||||
@@ -3458,7 +3506,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
audioVocalTrackGroup.opponentVolume = volume;
|
||||
menubarLabelVolumeVocalsOpponent.text = 'Enemy - ${Std.int(event.value)}%';
|
||||
};
|
||||
previousAudioVolumes[5] = menubarLabelVolumeVocalsOpponent.value;
|
||||
previousAudioVolumes[5] = menubarItemVolumeVocalsOpponent.value;
|
||||
|
||||
menubarItemPlaybackSpeed.onChange = event ->
|
||||
{
|
||||
@@ -3497,6 +3545,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
|
||||
function copySelection():Void
|
||||
{
|
||||
clipboardDirty = true;
|
||||
clipboardValid = true;
|
||||
// Doesn't use a command because it's not undoable.
|
||||
|
||||
// Calculate a single time offset for all the notes and events.
|
||||
@@ -3593,7 +3643,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
displayAutosavePopup = false;
|
||||
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
|
||||
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [{
|
||||
text: "Take Me There",
|
||||
text: "Open In Folder",
|
||||
callback: openBackupsFolder,
|
||||
}]);
|
||||
}
|
||||
@@ -4058,7 +4108,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
eventSprite.parentState = this;
|
||||
// trace('Creating new Event... (${renderedEvents.members.length})');
|
||||
|
||||
if (eventData?.value != null && (eventData?.value?.ease != null && eventData?.value?.easeDir == null))
|
||||
if (eventData?.value != null && (eventData.getString("ease") != null && eventData.getInt("easeDir") == null))
|
||||
{
|
||||
eventData.value = migrateEventEaseDirectionFields(eventData.value);
|
||||
}
|
||||
@@ -4505,7 +4555,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
}
|
||||
|
||||
// HOME = Scroll to Top
|
||||
if (FlxG.keys.justPressed.HOME)
|
||||
if (!FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.HOME)
|
||||
{
|
||||
// Scroll amount is the difference between the current position and the top.
|
||||
scrollAmount = 0 - this.scrollPositionInPixels;
|
||||
@@ -4521,7 +4571,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
}
|
||||
|
||||
// END = Scroll to Bottom
|
||||
if (FlxG.keys.justPressed.END)
|
||||
if (!FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.END)
|
||||
{
|
||||
// Scroll amount is the difference between the current position and the bottom.
|
||||
scrollAmount = this.songLengthInPixels - this.scrollPositionInPixels;
|
||||
@@ -5169,8 +5219,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
{
|
||||
if (dragLengthCurrent != dragLengthSteps)
|
||||
{
|
||||
stretchySounds = !stretchySounds;
|
||||
this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
|
||||
this.playStretchySound();
|
||||
|
||||
dragLengthCurrent = dragLengthSteps;
|
||||
}
|
||||
@@ -5847,8 +5896,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
|
||||
if (playheadDragLengthCurrent[column] != targetNoteLengthStepsInt)
|
||||
{
|
||||
stretchySounds = !stretchySounds;
|
||||
this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
|
||||
this.playStretchySound();
|
||||
playheadDragLengthCurrent[column] = targetNoteLengthStepsInt;
|
||||
}
|
||||
ghostHold.visible = true;
|
||||
@@ -6213,6 +6261,26 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
// Deselect all items.
|
||||
performCommand(new DeselectAllItemsCommand());
|
||||
}
|
||||
|
||||
// SHIFT + Home = Select all above playhead
|
||||
if (FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.HOME)
|
||||
{
|
||||
// CTRL + SHIFT + Home = Inverse - deselect all above playhead
|
||||
if (FlxG.keys.pressed.CONTROL)
|
||||
performCommand(new DeselectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, true, true, true));
|
||||
else
|
||||
performCommand(new SelectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, true, true, true));
|
||||
}
|
||||
|
||||
// SHIFT + End = Select all below playhead
|
||||
if (FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.END)
|
||||
{
|
||||
// CTRL + SHIFT + Home = Inverse - deselect all below playhead
|
||||
if (FlxG.keys.pressed.CONTROL)
|
||||
performCommand(new DeselectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, false, true, true));
|
||||
else
|
||||
performCommand(new SelectAllItemsBetweenTimeCommand(scrollPositionInMs + playheadPositionInMs, false, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6479,6 +6547,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
|
||||
LoadingState.loadPlayState(targetStateParams, false, true, function(targetState)
|
||||
{
|
||||
if (playbarHeadDragging) playbarHeadDragging = false;
|
||||
// Apply volume settings.
|
||||
if (playtestAudioSettings)
|
||||
{
|
||||
@@ -6924,7 +6993,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
{
|
||||
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
|
||||
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [{
|
||||
text: "Take Me There",
|
||||
text: "Open In Folder",
|
||||
callback: openBackupsFolder,
|
||||
}]);
|
||||
});
|
||||
@@ -7066,6 +7135,50 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
||||
menubarItemRedo.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}';
|
||||
}
|
||||
}
|
||||
if (clipboardDirty)
|
||||
{
|
||||
clipboardDirty = false;
|
||||
|
||||
if (funkin.util.ClipboardUtil.getClipboard() == null || !clipboardValid)
|
||||
{
|
||||
menubarItemPaste.disabled = true;
|
||||
menubarItemPasteUnsnapped.disabled = true;
|
||||
clipboardValid = false;
|
||||
}
|
||||
else if (clipboardValid)
|
||||
{
|
||||
menubarItemPaste.disabled = false;
|
||||
menubarItemPasteUnsnapped.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (editButtonsDirty)
|
||||
{
|
||||
editButtonsDirty = false;
|
||||
|
||||
if (currentEventSelection.length > 0 || currentNoteSelection.length > 0)
|
||||
{
|
||||
menubarItemCopy.disabled = false;
|
||||
menubarItemCut.disabled = false;
|
||||
menubarItemDelete.disabled = false;
|
||||
menubarItemSelectNone.disabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
menubarItemCopy.disabled = true;
|
||||
menubarItemCut.disabled = true;
|
||||
menubarItemDelete.disabled = true;
|
||||
menubarItemSelectNone.disabled = true;
|
||||
}
|
||||
if (currentNoteSelection.length > 0)
|
||||
{
|
||||
menubarItemFlipNotes.disabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
menubarItemFlipNotes.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,6 +42,7 @@ class AddEventsCommand implements ChartEditorCommand
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
@@ -52,10 +53,12 @@ class AddEventsCommand implements ChartEditorCommand
|
||||
|
||||
state.currentNoteSelection = [];
|
||||
state.currentEventSelection = [];
|
||||
state.playSound(Paths.sound('chartingSounds/undo'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ class AddNotesCommand implements ChartEditorCommand
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
@@ -56,6 +57,7 @@ class AddNotesCommand implements ChartEditorCommand
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ class CopyItemsCommand implements ChartEditorCommand
|
||||
});
|
||||
|
||||
performVisuals(state);
|
||||
state.clipboardDirty = true;
|
||||
state.clipboardValid = true;
|
||||
}
|
||||
|
||||
function performVisuals(state:ChartEditorState):Void
|
||||
|
||||
@@ -39,6 +39,9 @@ class CutItemsCommand implements ChartEditorCommand
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
state.clipboardDirty = true;
|
||||
state.clipboardValid = true;
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
@@ -53,6 +56,7 @@ class CutItemsCommand implements ChartEditorCommand
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package funkin.ui.debug.charting.commands;
|
||||
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
|
||||
/**
|
||||
* Command that deselects all selected notes and/or events above or past the time given in the chart editor.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class DeselectAllItemsBetweenTimeCommand implements ChartEditorCommand
|
||||
{
|
||||
var time:Float;
|
||||
var above:Bool;
|
||||
|
||||
var notes:Array<SongNoteData>;
|
||||
var events:Array<SongEventData>;
|
||||
|
||||
var shouldDeselectNotes:Bool;
|
||||
var shouldDeselectEvents:Bool;
|
||||
|
||||
public function new(time:Float, above:Bool, shouldDeselectNotes:Bool, shouldDeselectEvents:Bool)
|
||||
{
|
||||
this.time = time;
|
||||
this.above = above;
|
||||
|
||||
this.notes = [];
|
||||
this.events = [];
|
||||
|
||||
this.shouldDeselectNotes = shouldDeselectNotes;
|
||||
this.shouldDeselectEvents = shouldDeselectEvents;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
if (above)
|
||||
{
|
||||
if (shouldDeselectNotes)
|
||||
{
|
||||
for (i in 0...state.currentSongChartNoteData.length)
|
||||
{
|
||||
if (state.currentSongChartNoteData[i].time < time)
|
||||
notes.push(state.currentSongChartNoteData[i]);
|
||||
else
|
||||
// We've reached the end of the notes above this time,
|
||||
// there's no reason to waste our time running this loop to completion
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldDeselectEvents)
|
||||
{
|
||||
for (i in 0...state.currentSongChartEventData.length)
|
||||
{
|
||||
if (state.currentSongChartEventData[i].time < time)
|
||||
events.push(state.currentSongChartEventData[i]);
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else // Deselecting below the time given
|
||||
{
|
||||
if (shouldDeselectNotes)
|
||||
{
|
||||
for (i in 0...state.currentSongChartNoteData.length)
|
||||
{
|
||||
// Backwards for loop (kinda). Neat!
|
||||
if (state.currentSongChartNoteData[state.currentSongChartNoteData.length - i - 1].time > time)
|
||||
notes.push(state.currentSongChartNoteData[state.currentSongChartNoteData.length - i - 1]);
|
||||
else
|
||||
// We've reached the end of the notes below this time,
|
||||
// there's no reason to waste our time running this loop to completion
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldDeselectEvents)
|
||||
{
|
||||
for (i in 0...state.currentSongChartEventData.length)
|
||||
{
|
||||
if (state.currentSongChartEventData[state.currentSongChartEventData.length - i - 1].time > time)
|
||||
events.push(state.currentSongChartEventData[state.currentSongChartEventData.length- i - 1]);
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes);
|
||||
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events);
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
for (note in this.notes)
|
||||
{
|
||||
state.currentNoteSelection.push(note);
|
||||
}
|
||||
|
||||
for (event in this.events)
|
||||
{
|
||||
state.currentEventSelection.push(event);
|
||||
}
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0 || events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var isPlural = (notes.length + events.length) > 1;
|
||||
var notesOnly = (notes.length > 0 && events.length == 0);
|
||||
var eventsOnly = (notes.length == 0 && events.length > 0);
|
||||
|
||||
if (notesOnly)
|
||||
{
|
||||
return 'Deselect ${notes.length} ${isPlural ? 'Notes' : 'Note'}';
|
||||
}
|
||||
else if (eventsOnly)
|
||||
{
|
||||
return 'Deselect ${events.length} ${isPlural ? 'Events' : 'Event'}';
|
||||
}
|
||||
|
||||
return 'Deselect ${notes.length + events.length} Items';
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class DeselectAllItemsCommand implements ChartEditorCommand
|
||||
state.currentEventSelection = [];
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
@@ -35,6 +36,7 @@ class DeselectAllItemsCommand implements ChartEditorCommand
|
||||
state.currentEventSelection = previousEventSelection;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
|
||||
@@ -28,6 +28,7 @@ class DeselectItemsCommand implements ChartEditorCommand
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
@@ -44,6 +45,7 @@ class DeselectItemsCommand implements ChartEditorCommand
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
|
||||
@@ -29,6 +29,7 @@ class InvertSelectedItemsCommand implements ChartEditorCommand
|
||||
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
@@ -37,6 +38,7 @@ class InvertSelectedItemsCommand implements ChartEditorCommand
|
||||
state.currentEventSelection = previousEventSelection;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
|
||||
@@ -15,12 +15,16 @@ class MoveEventsCommand implements ChartEditorCommand
|
||||
var events:Array<SongEventData>;
|
||||
var movedEvents:Array<SongEventData>;
|
||||
var offset:Float;
|
||||
var setPos:Bool;
|
||||
|
||||
public function new(events:Array<SongEventData>, offset:Float)
|
||||
public function new(events:Array<SongEventData>, offset:Float, setPos:Bool = false, offsetInSteps:Bool = false)
|
||||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
// Clone the events to prevent editing from affecting the history.
|
||||
this.events = [for (event in events) event.clone()];
|
||||
if (offsetInSteps) this.offset = Conductor.instance.getStepTimeInMs(offset);
|
||||
else
|
||||
this.offset = offset;
|
||||
this.setPos = setPos;
|
||||
this.movedEvents = [];
|
||||
}
|
||||
|
||||
@@ -32,8 +36,11 @@ class MoveEventsCommand implements ChartEditorCommand
|
||||
|
||||
for (event in events)
|
||||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
// Clone the events to prevent editing from affecting the history.
|
||||
var resultEvent = event.clone();
|
||||
// If setting position, use the offset as the resulting time
|
||||
if (setPos) resultEvent.time = offset.clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
else
|
||||
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
|
||||
movedEvents.push(resultEvent);
|
||||
|
||||
@@ -19,12 +19,15 @@ class MoveItemsCommand implements ChartEditorCommand
|
||||
var offset:Float;
|
||||
var columns:Int;
|
||||
|
||||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, offset:Float, columns:Int)
|
||||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, offset:Float, columns:Int, offsetInSteps:Bool = false,
|
||||
roundStepTimeToNearestStep:Bool = false)
|
||||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
this.notes = notes.clone();
|
||||
this.events = events.clone();
|
||||
this.offset = offset;
|
||||
if (offsetInSteps) this.offset = Conductor.instance.getStepTimeInMs(offset);
|
||||
else
|
||||
this.offset = offset;
|
||||
this.columns = columns;
|
||||
this.movedNotes = [];
|
||||
this.movedEvents = [];
|
||||
|
||||
@@ -15,13 +15,17 @@ class MoveNotesCommand implements ChartEditorCommand
|
||||
var movedNotes:Array<SongNoteData>;
|
||||
var offset:Float;
|
||||
var columns:Int;
|
||||
var setPos:Bool;
|
||||
|
||||
public function new(notes:Array<SongNoteData>, offset:Float, columns:Int)
|
||||
public function new(notes:Array<SongNoteData>, offset:Float, columns:Int, setPos:Bool = false, offsetInSteps:Bool = false)
|
||||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
this.notes = [for (note in notes) note.clone()];
|
||||
this.offset = offset;
|
||||
if (offsetInSteps) this.offset = Conductor.instance.getStepTimeInMs(offset);
|
||||
else
|
||||
this.offset = offset;
|
||||
this.columns = columns;
|
||||
this.setPos = setPos;
|
||||
this.movedNotes = [];
|
||||
}
|
||||
|
||||
@@ -35,6 +39,9 @@ class MoveNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
var resultNote = note.clone();
|
||||
// If setting position, use the offset as the resulting time
|
||||
if (setPos) resultNote.time = offset.clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
else
|
||||
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0,
|
||||
ChartEditorState.STRUMLINE_SIZE * 2 - 1));
|
||||
|
||||
@@ -22,18 +22,27 @@ class PasteItemsCommand implements ChartEditorCommand
|
||||
var removedNotes:Array<SongNoteData> = [];
|
||||
var isRedo:Bool = false;
|
||||
|
||||
var currentClipboard:SongClipboardItems =
|
||||
{
|
||||
valid: false,
|
||||
notes: [],
|
||||
events: []
|
||||
};
|
||||
|
||||
public function new(targetTimestamp:Float)
|
||||
{
|
||||
this.targetTimestamp = targetTimestamp;
|
||||
// Doing this here so that clearing or changing the clipboard doesn't break the redo and string.
|
||||
this.currentClipboard = SongDataUtils.readItemsFromClipboard();
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
|
||||
|
||||
if (currentClipboard.valid != true)
|
||||
{
|
||||
state.error('Failed to Paste', 'Could not parse clipboard contents.');
|
||||
state.clipboardDirty = true;
|
||||
state.clipboardValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,6 +66,7 @@ class PasteItemsCommand implements ChartEditorCommand
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
|
||||
@@ -93,6 +103,7 @@ class PasteItemsCommand implements ChartEditorCommand
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.editButtonsDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
|
||||
@@ -107,8 +118,6 @@ class PasteItemsCommand implements ChartEditorCommand
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
|
||||
|
||||
var len:Int = currentClipboard.notes.length + currentClipboard.events.length;
|
||||
|
||||
if (currentClipboard.notes.length == 0) return 'Paste $len Events';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user