1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-12-08 04:58:48 +00:00

Compare commits

...

170 commits

Author SHA1 Message Date
Hyper_ 7ea7969c94 Fix a syntax error with the RegistryMacro and a partial fix for code completion 2025-12-07 16:21:31 -03:00
AbnormalPoof 758f712eb5 Add global offsets for Pico and Nene's pixel variants 2025-12-07 01:40:24 -07:00
Furo 01029817c0 Added funnyColor backwards compatibility 2025-12-07 01:40:20 -07:00
Furo 8a31e12d10 Change BGScrollingText to a single FlxText
BGScrollingText is now a single FlxText drawn multiple time in a single frame which is more optimized that a FlxSpriteGroup with multiple FlxSprites taking the text's graphic
2025-12-07 01:40:20 -07:00
EliteMasterEric 90ab04caa1 Tweak vertical offset on the measure numbers. 2025-12-07 01:40:20 -07:00
EliteMasterEric ba7f89b9a2 Redo the backing color of the measure ticks. 2025-12-07 01:40:20 -07:00
EliteMasterEric ee36cbbbcf Rewrite measure ticks to not get redrawn EVERY FRAME GODAM 2025-12-07 01:40:20 -07:00
Hyper_ d74b8fb4ca fix: Cursor not properly fading out when exiting CharSelect with backspace 2025-12-07 01:40:20 -07:00
Cameron Taylor 8fa622e147 remove unused pitch stuff in CharSelect 2025-12-07 01:40:20 -07:00
Cameron Taylor a12950d5a7 fix char select inputs for keyb and controllers 2025-12-07 01:40:20 -07:00
EliteMasterEric 71bc847698 Clean up the Lime target config dropdown by removing unused targets like AIR and Flash. 2025-12-07 01:39:25 -07:00
AbnormalPoof a560bf51a9 Re-export Pico's PERFECT rank animation with BTA 2025-12-07 01:39:25 -07:00
AbnormalPoof 866e5aa008 Fix the cursor lerping from the top left in character select 2025-12-07 01:39:22 -07:00
AbnormalPoof 3c213ad45c Add getFramesWithKeyword() 2025-12-07 01:39:22 -07:00
anysad a0b933ce8f Remove unused Chart Editor Context Menu code 2025-12-07 01:39:22 -07:00
MAJigsaw77 671e1435d2 Bump hxvlc to 2.2.5. 2025-11-30 19:20:23 -07:00
MAJigsaw77 369dad3951 Remove the yellow notice. 2025-11-30 19:20:23 -07:00
MAJigsaw77 8ecde809e6 Improve library version checks. 2025-11-30 19:20:23 -07:00
AbnormalPoof c4394f0d12 Lower him a little 2025-11-30 19:19:54 -07:00
AbnormalPoof 63cbaef4c4 Re-export Boyfriend (Car) to texture atlas 2025-11-30 19:19:51 -07:00
AbnormalPoof 4120b20a24 Revert "remove keyboard "scheme" related code, completely unused"
This reverts commit 7c8b7eee578b4c5caabf194d645b1473f233dd46.
2025-11-30 19:19:47 -07:00
EliteMasterEric a40972926d Update HXCPP to support building with C++20 2025-11-30 19:19:47 -07:00
Cameron Taylor 368531f6fa update our custom FunkinAction check() function with newer logic from FlxAction 2025-11-29 04:51:05 -07:00
Cameron Taylor b9d4ce70cc remove the unused _P and _R stuff from the Action enum abstract 2025-11-29 04:51:05 -07:00
Cameron Taylor 6ef3fe4faf more controls.hx cleanup, remove a few unused functions, and add Void return types to make checkstyle happier 2025-11-29 04:51:04 -07:00
Cameron Taylor 8abc9ab306 remove keyboard "scheme" related code, completely unused 2025-11-29 04:51:04 -07:00
Cameron Taylor 8349999833 remove more unused vars in controls.hx 2025-11-29 04:51:04 -07:00
AbnormalPoof cf52bcdf65 Adjust Darnell's idle animation 2025-11-29 04:50:41 -07:00
AbnormalPoof 4e03cf0a5d Re-export THE CHUDDER!!!! 2025-11-29 04:50:39 -07:00
Kolo 8952acae30 better backwards compatibility for FlxAtlasSprite -> FunkinSprite 2025-11-29 00:59:14 -07:00
Cameron Taylor 16c685ac98 use FlxMath.wrap() to make this cursor wrapping cleaner in CharSelectSubState 2025-11-28 18:45:23 -07:00
Cameron Taylor 5472bf691f clean up spamDirection code, use FlxDirectionFlags 2025-11-28 18:45:23 -07:00
MoonDroid 6a71f95cf9 fix: Make lerping consistent across different FPS 2025-11-28 18:45:23 -07:00
AbnormalPoof 48f3e984f4 Fix the cigarette not appearing in the Pico doppelganger cutscene 2025-11-28 18:43:46 -07:00
Cameron Taylor b125a49410 move character select cursor sprite code into it's own class 2025-11-28 18:43:44 -07:00
VirtuGuy de19d65ad6 Fix ghost tapping behavior when unpausing 2025-11-28 18:43:44 -07:00
Cameron Taylor 6334215be4 early return for Chart Editor handleCursor() 2025-11-28 18:43:44 -07:00
EliteMasterEric a3b7891b42 Fix a bug where cycling difficulties wasn't calculated properly when switching variations. 2025-11-28 18:43:44 -07:00
EliteMasterEric 53c62e219b Switching difficulty now forces the selection to be cleared. 2025-11-28 18:43:44 -07:00
EliteMasterEric adb96897dc Add difficulty switches to the undo/redo history. 2025-11-28 18:43:44 -07:00
Hyper_ 9a77539664 Add hardware keyboard detection on Android 2025-11-28 18:43:44 -07:00
MoonDroid 0d91d68300 Update lime ref 2025-11-28 18:43:44 -07:00
EliteMasterEric d49fd76bd1 Implemented Clone Difficulty button in the Difficulty toolbox.
Fixes to Clone Difficulty dialog, and added Move Difficulty dialog.

assets submod
2025-11-28 18:43:44 -07:00
Kolo 329ec524c0 fix starting a playtest opening pause menu 2025-11-27 13:21:18 -07:00
AbnormalPoof 3f89464608 Make Monster's singLEFT-hold offset consistent with singLEFT 2025-11-26 13:51:19 -06:00
Hundrec 118238105b Adjust lyrics for Monster and Winter Horrorland
Fix some errors and formatting
2025-11-26 13:51:07 -06:00
AbnormalPoof 0d32ccc214 Update assets and add getAtlasSettings() for texture atlas characters
Co-authored-by: VirtuGuy <Wyatt2010shaw@gmail.com>
2025-11-26 13:50:29 -06:00
EliteMasterEric ccbae3da7d Implement a checkbox to hide the waveform, which improves FPS. 2025-11-25 23:10:49 -07:00
EliteMasterEric b4058fd7b0 Code cleanup and additional docs for WaveformSprite. 2025-11-25 23:10:47 -07:00
EliteMasterEric c6add57710 Pause the chart editor when focus is lost. 2025-11-25 23:10:47 -07:00
Hundrec c33ec8c0ea Don't play Stress Pico cutscene if charting 2025-11-25 23:10:47 -07:00
PurSnake fe683eba43 Properly load variation scripts in the Chart Editor when playtesting 2025-11-25 23:10:43 -07:00
amyspark-ng ✨ 1534eb2422 fix notelay sound when not in grid 2025-11-25 22:08:05 -07:00
AbnormalPoof c2fd17c551 Re-export Pico's confirm backing card with BTA to bake the filters 2025-11-25 12:01:22 -07:00
Karim Akra ec804394b7 Fix stickers transition looking off-size when resizing the game during a song 2025-11-25 10:23:10 -07:00
VirtuGuy 04ce409f33 Fix anim editor onion skin offset problems 2025-11-25 10:16:30 -07:00
AbnormalPoof 635f6c094e Get the first owned character ID and use that for results 2025-11-25 10:16:30 -07:00
AbnormalPoof 0fbd7a7998 Fix the nametag erroneously showing as "Boyfriend" when entering character select as Pico 2025-11-25 10:01:22 -07:00
MightyTheArmiddilo 9e645db007 up and only up the notifs go 2025-11-23 06:51:25 -05:00
PurSnake 3040692852 Reduce volume on focus lost 2025-11-23 06:36:51 -05:00
TechnikTil 193c443bc4 Add offset var to note in NoteStyle.hx
Co-authored-by: NebulaStellaNova <78160470+NebulaStellaNova@users.noreply.github.com>
2025-11-23 06:12:45 -05:00
AbnormalPoof cb184f89ec 2hot cutscene realignment
Co-authored-by: hucks5 <hucks5@users.noreply.github.com>
2025-11-23 06:01:38 -05:00
AbnormalPoof c2d89243b6 Nene now swings her legs in-game in the 2hot cutscene
Co-authored-by: hucks5 <hucks5@users.noreply.github.com>
2025-11-23 05:49:23 -05:00
Nebula S. Nova 75d085d733 Remove redundant/useless sharpness setting from infoDisplay
This value only sets if you have antiAliasType as ADVANCED, which v-slice does not.
2025-11-23 04:33:21 -06:00
AbnormalPoof e550440407 Re-export Week 5 characters (and dad too I guess) 2025-11-23 05:17:13 -05:00
AbnormalPoof f1eb67bf52 Fix an issue where the default results animation (BF) wouldn't show up if the character ID wasn't associated 2025-11-23 05:11:47 -05:00
AbnormalPoof efafb1acea Re-export week 4 characters (Mom and GF) to texture atlases 2025-11-23 04:58:41 -05:00
AbnormalPoof 56c6c392b3 Fix Nene's heart in Week 7 being affected by rimlight 2025-11-23 03:40:25 -05:00
AbnormalPoof d5afb340a2 Add flush() back to Save 2025-11-23 03:29:46 -05:00
JVN-Pixels f95fbf57b6 Fix typo in Medals.hx 2025-11-23 02:22:34 -06:00
JVN-Pixels 6b5db32522 Fix typo in Conductor.hx 2025-11-23 02:22:34 -06:00
charlesisfeline 300ef8fef1 yeahh im listerning 2025-11-23 02:22:34 -06:00
MightyTheArmiddilo e09c256c80 The true different was the frie- 2025-11-23 02:22:34 -06:00
Excode 7174e776ac Remove useless comment on CharSelectSubState.hx 2025-11-23 02:22:34 -06:00
Excode f10ea83530 Update SpectogramSprite.hx 2025-11-23 02:22:34 -06:00
ComedyLost e19f2155b1 evil typo 2025-11-23 02:22:34 -06:00
AbnormalPoof 86a18067dd Fix Pico Backing Card Confirm
Co-authored-by: VirtuGuy <VirtuGuy@users.noreply.github.com>
2025-11-23 02:57:30 -05:00
AbnormalPoof 15caa7a6f7 literally one digit im krueing myself 2025-11-23 02:43:36 -05:00
Kolo b2897bc86c copy maps instead of assigning 2025-11-23 01:53:22 -05:00
Mikolka cc486ad94b Add song prev fade in delay 2025-11-23 01:39:38 -05:00
AbnormalPoof c5c22fe1a5 More strict null checking for animation 2025-11-22 19:46:20 -05:00
VirtuGuy 302beb2d6e Fix Menu Music Not Playing After Freeplay Exit 2025-11-22 15:39:25 -07:00
ACrazyTown a702040dfb [fix] Replace wrong PRESSED checks with JUST_PRESSED checks 2025-11-22 13:14:55 -06:00
VirtuGuy 1510e5922e Fix character select cutout size 2025-11-22 13:13:57 -06:00
EliteMasterEric 92344d2e30 Don't play the music if the game paused due to losing focus. 2025-11-22 05:19:50 -07:00
EliteMasterEric 5ce3315967 Only do it if "Pause on Unfocus" is enabled. 2025-11-22 05:19:50 -07:00
EliteMasterEric 858199e298 Pause the pause music if the window isn't focused (just like all other audio) 2025-11-22 05:19:50 -07:00
VirtuGuy 0739a40abf Fix mobile compiling issues 2025-11-21 20:58:29 -07:00
AbnormalPoof 215bbdb9da fix for "janky" pico rank anim 2025-11-21 18:53:42 -07:00
AbnormalPoof c9d4d07dc7 bump to latest and include all classes when building 2025-11-21 18:53:42 -07:00
AbnormalPoof 7e1442e969 maybe fix it 2025-11-21 18:53:42 -07:00
Abnormal fcfde61b24 Replace FlxAnimate with flixel-animate
Co-Authored-By: Hyper_ <40342021+NotHyper-474@users.noreply.github.com>
Co-Authored-By: MaybeMaru <97055307+MaybeMaru@users.noreply.github.com>
Co-Authored-By: Trayfellow <48515908+trayfellow@users.noreply.github.com>
2025-11-21 18:53:42 -07:00
Cameron Taylor fe6f5eb821 small typo in pausesubstate UI_DOWN -> controls.UI_DOWN 2025-11-21 19:17:13 -05:00
Cameron Taylor 512a3fc927 some extra pausesubstate tidyings/organization 2025-11-21 18:58:16 -05:00
EliteMasterEric 3bf06a1cc9 Check if Pause menu inputs were JUST pressed to prevent firing immediately when opening. 2025-11-21 18:58:16 -05:00
EliteMasterEric 2c3d61e629 Implement justPressed and justReleased chesk for PAUSE and ACCEPT. 2025-11-21 18:58:16 -05:00
EliteMasterEric 20051109dd Remove unused control actions. 2025-11-21 18:58:16 -05:00
EliteMasterEric 267cc55e40 Don't reset inputs on state switch (fixes justPressed triggering as soon as you enter a state) 2025-11-21 18:58:16 -05:00
EliteMasterEric 766937d5fe Cancel ongoing timers when leaving Freeplay. 2025-11-21 02:06:50 -05:00
Cameron Taylor 18d8fe7248 only show that giant white debug box thing on mobile 2025-11-21 02:05:01 -05:00
EliteMasterEric f7cb738803 WIP on implementing multiple keys per direction 2025-11-21 02:01:35 -05:00
EliteMasterEric 400ef63c5a Attract State shouldn't interrupt the GF ringtone song. 2025-11-20 05:25:52 -07:00
Lasercar 1ff6a1656c Open target song stage with song characters
Also, nullable test characters (but it's buggy)
2025-11-20 05:19:01 -07:00
Eric 20da3f85f1
Merge pull request #1933 from FunkinCrew/kade-github/non-pausestate-pausing
[ENHANCEMENT] Non-descript state pausing for PlayState
2025-11-18 22:28:39 -05:00
Kade d5e14a7101
change shouldPause to shouldSubstatePause 2025-11-18 16:13:11 -08:00
Kade-gtihub ffa8622903 non-descript state pausing 2025-11-18 16:05:40 -08:00
Kade-gtihub 8b559639e8 visible check for notes 2025-11-18 14:03:57 -07:00
Kade-gtihub 628c0e8975 other notes 2025-11-18 14:03:57 -07:00
Kade-gtihub 1734ec1dbf steps/fakes notekind 2025-11-18 14:00:18 -07:00
Karim Akra b10ae8dfa5 Fixed playstate not removing the black bars when loading it 2025-11-18 12:00:11 -07:00
Karim Akra dc634fa35c Fixed some substates looking a bit broken after resizing 2025-11-18 12:00:11 -07:00
Cameron Taylor 06246aa3d7 assets submod fix -> master 2025-11-17 17:05:45 -05:00
Karim Akra 7c1eb4f88c block vertically wide resolutions 2025-11-15 10:08:45 -08:00
Karim Akra 38634ef5ca Prevent the game bounds from breaking when resizing the window 2025-11-15 10:08:45 -08:00
EliteMasterEric 5d76d23d33 Fix a script error while hot reloading in the Game Over screen. 2025-11-14 16:40:19 -08:00
EliteMasterEric da422c913f Update chart editor layout to fix a bug 2025-11-14 16:36:55 -08:00
Kade-gtihub ba52b932ee controller crash fix 2025-11-14 16:34:05 -08:00
Eric 1b90b7bebf Include MacOS Actuator crash in HaxeUI update 2025-11-14 13:38:48 -08:00
Kade-gtihub 23ca9c14c1 Fix crash on non-existant tooltip 2025-11-14 13:38:48 -08:00
AbnormalPoof ab300be709 Fix a compilation error with UNLOCK_EVERYTHING disabled 2025-11-14 12:29:06 -06:00
Kade-gtihub e6aaca19f6 stepmania importing
fix stops
2025-11-07 21:59:07 -08:00
Cameron Taylor 0fe226132d quick fix for incorrect flush() usage 2025-11-04 15:24:28 -05:00
Cameron Taylor 452e3a58d8 modify chart editor to save some preferences more immediately 2025-11-04 14:59:24 -05:00
Cameron Taylor e8f60a23ef helpers for data binding in progress in Save.hx
move buncho save data stuff to our new SaveProperty class

SaveMacro to generate save property stuff
2025-11-04 14:59:24 -05:00
Cameron Taylor 958a6f49ce some data binding funzies i think 2025-11-04 14:59:24 -05:00
Cameron Taylor 2005628358 move these vars up with the rest of em 2025-11-04 14:59:24 -05:00
Cameron Taylor 439901b42e buncho lil null tidying 2025-11-04 14:59:24 -05:00
Cameron Taylor 7bbe356009 move a buncho stuff from Save.hx to SaveSystem.hx
to rebase
2025-11-04 14:59:24 -05:00
Cameron Taylor 9d710b0490 some more Save.hx cleanup worky 2025-11-04 14:59:24 -05:00
Cameron Taylor d140caf4ba move Save.hx consts to Constants.hx file 2025-11-04 14:59:24 -05:00
MAJigsaw77 15b691ad7b Make the haxelib warnings be properly handled on Git Dev libraries.
Adjust the haxelib warning message aligment.

Acually, it aint neccesary.
2025-11-03 18:03:50 -05:00
Abnormal 02b53e4900
Fix a security vulnerability in ReflectUtil 2025-11-03 13:03:54 -08:00
AbnormalPoof 5b5d76aa59 Make CLASSIC the default easing type for Focus Camera 2025-10-28 22:52:40 -04:00
AbnormalPoof ae861214cb Revert "Fix type mismatch in smooth lerp functions"
This reverts commit 4f1bbd3f9b.
2025-10-27 18:59:13 -04:00
AbnormalPoof 4d5558f3d8 Add the new characters to the Results Screen song name font... thingyy.... 2025-10-27 17:54:33 -05:00
MoonDroid 0af6f61fb9 Update flixel ref.
* fix: Fix glitchy bitmapfont text for Results, etc. when using missing letters.
2025-10-27 17:54:33 -05:00
Eric 72a2e9c895
Merge pull request #1914 from FunkinCrew/fix/smoothlerp-mismatch
Fix type mismatch in smooth lerp functions
2025-10-27 15:43:35 -04:00
MoonDroid 4f1bbd3f9b Fix type mismatch in smooth lerp functions 2025-10-24 16:58:51 +07:00
Hundrec b99089dd88 Assets for ansi 2025-10-23 17:29:55 -04:00
MoonDroid 380eb5cbaf Add remaining ansi flair woo to every class that needs it 2025-10-23 17:29:55 -04:00
MoonDroid a398eb17c4 Add missing Ansi flair to macros 2025-10-23 17:29:55 -04:00
MoonDroid 9765f5efe5 Use ansi for polymod logs 2025-10-23 17:29:55 -04:00
MoonDroid d6e1f0c90d Remove dupe ERROR tag from getGitCommit() 2025-10-23 17:29:55 -04:00
MoonDroid f56fa1955e Make AnsiUtil.apply() remove duplicate RESET codes from provided string 2025-10-23 17:29:55 -04:00
MAJigsaw77 277abfab2b Fix html5 compile aswell as some other ANSI not showing up. 2025-10-23 17:29:55 -04:00
MoonDroid 5c1b0ae184 Make AnsiUtil be more like StringTools instead. 2025-10-23 17:29:55 -04:00
MoonDroid ee952c0789 Ansify Newgrounds, Discord, and HXVLC traces 2025-10-23 17:29:55 -04:00
MoonDroid eb1d54e750 Use AnsiUtil for AnsiTrace, and highlight traces with headers 2025-10-23 17:29:55 -04:00
MoonDroid 0d8fd214a1 Add BG_ORANGE 2025-10-23 17:29:55 -04:00
MoonDroid 07b0af53f1 Add tools.AnsiUtil fir specific macros and project.hxp
Co-Authored-By: Mihai Alexandru <77043862+MAJigsaw77@users.noreply.github.com>
2025-10-23 17:29:55 -04:00
Lasercar 7ffa3d5bf2 Fix UI file checks 2025-10-23 10:57:26 -05:00
Hundrec 615d52aff2 Remove BF instrumentals for Pico 2025-10-22 17:54:11 -05:00
ImVeryBad 4961d4ee99 Allow level props to have a flipX field in JSON 2025-10-22 17:06:57 -05:00
ImVeryBad 33a020a34b StageProp animation offsets finally working ! 2025-10-22 16:57:34 -05:00
ComedyLost 6ba44d6cb0 one line banger 2025-10-22 16:51:39 -05:00
Nebula S. Nova 277d16895b Fix Healthicon bug when restarting 2025-10-22 16:26:43 -05:00
Abnormal dbd19159b6
Merge pull request #1905 from FunkinCrew/merging-because-til-asked-me-to 2025-10-22 16:12:47 -05:00
Hundrec 9f5c88ffa1
Merging main into develop because Til asked me to 2025-10-22 17:01:52 -04:00
Hundrec a528a11b8c Bump assets 2025-10-22 13:14:35 -07:00
Furo a52f4c5e95 Changes that Kade Requested 2025-10-22 12:50:11 -07:00
Furo 0be42bf047 Osu!Mania Importing 2025-10-22 12:50:11 -07:00
AbnormalPoof ba31f3611a Fix macos compilation due to a mobile-specific change 2025-10-21 06:14:49 +07:00
MoonDroid 3119a0cb6e fix: Make it so external music gets paused when tabbing/opening into the game for mobile 2025-10-20 16:53:55 -04:00
Lasercar 5326c40347 Stage the backup 2025-10-19 23:39:49 -05:00
Hyper_ bbcb19d476 next time you create a Sprite that covers the whole screen pls don't make it receive mouse events 2025-10-19 23:31:44 -05:00
TechnikTil e6d6972ab0 Fix htmlText subtitles 2025-10-20 00:14:21 -04:00
CrusherNotDrip 4f8da59577 Make the backups folder more organized. 2025-10-19 23:03:14 -05:00
TechnikTil 6686812504 Host README.md images on the repository 2025-10-19 19:49:52 -04:00
AbnormalPoof 80993df87f Fix assets being cleared when running lime run 2025-10-19 19:34:07 -04:00
AbnormalPoof 30361954f3 Fix the assets folder... not deleting assets at all. 2025-10-19 01:08:52 -06:00
174 changed files with 7705 additions and 5501 deletions

112
.vscode/settings.json vendored
View file

@ -1,31 +1,22 @@
{
"[haxe]": {
// Automatically keep Haxe files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never"
},
"editor.codeActionsOnSave": { "source.organizeImports": "never" },
"editor.defaultFormatter": "nadako.vshaxe",
"editor.tabSize": 2
},
"[json]": {
// Automatically keep JSON files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
// Automatically keep JSONC files formatted.
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.tabWidth": 2,
// XML formatting style configuration
// XML Formatting
"xml.format.enabled": true,
"xml.format.legacy": false,
"xml.format.emptyElements": "collapse",
@ -33,7 +24,7 @@
"xml.format.enforceQuoteStyle": "preferred",
"xml.format.preserveAttributeLineBreaks": false,
"xml.format.preservedNewlines": 0,
"xml.format.splitAttributes": false,
"xml.format.splitAttributes": "preserve",
"xml.format.joinCDATALines": true,
"xml.format.preserveEmptyContent": false,
"xml.format.joinCommentLines": false,
@ -56,26 +47,19 @@
"xml.format.maxLineWidth": 0,
"xml.format.grammarAwareFormatting": true,
// Generic file formatting style configuration
// General formatting
"files.insertFinalNewline": true,
"files.trimFinalNewlines": false,
"files.trimTrailingWhitespace": true,
// Automatically detect indentation.
"editor.detectIndentation": true,
"editor.insertSpaces": true,
"editor.tabSize": 2,
// Automatically enforce Linux style line endings.
"prettier.tabWidth": 2,
"files.eol": "\n",
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": true,
"haxe.enableServerView": true,
"haxe.displayServer": {
"arguments": ["-v"]
},
// Fix file associations for HScript.
"haxe.displayServer": { "arguments": ["-v"] },
"files.associations": {
"*.hxp": "haxe",
"*.hscript": "haxe",
@ -84,14 +68,27 @@
"*.hxc": "haxe"
},
"projectManager.git.baseFolders": ["./"],
"haxecheckstyle.sourceFolders": ["src", "source"],
"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" },
{ "name": "linux", "enabled": true, "label": "Linux" },
{ "name": "html5", "enabled": true, "label": "HTML5" },
{ "name": "android", "enabled": true, "label": "Android" },
{ "name": "ios", "enabled": true, "label": "iOS" },
// Disabled targets
{ "name": "hl", "enabled": false, "label": "HashLink" },
{ "name": "air", "enabled": false },
{ "name": "electron", "enabled": false },
{ "name": "flash", "enabled": false },
{ "name": "neko", "enabled": false },
{ "name": "tvos", "enabled": false }
],
"lime.defaultTargetConfiguration": "Windows / Debug",
"lime.targetConfigurations": [
{
"label": "Windows / Debug (Discord)",
@ -103,21 +100,11 @@
"target": "windows",
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
"target": "hl",
"args": ["-debug", "-DANIMATE"]
},
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
"target": "hl",
"args": ["-debug", "-DFREEPLAY"]
},
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
@ -132,26 +119,6 @@
"target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
"target": "hl",
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
},
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Conversation Test)",
"target": "hl",
"args": ["-debug", "-DDIALOGUE"]
},
{
"label": "Windows / Debug (Results Screen Test)",
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Stage Editor)",
"target": "windows",
@ -172,36 +139,16 @@
"target": "windows",
"args": ["-debug", "-DHXVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
"target": "hl",
"args": ["-debug", "-DANIMDEBUG"]
},
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Latency Test)",
"target": "hl",
"args": ["-debug", "-DLATENCY"]
},
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Release (GitHub Actions)",
"target": "windows",
"args": ["-release", "-DGITHUB_BUILD"]
},
{
"label": "HashLink / Debug (Waveform Test)",
"target": "hl",
"args": ["-debug", "-DWAVEFORM"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
@ -209,10 +156,7 @@
}
],
"lime.buildTypes": [
{
"label": "Debug",
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{ "label": "Debug", "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"] },
{
"label": "Debug (Unlock Everything)",
"args": ["-debug", "-DUNLOCK_EVERYTHING"]
@ -225,10 +169,7 @@
"label": "Debug (Straight to Chart Editor)",
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Release",
"args": ["-release"]
},
{ "label": "Release", "args": ["-release"] },
{
"label": "Release (GitHub Actions)",
"args": ["-release", "-DGITHUB_BUILD"]
@ -244,9 +185,6 @@
],
"vscord.app.privacyMode.enable": true,
"json.schemas": [
{
"fileMatch": ["/hmm.json"],
"url": "./.vscode/schema/hmm.json"
}
{ "fileMatch": ["/hmm.json"], "url": "./.vscode/schema/hmm.json" }
]
}

View file

@ -1,4 +1,4 @@
<div align='center'><img src="https://fridaynightfunkin.wiki.gg/images/FNF_logo.png" width="800">
<div align='center'><img src="docs/readme_images/FNF_logo.png" width="800">
<h2>Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for <a href="https://ldjam.com/events/ludum-dare/47">Ludum Dare 47.</a></h2>
@ -14,8 +14,8 @@ This game was made with love to Newgrounds and its community. Extra love to Tom
<div align='center'>
<table>
<tr>
<td><img src="https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif" alt="Title Screen" width="350"/></td>
<td><img src="https://fridaynightfunkin.wiki.gg/images/9/99/Menu.png" alt="Main Menu" width="350"/></td>
<td><img src="docs/readme_images/Title_Card.gif" alt="Title Screen" width="350"/></td>
<td><img src="docs/readme_images/Menu.png" alt="Main Menu" width="350"/></td>
</tr>
</table>
</div>

2
assets

@ -1 +1 @@
Subproject commit f81cc16c8f99655f76b7e27ce221819798c9c94e
Subproject commit 1a961f111381eb3bfc452166c4e4b5a18b409781

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

BIN
docs/readme_images/Menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

View file

@ -0,0 +1,5 @@
Images stored in this folder come from these links:
FNF_logo.png - https://fridaynightfunkin.wiki.gg/images/FNF_logo.png
Menu.png - https://fridaynightfunkin.wiki.gg/images/Menu.png
Title_Card.gif - https://fridaynightfunkin.wiki.gg/images/d/d7/Title_Card.gif

View file

@ -45,7 +45,7 @@
"name": "flixel",
"type": "git",
"dir": null,
"ref": "125c5b5a05a90918e0bd4f97d596ed1321bde5c2",
"ref": "757f2d0ab241ad0fb99fbcdac598fee31a905ea5",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@ -56,11 +56,11 @@
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
"name": "flxanimate",
"name": "flixel-animate",
"type": "git",
"dir": null,
"ref": "49214278b9124823582cdcecd94f4a1de9a4b36b",
"url": "https://github.com/FunkinCrew/flxanimate"
"ref": "220463c8089444d6c9957b68919fe88e6cba495f",
"url": "https://github.com/MaybeMaru/flixel-animate"
},
{
"name": "format",
@ -90,8 +90,8 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "578d61bc275cf71f473647eeb5aab002e45338be",
"url": "https://github.com/haxeui/haxeui-core"
"ref": "088b153b35985f8e4419ef710057ebd5b3749513",
"url": "https://github.com/FunkinCrew/haxeui-core"
},
{
"name": "haxeui-flixel",
@ -111,7 +111,7 @@
"name": "hxcpp",
"type": "git",
"dir": null,
"ref": "6546fa5c3ad1bac065f144745122ab5a6d4195ff",
"ref": "5932340d095a7eea8635fe4d1355f1c0efd0b3c2",
"url": "https://github.com/FunkinCrew/hxcpp"
},
{
@ -146,7 +146,7 @@
{
"name": "hxvlc",
"type": "haxelib",
"version": "2.2.4"
"version": "2.2.5"
},
{
"name": "json2object",
@ -173,7 +173,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "27092822abf7b8c2ec2905053cf5435be4de838e",
"ref": "d1596fe7daa9c479384ad2705e7813dbccf28839",
"url": "https://github.com/FunkinCrew/lime"
},
{

View file

@ -11,6 +11,7 @@ import haxe.ds.Map;
import haxe.xml.Printer;
using StringTools;
using tools.AnsiUtil;
/**
* This HXP performs the functions of a Lime `project.xml` file,
@ -113,7 +114,11 @@ class Project extends HXProject
*/
static final ANDROID_TARGET_SDK_VERSION:Int = 35;
static final ANDROID_EXTENSIONS:Array<String> = ["funkin.extensions.CallbackUtil", "funkin.extensions.FNFCExtension"];
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.
@ -293,6 +298,13 @@ class Project extends HXProject
*/
static final FEATURE_DISCORD_RPC:FeatureFlag = "FEATURE_DISCORD_RPC";
/**
* `-DFEATURE_LOST_FOCUS_VOLUME`
* If this flag is enabled, the game will reduce application volume, when lost focus.
* Allowed only on Desktop targets.
*/
static final FEATURE_LOST_FOCUS_VOLUME:FeatureFlag = "FEATURE_LOST_FOCUS_VOLUME";
/**
* `-DFEATURE_FILE_DROP`
* If this flag is enabled, the game will support dragging and dropping files onto it for various features.
@ -857,6 +869,8 @@ class Project extends HXProject
// 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_LOST_FOCUS_VOLUME.apply(this, isDesktop());
// Newgrounds features
FEATURE_NEWGROUNDS.apply(this, !isMobile());
FEATURE_NEWGROUNDS_DEBUG.apply(this, false);
@ -1088,6 +1102,9 @@ class Project extends HXProject
addHaxeMacro("include('haxe.ui.containers.properties')");
}
// Ensure all flixel-animate classes are available at runtime.
addHaxeMacro("include('animate')");
// 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' ])");
@ -1299,7 +1316,7 @@ class Project extends HXProject
}
addHaxelib('polymod'); // Modding framework
addHaxelib('flxanimate'); // Texture atlas rendering
addHaxelib('flixel-animate'); // Texture atlas rendering
addHaxelib('json2object'); // JSON parsing
addHaxelib('jsonpath'); // JSON parsing
@ -1410,9 +1427,9 @@ class Project extends HXProject
if (!FEATURE_ANIMATION_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/animation-editor', "default");
if (!FEATURE_ANIMATION_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/chart-editor', "default");
if (!FEATURE_CHART_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/chart-editor', "default");
if (!FEATURE_ANIMATION_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/stage-editor', "default");
if (!FEATURE_STAGE_EDITOR.isEnabled(this)) removeAssetPath('assets/preload/data/ui/stage-editor', "default");
// Font assets
var shouldEmbedFonts = true;
@ -1470,8 +1487,8 @@ class Project extends HXProject
function clearAssets():Void
{
// We don't want the haxe compilation server deleting our assets, do we?
if (!isDisplay()) return;
// Don't run on non-build commands.
if (!isBuild()) return;
var exportPath:Null<String> = app.path ?? "";
@ -1739,6 +1756,11 @@ class Project extends HXProject
return this.command == "clean";
}
public function isBuild():Bool
{
return this.command == "test" || this.command == "build";
}
public function isDebug():Bool
{
return this.debug;
@ -1906,7 +1928,7 @@ class Project extends HXProject
}
else
{
// info(' Found asset path "${path}".');
// info(' Found asset path "${path}".');
}
for (file in sys.FileSystem.readDirectory(path))
@ -2013,7 +2035,7 @@ class Project extends HXProject
if (process.exitCode() != 0)
{
var message = process.stderr.readAll().toString();
error('[ERROR] Could not determine current git commit; is this a proper Git repository?');
error('Could not determine current git commit; is this a proper Git repository?');
}
var commitHash:String = process.stdout.readLine();
@ -2025,9 +2047,9 @@ class Project extends HXProject
}
@SuppressWarnings('checkstyle:Dynamic')
static function checkLibraries():Void
function checkLibraries():Void
{
var outdatedLibraries:Map<String, Array<String>> = new Map<String, Array<String>>();
var diffrentLibraries:Map<String, Array<String>> = new Map<String, Array<String>>();
var hmmData:Dynamic = haxe.Json.parse(sys.io.File.getContent(#if ios '../../../../../' + #end 'hmm.json'));
@ -2041,57 +2063,80 @@ class Project extends HXProject
var libraryCurrentVersion:String = readLibraryCurrentVersion(libraryName);
switch (library.type)
{
case 'haxelib':
if (libraryDev != "")
{
outdatedLibraries.set(libraryName, [libraryDev, library.version]);
}
else if (library.version != libraryCurrentVersion)
{
outdatedLibraries.set(libraryName, [libraryCurrentVersion, library.version]);
}
case 'git':
if (libraryDev != "")
{
outdatedLibraries.set(libraryName, [libraryDev, library.ref]);
}
else
{
var commitHash:String = readLibraryGitCommitHash(libraryName);
var libraryCurrentCommitHash:String = readLibraryGitCommitHash(libraryName);
if (commitHash != library.ref && commitHash != "") outdatedLibraries.set(libraryName, [commitHash, library.ref]);
}
if (libraryDev != "" && !isLibraryLocalGitDev(libraryName))
{
switch (library.type)
{
case 'haxelib':
diffrentLibraries.set(libraryName, [libraryDev, libraryCurrentCommitHash, library.version, 'haxelib']);
case 'git':
if (libraryCurrentCommitHash != library.ref)
{
diffrentLibraries.set(libraryName, [libraryDev, libraryCurrentCommitHash, library.ref, 'git']);
}
}
}
else
{
switch (library.type)
{
case 'haxelib':
if (library.version != libraryCurrentVersion)
{
diffrentLibraries.set(libraryName, ['', libraryCurrentVersion, library.version, 'haxelib']);
}
case 'git':
if (libraryCurrentCommitHash != library.ref)
{
diffrentLibraries.set(libraryName, ['', libraryCurrentCommitHash, library.ref, 'git']);
}
}
}
}
if (Lambda.count(outdatedLibraries) > 0)
if (Lambda.count(diffrentLibraries) > 0)
{
Sys.println("\x1b[1;33m\nWARNING: The following haxelibs diverge from the versions set in hmm.json."
+ "\n\nThey may be outdated, so it is recommended to abort compilation and run `hmm reinstall [library]` to update each library."
+ "\n\nYou may ignore this warning if your libraries are newer than the versions in hmm.json, or if you know what you're doing.\n\x1b[0m");
warn("Some libraries differ from the versions defined in hmm.json.".bold().yellow());
warn("To ensure consistency, it's recommended to stop the build and run `hmm reinstall [library]`.".bold().yellow());
warn("If you're intentionally using development builds or newer versions, you can ignore this warning.".bold().yellow());
for (libraryName in outdatedLibraries.keys())
Sys.println('');
for (libraryName in diffrentLibraries.keys())
{
var versions:Null<Array<String>> = outdatedLibraries.get(libraryName);
if (versions == null) continue;
var infos:Null<Array<String>> = diffrentLibraries.get(libraryName);
var outdatedVersion:String = versions[0];
var expectedVersion:String = versions[1];
if (infos == null) continue;
var isDev:Bool = outdatedVersion.contains("/");
var devPath:String = infos[0];
var currentVersion:String = infos[1];
var expectedVersion:String = infos[2];
var libType:String = infos[3];
Sys.println("- " + libraryName.replace(",", ".") + (isDev ? "\x1b[1;34m (Development Build)\x1b[0m" : ""));
Sys.println(" \x1b[38;5;203mCurrent version: " + outdatedVersion + "\x1b[0m");
Sys.println(" \x1b[38;5;82mExpected version: " + expectedVersion + "\x1b[0m\n");
if (haxe.io.Path.isAbsolute(devPath))
{
Sys.println("- " + libraryName.replace(",", ".") + " (Development Build)".blue().bold());
Sys.println((" Path: " + haxe.io.Path.removeTrailingSlashes(devPath)).blue());
}
else
{
Sys.println("- " + libraryName.replace(",", "."));
}
Sys.println((" Current: " + currentVersion).red());
Sys.println((" Expected: " + expectedVersion).green());
}
Sys.println('');
}
}
static function readLibraryCurrentVersion(libraryName:String):String
{
var path = haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']);
var path = getLibraryCurrentFile(libraryName);
if (sys.FileSystem.exists(path)) return sys.io.File.getContent(path);
@ -2100,7 +2145,7 @@ class Project extends HXProject
static function readLibraryDev(libraryName:String):String
{
var path = haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.dev']);
var path = getLibraryDevFile(libraryName);
if (sys.FileSystem.exists(path)) return sys.io.File.getContent(path);
@ -2109,11 +2154,48 @@ class Project extends HXProject
static function readLibraryGitCommitHash(libraryName:String):String
{
var gitProccess = new sys.io.Process("git", ["-C", ".haxelib/" + libraryName + "/git", "rev-parse", "HEAD"]);
var commit:String = '';
var gitProccess = new sys.io.Process("git", ["-C", getLibraryGitPath(libraryName), "rev-parse", "HEAD"]);
gitProccess.exitCode(true);
commit = gitProccess.stdout.readAll().toString().trim();
return gitProccess.stdout.readAll().toString().trim();
if (commit.length <= 0)
{
gitProccess = new sys.io.Process("git", ["-C", readLibraryDev(libraryName), "rev-parse", "HEAD"]);
gitProccess.exitCode(true);
commit = gitProccess.stdout.readAll().toString().trim();
}
return commit;
}
static function getLibraryCurrentFile(libraryName:String):String
{
return haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']);
}
static function getLibraryDevFile(libraryName:String):String
{
return haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.dev']);
}
static function getLibraryGitPath(libraryName:String):String
{
return haxe.io.Path.join([haxe.io.Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, 'git']);
}
static function isLibraryLocalGitDev(libraryName:String):Bool
{
final gitPath:String = getLibraryGitPath(libraryName);
final devFile:String = getLibraryDevFile(libraryName);
if (FileSystem.exists(gitPath) && FileSystem.exists(devFile))
{
return Path.normalize(readLibraryDev(libraryName)).startsWith(gitPath);
}
return false;
}
static function getLibraryPath(libraryName:String):String
@ -2194,7 +2276,7 @@ class Project extends HXProject
*/
public function error(message:String):Void
{
Sys.stderr().write(Bytes.ofString('[ERROR] ${message}'));
Sys.stderr().write(Bytes.ofString(' ERROR '.bold().bg_red() + " " + message));
Sys.exit(1);
}
@ -2205,7 +2287,18 @@ class Project extends HXProject
{
if (!(isDisplay() || isClean()))
{
Sys.println('[INFO] ${message}');
Sys.println(' INFO '.bold().bg_blue() + " " + message);
}
}
/**
* Display a warning message. This should not interfere with the build process.
*/
public function warn(message:String):Void
{
if (!(isDisplay() || isClean()))
{
Sys.println(' WARNING '.bold().bg_yellow() + " " + message);
}
}
@ -2217,7 +2310,7 @@ class Project extends HXProject
{
if (!FileSystem.exists(file))
{
info('Environment file does not exist: ${file}');
warn('Environment file does not exist: ${file}');
return null;
}
@ -2225,7 +2318,7 @@ class Project extends HXProject
if (envFile == null)
{
info('Failed to parse environment file: ${file}');
warn('Failed to parse environment file: ${file}');
return null;
}

View file

@ -21,6 +21,8 @@ import openfl.media.Video;
import openfl.net.NetStream;
import funkin.util.WindowUtil;
using funkin.util.AnsiUtil;
/**
* The main class which initializes HaxeFlixel and starts the game in its initial state.
*/
@ -133,11 +135,11 @@ class Main extends Sprite
Handle.initAsync(function(success:Bool):Void {
if (success)
{
trace('[HXVLC] LibVLC instance initialized!');
trace(' HXVLC '.bold().bg_white() + ' LibVLC instance initialized!');
}
else
{
trace('[HXVLC] LibVLC instance failed to initialize!');
trace(' HXVLC '.bold().bg_white() + ' LibVLC instance failed to initialize!');
}
});
#end

View file

@ -4,6 +4,7 @@ import sys.FileSystem;
import sys.io.File;
using StringTools;
using tools.AnsiUtil;
/**
* A script which executes after the game is built.
@ -29,34 +30,32 @@ class Postbuild
sys.FileSystem.deleteFile(BUILD_TIME_FILE);
Sys.println('[INFO] Build took: ${format(end - start)}');
Sys.println(' INFO '.bold().bg_blue() + ' Build took: ${format(end - start)}');
}
}
static function format(time:Float, decimals:Int = 1):String
{
var units = [
{name: "day", secs: 86400},
{name: "hour", secs: 3600},
{name: "minute", secs: 60},
{name: "second", secs: 1}
];
var parts:Array<String> = [];
var remaining:Float = time;
var factor = Math.pow(10, decimals); // compute once because the old code was computing it twice.
for (u in units)
{
var value:Float = (u.name == "second") ? Math.round(remaining * factor) / factor : Math.floor(remaining / u.secs);
if (u.name != "second")
remaining %= u.secs;
if (value > 0 || (u.name == "second" && parts.length == 0))
parts.push('${value} ${u.name}${value == 1 ? "" : "s"}');
}
return parts.join(" ");
var units = [
{name: "day", secs: 86400},
{name: "hour", secs: 3600},
{name: "minute", secs: 60},
{name: "second", secs: 1}
];
var parts:Array<String> = [];
var remaining:Float = time;
var factor = Math.pow(10, decimals); // compute once because the old code was computing it twice.
for (u in units)
{
var value:Float = (u.name == "second") ? Math.round(remaining * factor) / factor : Math.floor(remaining / u.secs);
if (u.name != "second") remaining %= u.secs;
if (value > 0 || (u.name == "second" && parts.length == 0)) parts.push('${value} ${u.name}${value == 1 ? "" : "s"}');
}
return parts.join(" ");
}
}

View file

@ -1,13 +1,14 @@
package funkin;
import openfl.utils.Future;
import funkin.util.macro.ConsoleMacro;
/**
* A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions.
* Later we'll add Funkin-specific caching to this.
*/
@:nullSafety
class Assets
class Assets implements ConsoleClass
{
/**
* The assets cache.

View file

@ -526,7 +526,7 @@ class Conductor
/**
* Can be called in-between frames, usually for input related things
* that can potentially get processed on exact milliseconds/timestmaps.
* that can potentially get processed on exact milliseconds/timestamps.
* If you need song position, use `Conductor.instance.songPosition` instead
* for use in update() related functions.
* @param soundToCheck Which FlxSound object to check, defaults to FlxG.sound.music if no input

View file

@ -111,6 +111,16 @@ class FunkinMemory
///// TEXTURES /////
/**
* Determine whether the texture with the given key is cached.
* @param key The key of the texture to check.
* @return Whether the texture is cached.
*/
public static function isTextureCached(key:String):Bool
{
return FlxG.bitmap.get(key) != null;
}
/**
* Ensures a texture with the given key is cached.
* @param key The key of the texture to cache.
@ -160,7 +170,7 @@ class FunkinMemory
graphic.persist = true;
permanentCachedTextures.set(key, graphic);
forceRender(graphic);
currentCachedTextures = permanentCachedTextures;
currentCachedTextures = permanentCachedTextures.copy();
}
/**
@ -201,7 +211,7 @@ class FunkinMemory
*/
public inline static function preparePurgeTextureCache():Void
{
previousCachedTextures = currentCachedTextures;
previousCachedTextures = currentCachedTextures.copy();
for (graphicKey in previousCachedTextures.keys())
{
@ -211,7 +221,7 @@ class FunkinMemory
}
}
currentCachedTextures = permanentCachedTextures;
currentCachedTextures = permanentCachedTextures.copy();
}
/**
@ -233,6 +243,7 @@ class FunkinMemory
if (graphic != null)
{
FlxG.bitmap.remove(graphic);
graphic.persist = false;
graphic.destroy();
previousCachedTextures.remove(graphicKey);
Assets.cache.clear(graphicKey);
@ -250,7 +261,7 @@ class FunkinMemory
{
var obj:Null<FlxGraphic> = FlxG.bitmap.get(key);
if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts"))
if (obj == null || (obj.persist && permanentCachedTextures.exists(key)) || key.contains("fonts"))
{
continue;
}
@ -262,6 +273,7 @@ class FunkinMemory
if (key.contains(purgeEntry))
{
FlxG.bitmap.removeKey(key);
obj.persist = false;
obj.destroy();
}
}
@ -346,7 +358,7 @@ class FunkinMemory
public static function preparePurgeSoundCache():Void
{
previousCachedSounds = currentCachedSounds;
previousCachedSounds = currentCachedSounds.copy();
for (key in previousCachedSounds.keys())
{
@ -356,7 +368,7 @@ class FunkinMemory
}
}
currentCachedSounds = permanentCachedSounds;
currentCachedSounds = permanentCachedSounds.copy();
}
/**

View file

@ -153,6 +153,11 @@ class InitState extends FlxState
// Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30;
// Persist controls inputs between states.
// Without this, the game would release your keybinds when you switch states,
// and then act like you released and re-pressed them the frame after.
FlxG.inputs.resetOnStateSwitch = false;
// Makes Flixel use frame times instead of locked movements per frame for things like tweens
FlxG.fixedTimestep = false;
@ -212,6 +217,10 @@ class InitState extends FlxState
});
#end
#if FEATURE_LOST_FOCUS_VOLUME
FlxG.signals.focusLost.add(onLostFocus);
FlxG.signals.focusGained.add(onGainFocus);
#end
//
// ANDROID SETUP
//
@ -287,6 +296,23 @@ class InitState extends FlxState
#end
}
#if FEATURE_LOST_FOCUS_VOLUME
@:noCompletion var _lastFocusVolume:Null<Float>;
function onLostFocus()
{
if (FlxG.sound.muted || FlxG.sound.volume == 0 || FlxG.autoPause) return;
_lastFocusVolume = FlxG.sound.volume;
FlxG.sound.volume *= Constants.LOST_FOCUS_VOLUME_MULTIPLIER;
}
function onGainFocus()
{
if (FlxG.sound.muted || FlxG.autoPause) return;
if (_lastFocusVolume != null) FlxG.sound.volume = _lastFocusVolume;
}
#end
/**
* Start the game.
*
@ -558,19 +584,6 @@ class InitState extends FlxState
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
TrackerUtil.initTrackers();
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
});
// Adds a red button to the debugger.
// This pauses the game AND the music! This ensures the Conductor stops.
@ -613,6 +626,22 @@ class InitState extends FlxState
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
#if mobile
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
});
#end // mobile big butotn crap
#end
}

View file

@ -2,13 +2,14 @@ package funkin;
import flixel.graphics.frames.FlxAtlasFrames;
import openfl.utils.AssetType;
import funkin.util.macro.ConsoleMacro;
import haxe.io.Path;
/**
* A core class which handles determining asset paths.
*/
@:nullSafety
class Paths
class Paths implements ConsoleClass
{
static var currentLevel:Null<String> = null;

View file

@ -44,7 +44,7 @@ class Preferences
#else
var save:Save = Save.instance;
save.options.framerate = value;
save.flush();
Save.system.flush();
FlxG.updateFramerate = value;
FlxG.drawFramerate = value;
return value;
@ -74,7 +74,7 @@ class Preferences
var save:Save = Save.instance;
save.options.naughtyness = value;
save.flush();
Save.system.flush();
return value;
}
@ -93,7 +93,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.downscroll = value;
save.flush();
Save.system.flush();
return value;
}
@ -112,7 +112,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.flashingLights = value;
save.flush();
Save.system.flush();
return value;
}
@ -131,7 +131,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.zoomCamera = value;
save.flush();
Save.system.flush();
return value;
}
@ -157,7 +157,7 @@ class Preferences
var save = Save.instance;
save.options.debugDisplay = value;
save.flush();
Save.system.flush();
return value;
}
@ -178,7 +178,7 @@ class Preferences
var save:Save = Save.instance;
save.options.debugDisplayBGOpacity = value;
save.flush();
Save.system.flush();
return value;
}
@ -219,7 +219,7 @@ class Preferences
var save:Save = Save.instance;
save.options.hapticsMode = string;
save.flush();
Save.system.flush();
return value;
}
@ -238,7 +238,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.hapticsIntensityMultiplier = value;
save.flush();
Save.system.flush();
return value;
}
@ -263,7 +263,7 @@ class Preferences
var save:Save = Save.instance;
save.options.autoPause = value;
save.flush();
Save.system.flush();
return value;
}
@ -282,7 +282,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.autoFullscreen = value;
save.flush();
Save.system.flush();
return value;
}
@ -302,7 +302,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.globalOffset = value;
save.flush();
Save.system.flush();
return value;
}
@ -349,7 +349,7 @@ class Preferences
var save:Save = Save.instance;
save.options.vsyncMode = string;
save.flush();
Save.system.flush();
return value;
}
@ -371,7 +371,7 @@ class Preferences
var save:Save = Save.instance;
save.options.unlockedFramerate = value;
save.flush();
Save.system.flush();
return value;
}
@ -411,7 +411,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.strumlineBackgroundOpacity = value;
save.flush();
Save.system.flush();
return value;
}
@ -430,7 +430,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.screenshot.shouldHideMouse = value;
save.flush();
Save.system.flush();
return value;
}
@ -449,7 +449,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.screenshot.fancyPreview = value;
save.flush();
Save.system.flush();
return value;
}
@ -468,7 +468,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.screenshot.previewOnSave = value;
save.flush();
Save.system.flush();
return value;
}
@ -535,7 +535,7 @@ class Preferences
{
var save:Save = Save.instance;
save.options.subtitles = value;
save.flush();
Save.system.flush();
return value;
}
@ -557,7 +557,7 @@ class Preferences
var save:Save = Save.instance;
save.mobileOptions.screenTimeout = value;
save.flush();
Save.system.flush();
return value;
}
@ -576,7 +576,7 @@ class Preferences
{
var save:Save = Save.instance;
save.mobileOptions.controlsScheme = value;
save.flush();
Save.system.flush();
return value;
}
@ -601,7 +601,7 @@ class Preferences
{
var save:Save = Save.instance;
save.mobileOptions.noAds = value;
save.flush();
Save.system.flush();
return value;
}
#end

View file

@ -29,7 +29,7 @@ class DiscordClient
private function new()
{
trace('[DISCORD] Initializing event handlers...');
trace(' DISCORD '.bold().bg_blue() + ' Initializing event handlers...');
handlers = new DiscordEventHandlers();
@ -40,7 +40,7 @@ class DiscordClient
public function init():Void
{
trace('[DISCORD] Initializing connection...');
trace(' DISCORD '.bold().bg_blue() + ' Initializing connection...');
if (!hasValidCredentials())
{
@ -86,7 +86,7 @@ class DiscordClient
public function shutdown():Void
{
trace('[DISCORD] Shutting down...');
trace(' DISCORD '.bold().bg_blue() + ' Shutting down...');
Discord.Shutdown();
}
@ -140,7 +140,7 @@ class DiscordClient
// TODO: WHAT THE FUCK get this pointer bullfuckery out of here
private static function onReady(request:cpp.RawConstPointer<DiscordUser>):Void
{
trace('[DISCORD] Client has connected!');
trace(' DISCORD '.bold().bg_blue() + ' Client has connected!');
final username:String = request[0].username;
final globalName:String = request[0].username;
@ -148,22 +148,22 @@ class DiscordClient
if (discriminator != null && discriminator != 0)
{
trace('[DISCORD] User: ${username}#${discriminator} (${globalName})');
trace(' DISCORD '.bold().bg_blue() + ' User: ${username}#${discriminator} (${globalName})');
}
else
{
trace('[DISCORD] User: @${username} (${globalName})');
trace(' DISCORD '.bold().bg_blue() + ' User: @${username} (${globalName})');
}
}
private static function onDisconnected(errorCode:Int, message:cpp.ConstCharStar):Void
{
trace('[DISCORD] Client has disconnected! ($errorCode) "${cast (message, String)}"');
trace(' DISCORD '.bold().bg_blue() + ' Client has disconnected! ($errorCode) "${cast (message, String)}"');
}
private static function onError(errorCode:Int, message:cpp.ConstCharStar):Void
{
trace('[DISCORD] Client has received an error! ($errorCode) "${cast (message, String)}"');
trace(' DISCORD '.bold().bg_blue() + ' Client has received an error! ($errorCode) "${cast (message, String)}"');
}
// public var type(get, set):DiscordActivityType;

View file

@ -39,21 +39,21 @@ class Events
switch (outcome)
{
case SUCCESS(data):
trace('[NEWGROUNDS] Logged event: ${data.eventName}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logged event: ${data.eventName}');
case FAIL(outcome):
switch (outcome)
{
case HTTP(error):
trace('[NEWGROUNDS] HTTP error while logging event: ${error}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' HTTP error while logging event: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Response error (${error.code}) while logging event: ${error.message}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Response error (${error.code}) while logging event: ${error.message}');
case RESULT(error):
switch (error.code)
{
case 103: // Invalid custom event name
trace('[NEWGROUNDS] Invalid custom event name: ${eventName}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Invalid custom event name: ${eventName}');
default:
trace('[NEWGROUNDS] Result error (${error.code}) while logging event: ${error.message}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Result error (${error.code}) while logging event: ${error.message}');
}
}
}

View file

@ -17,7 +17,7 @@ class Leaderboards
var leaderboardList:Null<ScoreBoardList> = NewgroundsClient.instance.leaderboards;
if (leaderboardList == null)
{
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, cannot fetch medal data!');
return [];
}
@ -44,9 +44,9 @@ class Leaderboards
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Submitted score!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitted score!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to submit score!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to submit score!');
trace(error);
}
});
@ -75,11 +75,11 @@ class Leaderboards
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Fetched scores!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched scores!');
if (params != null && params.onComplete != null) params.onComplete(leaderboardData.scores);
case FAIL(error):
trace('[NEWGROUNDS] Failed to fetch scores!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to fetch scores!');
trace(error);
if (params != null && params.onFail != null) params.onFail();
}

View file

@ -19,7 +19,7 @@ class Medals
if (medalList == null)
{
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, cannot fetch medal data!');
return [];
}
@ -34,12 +34,12 @@ class Medals
@:privateAccess
if (medalData == null || medalData._data == null)
{
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Could not retrieve data for medal: ${medal}');
return;
}
else if (!medalData.unlocked)
{
trace('[NEWGROUNDS] Awarding medal (${medal}).');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Awarding medal (${medal}).');
medalData.sendUnlock();
// Play the medal unlock animation, but only if the user has not already unlocked it.
@ -81,12 +81,12 @@ class Medals
}
else
{
trace('[NEWGROUNDS] User already has medal (${medal}).');
trace(' NEWGROUNDS '.bold().bg_orange() + ' User already has medal (${medal}).');
}
}
else
{
trace('[NEWGROUNDS] Attempted to award medal (${medal}), but not logged into Newgrounds.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Attempted to award medal (${medal}), but not logged into Newgrounds.');
}
}
@ -98,12 +98,12 @@ class Medals
var parser = new json2object.JsonParser<Array<MedalJSON>>();
parser.ignoreUnknownVariables = false;
trace('[NEWGROUNDS] Parsing local medal data...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Parsing local medal data...');
parser.fromJson(jsonString, jsonPath);
if (parser.errors.length > 0)
{
trace('[NEWGROUNDS] Failed to parse local medal data!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to parse local medal data!');
for (error in parser.errors)
funkin.data.DataError.printError(error);
medalJSON = [];
@ -120,7 +120,7 @@ class Medals
@:privateAccess
if (medalData == null || medalData._data == null)
{
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Could not retrieve data for medal: ${medal}');
return null;
}
@ -141,7 +141,7 @@ class Medals
var medal:Medal = Medal.getMedalByStoryLevel(id);
if (medal == Medal.Unknown)
{
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Story level does not have a medal! (${id}).');
return;
}
Medals.award(medal);
@ -226,7 +226,7 @@ enum abstract Medal(Int) from Int to Int
/**
* That's How You Do It!
* Beat Tutoria l in Story Mode (on any difficulty).
* Beat Tutorial in Story Mode (on any difficulty).
*/
var StoryTutorial = #if FEATURE_NEWGROUNDS_TESTING_MEDALS 80906 #else 83647 #end;

View file

@ -25,7 +25,7 @@ class NGSaveSlot
public static function loadInstance():NGSaveSlot
{
var loadedSave:NGSaveSlot = loadSlot(Save.BASE_SAVE_SLOT);
var loadedSave:NGSaveSlot = loadSlot(Constants.BASE_SAVE_SLOT);
if (_instance == null) _instance = loadedSave;
return loadedSave;
@ -33,7 +33,7 @@ class NGSaveSlot
static function loadSlot(slot:Int):NGSaveSlot
{
trace('[NEWGROUNDS] Getting save slot from ID $slot');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Getting save slot from ID $slot');
var saveSlot:Null<SaveSlot> = NewgroundsClient.instance.saveSlots?.getById(slot);
@ -46,11 +46,6 @@ class NGSaveSlot
public function new(?ngSaveSlot:Null<SaveSlot>)
{
this.ngSaveSlot = ngSaveSlot;
#if FLX_DEBUG
FlxG.console.registerClass(NGSaveSlot);
FlxG.console.registerClass(Save);
#end
}
/**
@ -67,16 +62,16 @@ class NGSaveSlot
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Successfully saved save data to save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Successfully saved save data to save slot!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to save data to save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to save data to save slot!');
trace(error);
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to save data to save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to save data to save slot!');
trace(error);
}
}
@ -89,7 +84,7 @@ class NGSaveSlot
switch (outcome)
{
case SUCCESS(value):
trace('[NEWGROUNDS] Loaded save slot with the ID of ${ngSaveSlot?.id}!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Loaded save slot with the ID of ${ngSaveSlot?.id}!');
#if FEATURE_DEBUG_FUNCTIONS
trace('Save Slot Data:');
trace(value);
@ -101,7 +96,7 @@ class NGSaveSlot
onComplete(decodedData);
}
case FAIL(error):
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(error);
if (onError != null)
@ -113,7 +108,7 @@ class NGSaveSlot
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to load save slot with the ID of ${ngSaveSlot?.id}!');
trace(error);
if (onError != null)
@ -131,26 +126,26 @@ class NGSaveSlot
switch (outcome)
{
case SUCCESS:
trace('[NEWGROUNDS] Successfully cleared save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Successfully cleared save slot!');
case FAIL(error):
trace('[NEWGROUNDS] Failed to clear save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to clear save slot!');
trace(error);
}
});
}
catch (error:String)
{
trace('[NEWGROUNDS] Failed to clear save slot!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Failed to clear save slot!');
trace(error);
}
}
public function checkSlot():Void
{
trace('[NEWGROUNDS] Checking save slot with the ID of ${ngSaveSlot?.id}...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Checking save slot with the ID of ${ngSaveSlot?.id}...');
trace(' Is null? ${ngSaveSlot == null}');
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
trace(' Is null? ${ngSaveSlot == null}');
trace(' Is empty? ${ngSaveSlot?.isEmpty() ?? false}');
}
}
#end

View file

@ -42,11 +42,11 @@ class NewgroundsClient
private function new()
{
trace('[NEWGROUNDS] Initializing client...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Initializing client...');
#if FEATURE_NEWGROUNDS_DEBUG
trace('[NEWGROUNDS] App ID: ${API_NG_APP_ID}');
trace('[NEWGROUNDS] Encryption Key: ${API_NG_ENC_KEY}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' App ID: ${API_NG_APP_ID}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Encryption Key: ${API_NG_ENC_KEY}');
#end
if (!hasValidCredentials())
@ -67,7 +67,7 @@ class NewgroundsClient
{
if (NG.core == null) return;
trace('[NEWGROUNDS] Setting up connection...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Setting up connection...');
#if FEATURE_NEWGROUNDS_DEBUG
NG.core.verbose = true;
@ -78,16 +78,16 @@ class NewgroundsClient
if (NG.core.attemptingLogin)
{
// Session ID was valid and we should be logged in soon.
trace('[NEWGROUNDS] Waiting for existing login!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Waiting for existing login!');
}
else
{
#if FEATURE_NEWGROUNDS_AUTOLOGIN
// Attempt an automatic login.
trace('[NEWGROUNDS] Attempting new login immediately!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Attempting new login immediately!');
this.autoLogin();
#else
trace('[NEWGROUNDS] Not logged in, you have to login manually!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Not logged in, you have to login manually!');
#end
}
}
@ -107,7 +107,7 @@ class NewgroundsClient
if (NG.core.attemptingLogin)
{
trace("[NEWGROUNDS] Login attempt ongoing, will not login until finished.");
trace(" NEWGROUNDS '.bold().bg_orange() + ' Login attempt ongoing, will not login until finished.");
return;
}
@ -165,7 +165,7 @@ class NewgroundsClient
}
}
Save.instance.ngSessionId = null;
Save.instance.ngSessionId.value = null;
}
/**
@ -245,17 +245,17 @@ class NewgroundsClient
{
if (NG.core == null) return;
trace('[NEWGROUNDS] Login successful!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login successful!');
// Persist the session ID.
Save.instance.ngSessionId = NG.core.sessionId;
Save.instance.ngSessionId.value = NG.core.sessionId;
trace('[NEWGROUNDS] Submitting medal request...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting medal request...');
NG.core.requestMedals(onFetchedMedals);
trace('[NEWGROUNDS] Submitting leaderboard request...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting leaderboard request...');
NG.core.scoreBoards.loadList(onFetchedLeaderboards);
trace('[NEWGROUNDS] Submitting save slot request...');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Submitting save slot request...');
NG.core.saveSlots.loadList(onFetchedSaveSlots);
}
@ -267,32 +267,32 @@ class NewgroundsClient
switch (type)
{
case PASSPORT:
trace('[NEWGROUNDS] Login cancelled by passport website.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by passport website.');
case MANUAL:
trace('[NEWGROUNDS] Login cancelled by application.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by application.');
default:
trace('[NEWGROUNDS] Login cancelled by unknown source.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login cancelled by unknown source.');
}
case ERROR(error):
switch (error)
{
case HTTP(error):
trace('[NEWGROUNDS] Login failed due to HTTP error: ${error}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to HTTP error: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Login failed due to response error: ${error.message} (${error.code})');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to response error: ${error.message} (${error.code})');
case RESULT(error):
trace('[NEWGROUNDS] Login failed due to result error: ${error.message} (${error.code})');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to result error: ${error.message} (${error.code})');
default:
trace('[NEWGROUNDS] Login failed due to unknown error: ${error}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to unknown error: ${error}');
}
default:
trace('[NEWGROUNDS] Login failed due to unknown reason.');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Login failed due to unknown reason.');
}
}
function onLogoutSuccessful():Void
{
trace('[NEWGROUNDS] Logout successful!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout successful!');
}
function onLogoutFailed(result:CallError):Void
@ -300,31 +300,31 @@ class NewgroundsClient
switch (result)
{
case HTTP(error):
trace('[NEWGROUNDS] Logout failed due to HTTP error: ${error}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to HTTP error: ${error}');
case RESPONSE(error):
trace('[NEWGROUNDS] Logout failed due to response error: ${error.message} (${error.code})');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to response error: ${error.message} (${error.code})');
case RESULT(error):
trace('[NEWGROUNDS] Logout failed due to result error: ${error.message} (${error.code})');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to result error: ${error.message} (${error.code})');
default:
trace('[NEWGROUNDS] Logout failed due to unknown error: ${result}');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Logout failed due to unknown error: ${result}');
}
}
function onFetchedMedals(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched medals!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched medals!');
}
function onFetchedLeaderboards(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched leaderboards!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched leaderboards!');
// trace(funkin.api.newgrounds.Leaderboards.listLeaderboardData());
}
function onFetchedSaveSlots(outcome:Outcome<CallError>):Void
{
trace('[NEWGROUNDS] Fetched save slots!');
trace(' NEWGROUNDS '.bold().bg_orange() + ' Fetched save slots!');
NGSaveSlot.instance.checkSlot();
}
@ -362,7 +362,7 @@ class NewgroundsClient
#end
// We have to fetch the session ID from the save file.
return Save.instance.ngSessionId;
return Save.instance.ngSessionId.value;
}
}

View file

@ -176,7 +176,7 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
for (i in 0...group.members.length)
{
// needs to be exponential growth / scaling
// still need to optmize the FFT to run better, gets only samples needed?
// still need to optimize the FFT to run better, gets only samples needed?
// not every frequency is built the same!
// 20hz to 40z is a LOT of subtle low ends, but somethin like 20,000hz to 20,020hz, the difference is NOT the same!

View file

@ -166,7 +166,7 @@ class WaveformData
var ratio = newSamplesPerPoint / samplesPerPoint;
if (ratio == 1) return result;
if (ratio < 1) trace('[WARNING] Downsampling will result in a low precision.');
if (ratio < 1) trace(' WARNING '.bg_yellow().bold() + ' Downsampling will result in a low precision.');
var inputSampleCount = this.lenSamples();
var outputSampleCount = Std.int(inputSampleCount * ratio);

View file

@ -3,6 +3,10 @@ package funkin.audio.waveform;
import funkin.graphics.rendering.MeshRender;
import flixel.util.FlxColor;
/**
* A sprite which displays the waveform of audio data.
* Generate a WaveformData and provide it to this sprite.
*/
class WaveformSprite extends MeshRender
{
static final DEFAULT_COLOR:FlxColor = FlxColor.WHITE;
@ -18,14 +22,17 @@ class WaveformSprite extends MeshRender
* Do this any time the data or drawable area of the waveform changes.
* This often (but not always) needs to be done every frame.
*/
var isWaveformDirty:Bool = true;
var isWaveformDirty:Bool;
/**
* If true, force the waveform to redraw every frame.
* Useful if the waveform's clipRect is constantly changing.
*/
public var forceUpdate:Bool = false;
public var forceUpdate:Bool;
/**
* The data to render the waveform with.
*/
public var waveformData(default, set):Null<WaveformData>;
function set_waveformData(value:Null<WaveformData>):Null<WaveformData>
@ -52,6 +59,9 @@ class WaveformSprite extends MeshRender
return waveformColor;
}
/**
* Whether the Waveform is horizontal or vertical.
*/
public var orientation(default, set):WaveformOrientation;
function set_orientation(value:WaveformOrientation):WaveformOrientation
@ -68,7 +78,7 @@ class WaveformSprite extends MeshRender
*/
public var time(default, set):Float;
function set_time(value:Float)
function set_time(value:Float):Float
{
if (time == value) return value;
@ -77,13 +87,22 @@ class WaveformSprite extends MeshRender
return time;
}
override function set_visible(value:Bool):Bool
{
if (visible == value) return value;
visible = value;
isWaveformDirty = true;
return visible;
}
/**
* The duration, in seconds, that the waveform represents.
* The section of waveform from `time` to `time + duration` and `width` are used to determine how many samples each pixel represents.
*/
public var duration(default, set):Float;
function set_duration(value:Float)
function set_duration(value:Float):Float
{
if (duration == value) return value;
@ -120,13 +139,13 @@ class WaveformSprite extends MeshRender
*
* NOTE: This is technically doubled since it's applied above and below the center of the waveform.
*/
public var minWaveformSize:Int = 1;
public var minWaveformSize:Int;
/**
* A multiplier on the size of the waveform.
* Still capped at the width and height set for the sprite.
*/
public var amplitude:Float = 1.0;
public var amplitude:Float;
public function new(?waveformData:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
{
@ -135,6 +154,11 @@ class WaveformSprite extends MeshRender
this.width = DEFAULT_WIDTH;
this.height = DEFAULT_HEIGHT;
this.minWaveformSize = 1;
this.amplitude = 1.0;
this.isWaveformDirty = true;
this.forceUpdate = false;
this.waveformData = waveformData;
this.orientation = orientation ?? DEFAULT_ORIENTATION;
this.time = 0.0;
@ -151,7 +175,7 @@ class WaveformSprite extends MeshRender
isWaveformDirty = true;
}
public override function update(elapsed:Float)
public override function update(elapsed:Float):Void
{
super.update(elapsed);
@ -170,6 +194,11 @@ class WaveformSprite extends MeshRender
makeGraphic(1, 1, this.waveformColor);
}
public override function draw():Void
{
super.draw();
}
/**
* @param offsetX Horizontal offset to draw the waveform at, in samples.
*/
@ -183,12 +212,12 @@ class WaveformSprite extends MeshRender
this.clear();
if (waveformData == null) return;
if (waveformData == null || !this.visible) return;
// Center point of the waveform. When horizontal this is half the height, when vertical this is half the width.
var waveformCenterPos:Int = orientation == HORIZONTAL ? Std.int(this.height / 2) : Std.int(this.width / 2);
var oneSecondInIndices:Int = waveformData.secondsToIndex(1);
// var oneSecondInIndices:Int = waveformData.secondsToIndex(1)
var startTime:Float = time;
var endTime:Float = time + duration;
@ -223,7 +252,10 @@ class WaveformSprite extends MeshRender
var isBeforeClipRect:Bool = (clipRect != null) && ((orientation == HORIZONTAL) ? pixelPos < clipRect.x : pixelPos < clipRect.y);
if (isBeforeClipRect) continue;
if (isBeforeClipRect)
{
continue;
}
var isAfterClipRect:Bool = (clipRect != null)
&& ((orientation == HORIZONTAL) ? pixelPos > (clipRect.x + clipRect.width) : pixelPos > (clipRect.y + clipRect.height));
@ -426,20 +458,40 @@ class WaveformSprite extends MeshRender
}
}
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
/**
* Build a WaveformSprite from waveform data.
* @param data The data for the waveform to use.
* @param orientation Whether the waveform should be horizontal or vertical.
* @param color The color of the waveform.
* @param duration The width of the waveform, in seconds.
*
* @return The resulting WaveformSprite.
*/
public static function buildFromWaveformData(data:WaveformData, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
{
return new WaveformSprite(data, orientation, color, duration);
}
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float)
/**
* Build a WaveformSprite from a FunkinSound's waveform data.
* @param sound The audio for the waveform to use.
* @param orientation Whether the waveform should be horizontal or vertical.
* @param color The color of the waveform.
* @param duration The width of the waveform, in seconds.
*
* @return The resulting WaveformSprite.
*/
public static function buildFromFunkinSound(sound:FunkinSound, ?orientation:WaveformOrientation, ?color:FlxColor, ?duration:Float):WaveformSprite
{
// TODO: Build waveform data from FunkinSound.
var data = null;
var data = WaveformDataParser.interpretFlxSound(sound);
return buildFromWaveformData(data, orientation, color, duration);
}
}
/**
* The possible orientations of a waveform.
*/
enum WaveformOrientation
{
HORIZONTAL;

View file

@ -18,7 +18,7 @@ typedef EntryConstructorFunction = (String, ?Dynamic) -> Void;
*/
@:nullSafety
@:generic
@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry())
@:autoBuild(funkin.util.macro.RegistryMacro.buildRegistry())
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J, P>
{
/**
@ -120,14 +120,14 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
var entry:Null<T> = createEntry(entryId);
if (entry != null)
{
trace(' Loaded entry data: ${entry}');
trace(' Loaded entry data: ${entry}');
entries.set(entry.id, entry);
}
}
catch (e)
{
// Print the error.
trace(' Failed to load entry data: ${entryId}');
trace(' Failed to load entry data: ${entryId}');
trace(e);
continue;
}
@ -157,7 +157,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param id The ID of the entry.
* @return `true` if the entry has an attached script, `false` otherwise.
*/
public function isScriptedEntry(id:String):Bool
public function isScriptedEntry(id:String, ?params:Null<P>):Bool
{
return scriptedEntryIds.exists(id);
}
@ -167,7 +167,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param id The ID of the entry.
* @return The class name, or `null` if it does not exist.
*/
public function getScriptedEntryClassName(id:String):Null<String>
public function getScriptedEntryClassName(id:String, ?params:Null<P>):Null<String>
{
return scriptedEntryIds.get(id);
}

View file

@ -12,27 +12,27 @@ class DataError
switch (error)
{
case IncorrectType(vari, expected, pos):
trace(' Expected field "$vari" to be of type "$expected".');
trace(' Expected field "$vari" to be of type "$expected".');
printPos(pos);
case IncorrectEnumValue(value, expected, pos):
trace(' Invalid enum value (expected "$expected", got "$value")');
trace(' Invalid enum value (expected "$expected", got "$value")');
printPos(pos);
case InvalidEnumConstructor(value, expected, pos):
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
printPos(pos);
case UninitializedVariable(vari, pos):
trace(' Uninitialized variable "$vari"');
trace(' Uninitialized variable "$vari"');
printPos(pos);
case UnknownVariable(vari, pos):
trace(' Unknown variable "$vari"');
trace(' Unknown variable "$vari"');
printPos(pos);
case ParserError(message, pos):
trace(' Parsing error: ${message}');
trace(' Parsing error: ${message}');
printPos(pos);
case CustomFunctionException(e, pos):
if (Std.isOfType(e, String))
{
trace(' ${e}');
trace(' ${e}');
}
else
{
@ -49,11 +49,11 @@ class DataError
switch (Type.typeof(e))
{
case TClass(c):
trace(' [${Type.getClassName(c)}] ${e.toString()}');
trace(' [${Type.getClassName(c)}] ${e.toString()}');
case TEnum(c):
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
default:
trace(' [${Type.typeof(e)}] ${e.toString()}');
trace(' [${Type.typeof(e)}] ${e.toString()}');
}
}
@ -66,11 +66,11 @@ class DataError
{
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
{
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
}
else
{
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
}
}
}

View file

@ -83,7 +83,7 @@ typedef UnnamedAnimationData =
/**
* Optionally specify an asset path to use for this specific animation.
* ONLY for use by MultiSparrow characters.
* ONLY for use by MultiSparrow and MultiAnimateAtlas characters.
* @default The assetPath of the parent sprite
*/
@:optional
@ -138,4 +138,13 @@ typedef UnnamedAnimationData =
@:default([])
@:optional
var frameIndices:Null<Array<Int>>;
/**
* The type of animation to use.
* Only available for texture atlases.
* Options: "framelabel", "symbol"
*/
@:default("framelabel")
@:optional
var animType:String;
}

View file

@ -6,12 +6,14 @@ import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedMultiAnimateAtlasCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
import funkin.play.character.AnimateAtlasCharacter;
import funkin.play.character.BaseCharacter;
import funkin.play.character.SparrowCharacter;
import funkin.play.character.MultiSparrowCharacter;
import funkin.play.character.MultiAnimateAtlasCharacter;
import funkin.play.character.PackerCharacter;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
@ -58,7 +60,7 @@ class CharacterDataParser
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool {
return !characterCache.exists(charId);
});
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
for (charId in unscriptedCharIds)
{
try
@ -66,7 +68,7 @@ class CharacterDataParser
var charData:Null<CharacterData> = parseCharacterData(charId);
if (charData != null)
{
trace(' Loaded character data: ${charId}');
trace(' Loaded character data: ${charId}');
characterCache.set(charId, charData);
}
}
@ -86,18 +88,18 @@ class CharacterDataParser
var scriptedCharClassNames1:Array<String> = ScriptedSparrowCharacter.listScriptClasses();
if (scriptedCharClassNames1.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
for (charCls in scriptedCharClassNames1)
{
try
{
var character:SparrowCharacter = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
trace(' Initialized character ${character.characterName}');
trace(' Initialized character ${character.characterName}');
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Sparrow character: ${charCls}');
trace(' FAILED to instantiate scripted Sparrow character: ${charCls}');
trace(e);
}
}
@ -106,7 +108,7 @@ class CharacterDataParser
var scriptedCharClassNames2:Array<String> = ScriptedPackerCharacter.listScriptClasses();
if (scriptedCharClassNames2.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
for (charCls in scriptedCharClassNames2)
{
try
@ -116,7 +118,7 @@ class CharacterDataParser
}
catch (e)
{
trace(' FAILED to instantiate scripted Packer character: ${charCls}');
trace(' FAILED to instantiate scripted Packer character: ${charCls}');
trace(e);
}
}
@ -125,7 +127,7 @@ class CharacterDataParser
var scriptedCharClassNames3:Array<String> = ScriptedMultiSparrowCharacter.listScriptClasses();
if (scriptedCharClassNames3.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
for (charCls in scriptedCharClassNames3)
{
try
@ -135,7 +137,7 @@ class CharacterDataParser
}
catch (e)
{
trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}');
trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}');
trace(e);
}
}
@ -144,7 +146,7 @@ class CharacterDataParser
var scriptedCharClassNames4:Array<String> = ScriptedAnimateAtlasCharacter.listScriptClasses();
if (scriptedCharClassNames4.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
for (charCls in scriptedCharClassNames4)
{
try
@ -154,7 +156,26 @@ class CharacterDataParser
}
catch (e)
{
trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}');
trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}');
trace(e);
}
}
}
var scriptedCharClassNames5:Array<String> = ScriptedMultiAnimateAtlasCharacter.listScriptClasses();
if (scriptedCharClassNames5.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames5.length} (Multi-Animate Atlas) scripted characters...');
for (charCls in scriptedCharClassNames5)
{
try
{
var character:MultiAnimateAtlasCharacter = ScriptedMultiAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID);
characterScriptedClass.set(character.characterId, charCls);
}
catch (e)
{
trace(' FAILED to instantiate scripted Multi-Animate Atlas character: ${charCls}');
trace(e);
}
}
@ -167,29 +188,30 @@ class CharacterDataParser
return !(scriptedCharClassNames1.contains(charCls)
|| scriptedCharClassNames2.contains(charCls)
|| scriptedCharClassNames3.contains(charCls)
|| scriptedCharClassNames4.contains(charCls));
|| scriptedCharClassNames4.contains(charCls)
|| scriptedCharClassNames5.contains(charCls));
});
if (scriptedCharClassNames.length > 0)
{
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
for (charCls in scriptedCharClassNames)
{
var character:BaseCharacter = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID, Custom);
if (character == null)
{
trace(' Failed to instantiate scripted character: ${charCls}');
trace(' Failed to instantiate scripted character: ${charCls}');
continue;
}
else
{
trace(' Successfully instantiated scripted character: ${charCls}');
trace(' Successfully instantiated scripted character: ${charCls}');
characterScriptedClass.set(character.characterId, charCls);
}
}
}
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
}
/**
@ -226,6 +248,8 @@ class CharacterDataParser
char = ScriptedSparrowCharacter.init(charScriptClass, charId);
case CharacterRenderType.Packer:
char = ScriptedPackerCharacter.init(charScriptClass, charId);
case CharacterRenderType.MultiAnimateAtlas:
char = ScriptedMultiAnimateAtlasCharacter.init(charScriptClass, charId);
default:
// We're going to assume that the script class does the rendering.
char = ScriptedBaseCharacter.init(charScriptClass, charId, CharacterRenderType.Custom);
@ -243,8 +267,10 @@ class CharacterDataParser
char = new SparrowCharacter(charId);
case CharacterRenderType.Packer:
char = new PackerCharacter(charId);
case CharacterRenderType.MultiAnimateAtlas:
char = new MultiAnimateAtlasCharacter(charId);
default:
trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
trace(' WARNING '.bold().bg_yellow() + ' Creating character with undefined renderType ${charData.renderType}');
char = new BaseCharacter(charId, CharacterRenderType.Custom);
}
}
@ -312,7 +338,7 @@ class CharacterDataParser
if (!Assets.exists(Paths.image(charPath)))
{
trace('[WARN] Character ${char} has no freeplay icon.');
trace(' WARNING '.bold().bg_yellow() + ' Character ${char} has no freeplay icon.');
return null;
}
@ -329,7 +355,7 @@ class CharacterDataParser
if (idleFrame == null)
{
trace('[WARN] Character ${char} has no idle in their freeplay icon.');
trace(' WARNING '.bold().bg_yellow() + ' Character ${char} has no idle in their freeplay icon.');
return null;
}
@ -404,8 +430,8 @@ class CharacterDataParser
}
catch (e)
{
trace(' Error parsing data for character: ${charId}');
trace(' ${e}');
trace(' Error parsing data for character: ${charId}');
trace(' ${e}');
return null;
}
}
@ -431,6 +457,15 @@ class CharacterDataParser
public static final DEFAULT_SCALE:Float = 1;
public static final DEFAULT_SCROLL:Array<Float> = [0, 0];
public static final DEFAULT_STARTINGANIM:String = 'idle';
public static final DEFAULT_APPLYSTAGEMATRIX:Bool = false;
public static final DEFAULT_ANIMTYPE:String = "framelabel";
public static final DEFAULT_ATLASSETTINGS:funkin.data.stage.StageData.TextureAtlasData =
{
swfMode: true,
cacheOnLoad: false,
filterQuality: 1,
applyStageMatrix: false
};
/**
* Set unspecified parameters to their defaults.
@ -559,6 +594,16 @@ class CharacterDataParser
input.flipX = DEFAULT_FLIPX;
}
if (input.applyStageMatrix == null)
{
input.applyStageMatrix = DEFAULT_APPLYSTAGEMATRIX;
}
if (input.atlasSettings == null)
{
input.atlasSettings = DEFAULT_ATLASSETTINGS;
}
if (input.animations.length == 0 && input.startingAnimation != null)
{
return null;
@ -596,6 +641,11 @@ class CharacterDataParser
{
inputAnimation.flipY = DEFAULT_FLIPY;
}
if (inputAnimation.animType == null)
{
inputAnimation.animType = DEFAULT_ANIMTYPE;
}
}
// All good!
@ -624,10 +674,15 @@ enum abstract CharacterRenderType(String) from String to String
public var MultiSparrow = 'multisparrow';
/**
* Renders the character using a spritesheet of symbols and JSON data.
* Renders the character using a single spritesheet of symbols and JSON data.
*/
public var AnimateAtlas = 'animateatlas';
/**
* Renders the character using multiple spritesheets of symbols and JSON data.
*/
public var MultiAnimateAtlas = 'multianimateatlas';
/**
* Renders the character using a custom method.
*/
@ -674,6 +729,9 @@ typedef CharacterData =
*/
var healthIcon:Null<HealthIconData>;
/**
* Optional data about the death animation for the character.
*/
var death:Null<DeathData>;
/**
@ -734,6 +792,23 @@ typedef CharacterData =
* @default false
*/
var flipX:Null<Bool>;
/**
* NOTE: This only applies to animate atlas characters.
*
* Whether to apply the stage matrix, if it was exported from a symbol instance.
* Also positions the Texture Atlas as it displays in Animate.
* Turning this on is only recommended if you prepositioned the character in Animate.
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
*/
var applyStageMatrix:Null<Bool>;
/**
* Various settings for the prop.
* Only available for texture atlases.
*/
@:optional
var atlasSettings:funkin.data.stage.StageData.TextureAtlasData;
};
/**

View file

@ -46,12 +46,12 @@ class SongEventRegistry
if (event != null)
{
trace(' Loaded built-in song event: ${event.id}');
trace(' Loaded built-in song event: ${event.id}');
eventCache.set(event.id, event);
}
else
{
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
}
}
}
@ -68,12 +68,12 @@ class SongEventRegistry
if (event != null)
{
trace(' Loaded scripted song event: ${event.id}');
trace(' Loaded scripted song event: ${event.id}');
eventCache.set(event.id, event);
}
else
{
trace(' Failed to instantiate scripted song event class: ${eventCls}');
trace(' Failed to instantiate scripted song event class: ${eventCls}');
}
}
}

View file

@ -89,7 +89,7 @@ class PlayerData
updateVersionToLatest();
var writer = new json2object.JsonWriter<PlayerData>();
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
@ -103,6 +103,10 @@ class PlayerFreeplayDJData
var assetPath:String;
var animations:Array<AnimationData>;
@:optional
@:default(false)
var applyStageMatrix:Bool;
@:optional
@:default("BOYFRIEND")
var text1:String;
@ -158,11 +162,16 @@ class PlayerFreeplayDJData
}
public inline function getAssetPath():String
return assetPath; // return Paths.animateAtlas(assetPath);
return assetPath; // return assetPath;
public inline function getAnimationsList():Array<AnimationData>
return animations;
public function useApplyStageMatrix():Bool
{
return applyStageMatrix;
}
public function getFreeplayDJText(index:Int):String
{
switch (index)
@ -343,6 +352,10 @@ typedef PlayerResultsAnimationData =
*/
var renderType:String;
@:optional
@:default(false)
var applyStageMatrix:Bool;
@:optional
var assetPath:Null<String>;

View file

@ -70,7 +70,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
public function hasNewCharacter():Bool
{
#if (!UNLOCK_EVERYTHING)
var charactersSeen = Save.instance.charactersSeen.clone();
var charactersSeen = Save.instance.charactersSeen.value.clone();
for (charId in listEntryIds())
{
@ -94,7 +94,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
var result = [];
#if (!UNLOCK_EVERYTHING)
var charactersSeen = Save.instance.charactersSeen.clone();
var charactersSeen = Save.instance.charactersSeen.value.clone();
for (charId in listEntryIds())
{
var player = fetchEntry(charId);
@ -143,7 +143,7 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData, PlayerE
#if UNLOCK_EVERYTHING
return true;
#else
return Save.instance.charactersSeen.contains(characterId);
return Save.instance.charactersSeen.value.contains(characterId);
#end
}
}

View file

@ -126,7 +126,7 @@ class SongMetadata implements ICloneable<SongMetadata>
// I believe @:jignored should be ignored by the writer?
// var output = this.clone();
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
@ -664,7 +664,7 @@ class SongChartData implements ICloneable<SongChartData>
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void

View file

@ -212,7 +212,7 @@ class SongDataUtils
{
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongClipboardItems>(ignoreNullOptionals);
var dataString:String = writer.write(data, ' ');
var dataString:String = writer.write(data, ' ');
ClipboardUtil.setClipboard(dataString);

View file

@ -99,14 +99,14 @@ using funkin.data.song.migrator.SongDataMigrator;
var entry:Null<Song> = createEntry(entryId);
if (entry != null)
{
trace(' Loaded entry data: ${entry}');
trace(' Loaded entry data: ${entry}');
entries.set(entry.id, entry);
}
}
catch (e:Dynamic)
{
// Print the error.
trace(' Failed to load entry data: ${entryId}');
trace(' Failed to load entry data: ${entryId}');
trace(e);
continue;
}
@ -129,6 +129,29 @@ using funkin.data.song.migrator.SongDataMigrator;
return parseEntryMetadataRaw(contents);
}
public override function isScriptedEntry(id:String, ?params:Null<SongEntryParams>)
{
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
if (variation != Constants.DEFAULT_VARIATION)
{
return scriptedSongVariations.exists('${id}:${variation}');
}
return super.isScriptedEntry(id, params);
}
public override function getScriptedEntryClassName(id:String, ?params:Null<SongEntryParams>):Null<String>
{
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
if (variation != Constants.DEFAULT_VARIATION)
{
final variationSongId:ScriptedSong = cast scriptedSongVariations.get('${id}:${variation}');
@:privateAccess
var path:String = variationSongId._asc._c.name;
return path;
}
return super.getScriptedEntryClassName(id, params);
}
/**
* We override `fetchEntry` to handle song variations!
*/
@ -446,7 +469,7 @@ using funkin.data.song.migrator.SongDataMigrator;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
if (!openfl.Assets.exists(entryFilePath))
{
trace(' [WARN] Could not locate file $entryFilePath');
trace(' WARNING '.bold().bg_yellow() + ' Could not locate file $entryFilePath');
return null;
}
var rawJson:Null<String> = openfl.Assets.getText(entryFilePath);
@ -524,7 +547,7 @@ using funkin.data.song.migrator.SongDataMigrator;
if (character == null)
{
trace(' [WARN] Could not locate character $characterId');
trace(' WARNING '.bold().bg_yellow() + ' Could not locate character $characterId');
return allDifficulties;
}
@ -542,7 +565,7 @@ using funkin.data.song.migrator.SongDataMigrator;
if (allDifficulties.length == 0)
{
trace(' [WARN] No difficulties found. Returning default difficulty list.');
trace(' WARNING '.bold().bg_yellow() + ' No difficulties found. Returning default difficulty list.');
allDifficulties = Constants.DEFAULT_DIFFICULTY_LIST.copy();
}

View file

@ -22,6 +22,7 @@ class ChartManifestData
* The metadata and chart data file names are derived from this.
*/
public var songId(default, set):String;
public function set_songId(value:String):String
{
return songId = invalidIdRegex.replace(value.trim(), '');
@ -71,7 +72,7 @@ class ChartManifestData
updateVersionToLatest();
var writer = new json2object.JsonWriter<ChartManifestData>();
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void

View file

@ -0,0 +1,82 @@
package funkin.data.song.importer;
/**
* Structure of a parsed Osu!Mania .osu file
* Stuctured like a INI file format by CSV for HitObjects and more
*/
typedef OsuManiaData =
{
var General:
{
var PreviewTime:Int;
};
var Editor:
{
var DistanceSpacing:Float;
var BeatDivisor:Int;
var GridSize:Int;
};
var Metadata:
{
var Title:String;
var TitleUnicode:String;
var Artist:String;
var ArtistUnicode:String;
var Creator:String;
var Version:String;
};
var Difficulty:
{
var OverallDifficulty:Int;
var SliderMultiplier:Float;
var SliderTickRate:Float;
var CircleSize:Int;
};
var HitObjects:Array<ManiaHitObject>;
var TimingPoints:Array<TimingPoint>;
var Events:Array<Any>;
}
class TimingPoint
{
public var time:Float;
public var beatLength:Float;
public var meter:Int;
public var sampleSet:Int;
public var sampleIndex:Int;
public var volume:Int;
public var uninherited:Int;
public var effects:Int;
public var bpm:Null<Float>;
public var sv:Null<Float>;
public function new(time:Float, beatLength:Float, meter:Int, sampleSet:Int, sampleIndex:Int, volume:Int, uninherited:Int, effects:Int)
{
this.time = time;
this.beatLength = beatLength;
this.meter = meter;
this.sampleSet = sampleSet;
this.sampleIndex = sampleIndex;
this.volume = volume;
this.uninherited = uninherited;
this.effects = effects;
// Derived values
this.bpm = (uninherited == 1) ? (Math.round((60000 / beatLength) * 10) / 10) : null;
this.sv = (uninherited == 0) ? (beatLength / 100) : null; // Just incase someone wants to add Scroll Velocity Support
}
}
class ManiaHitObject
{
public var time:Int;
public var column:Int;
public var holdDuration:Int;
public function new(time:Int, column:Int, holdDuration:Int)
{
this.time = time;
this.column = column;
this.holdDuration = holdDuration;
}
}

View file

@ -0,0 +1,199 @@
package funkin.data.song.importer;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.importer.OsuManiaData;
import funkin.data.song.importer.OsuManiaData.TimingPoint;
import funkin.data.song.importer.OsuManiaData.ManiaHitObject;
class OsuManiaImporter
{
public static function parseOsuFile(osuContent:String):OsuManiaData
{
var lines:Array<String> = osuContent.split("\n");
var result:Dynamic = {};
var currentSection:String = null;
var nonCSVLikeSections = ["General", "Editor", "Metadata", "Difficulty"];
for (line in lines)
{
line = StringTools.trim(line);
if (line == "" || StringTools.startsWith(line, "//")) continue;
// Section header like [General]
var sectionRegex = ~/^\[(.+)\]$/;
if (sectionRegex.match(line))
{
currentSection = sectionRegex.matched(1);
if (nonCSVLikeSections.contains(currentSection))
{
Reflect.setField(result, currentSection, {});
}
else
{
Reflect.setField(result, currentSection, []);
}
continue;
}
// Key-value pairs (INI style)
if (currentSection != null && nonCSVLikeSections.contains(currentSection))
{
var parts:Array<String> = line.split(":");
var key:String = StringTools.trim(parts.shift());
var value:String = StringTools.trim(parts.join(":"));
if (Reflect.field(result, currentSection) == null) Reflect.setField(result, currentSection, {});
Reflect.setField(Reflect.field(result, currentSection), key, value);
}
// For CSV-like sections
else if (currentSection != null)
{
var theArray:Array<String> = cast Reflect.field(result, currentSection);
theArray.push(line);
Reflect.setField(result, currentSection, theArray);
}
}
return result;
}
/**
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
* @param difficulty The difficulty name to assign to the migrated chart.
* @return SongMetadata
*/
public static function migrateMetadata(songData:OsuManiaData, difficulty:String = 'normal'):SongMetadata
{
trace('Migrating song metadata from Osu!Mania.');
var songMetadata:SongMetadata = new SongMetadata('Import', songData.Metadata.ArtistUnicode ?? songData.Metadata.Artist ?? Constants.DEFAULT_ARTIST,
songData.Metadata.Creator ?? Constants.DEFAULT_CHARTER, Constants.DEFAULT_VARIATION);
// Set generatedBy string for debugging.
songMetadata.generatedBy = 'Chart Editor Import (Osu!Mania)';
songMetadata.playData.stage = 'mainStage';
songMetadata.songName = songData.Metadata.TitleUnicode ?? songData.Metadata.Title ?? 'Import';
songMetadata.playData.difficulties = [difficulty];
songMetadata.playData.songVariations = [];
songMetadata.timeChanges = rebuildTimeChanges(songData);
songMetadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
songMetadata.playData.ratings.set(difficulty, songData.Difficulty.OverallDifficulty ?? 0);
return songMetadata;
}
static function rebuildTimeChanges(songData:OsuManiaData):Array<SongTimeChange>
{
var timings:Array<TimingPoint> = parseTimingPoints(cast songData.TimingPoints);
var bpmPoints:Array<TimingPoint> = timings.filter((tp) -> tp.uninherited == 1);
var result:Array<SongTimeChange> = [];
if (bpmPoints.length >= 1)
{
result.push(new SongTimeChange(0, bpmPoints[0].bpm ?? Constants.DEFAULT_BPM));
for (i in 1...bpmPoints.length)
{
var bpmPoint:TimingPoint = bpmPoints[i];
result.push(new SongTimeChange(bpmPoint.time, bpmPoint.bpm ?? Constants.DEFAULT_BPM));
}
}
if (result.length == 0)
{
result.push(new SongTimeChange(0, Constants.DEFAULT_BPM));
trace("[WARN] No BPM points found, resulting to default BPM...");
}
return result;
}
/**
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
* @param difficulty The difficulty name to assign to the migrated chart.
* @return SongChartData
*/
public static function migrateChartData(songData:OsuManiaData, difficulty:String = 'normal'):SongChartData
{
trace('Migrating song chart data from Osu!Mania.');
// Osu!Mania doesn't have a scroll speed variable as its controlled by the player
var songChartData:SongChartData = new SongChartData([difficulty => Constants.DEFAULT_SCROLLSPEED], [], [difficulty => []]);
// songData.HitObjects is a Array<String> here so im casting it so haxe stops yelling at me
var osuNotes:Array<ManiaHitObject> = parseManiaHitObjects(cast songData.HitObjects, songData.Difficulty.CircleSize);
songChartData.notes.set(difficulty, convertNotes(osuNotes, songData.Difficulty.CircleSize));
songChartData.events = [];
return songChartData;
}
static final STRUMLINE_SIZE = 4;
static function convertNotes(hitObjects:Array<ManiaHitObject>, keyCount:Int):Array<SongNoteData>
{
var result:Array<SongNoteData> = [];
for (hitObject in hitObjects)
{
var wrappedColumn:Int = hitObject.column % (keyCount * 2); // wrap overflow for 9K+
if (keyCount <= 4) // if its 4K or less, flip to BF side
{
wrappedColumn -= 4;
var noteOffset:Int = Std.int(Math.abs(keyCount - STRUMLINE_SIZE));
var flippedNoteData:Int = wrappedColumn + keyCount + noteOffset;
result.push(new SongNoteData(hitObject.time, flippedNoteData, hitObject.holdDuration ?? 0, ''));
}
else
result.push(new SongNoteData(hitObject.time, wrappedColumn, hitObject.holdDuration ?? 0, ''));
}
return result;
}
static function parseTimingPoints(timingLines:Array<String>):Array<TimingPoint>
{
return timingLines.map(function(line:String):TimingPoint {
var parts = line.split(",");
var time = Std.parseFloat(parts[0]);
var beatLength = Std.parseFloat(parts[1]);
var meter = Std.parseInt(parts[2]);
var sampleSet = Std.parseInt(parts[3]);
var sampleIndex = Std.parseInt(parts[4]);
var volume = Std.parseInt(parts[5]);
var uninherited = Std.parseInt(parts[6]);
var effects = Std.parseInt(parts[7]);
return new TimingPoint(time, beatLength, meter, sampleSet, sampleIndex, volume, uninherited, effects);
});
}
static function parseManiaHitObjects(hitObjectsLines:Array<String>, ?columns:Int = 4):Array<ManiaHitObject>
{
return hitObjectsLines.map(function(line:String):ManiaHitObject {
var parts = line.split(",");
var x:Int = Std.parseInt(parts[0]);
var time:Int = Std.parseInt(parts[2]);
var type:Int = Std.parseInt(parts[3]);
var hasHold:Bool = (type & 128) == 128;
var noteD:Int = Std.int(x / (512 / columns));
var holdEndTime:Null<Int> = hasHold ? Std.parseInt(parts[5].split(":")[0]) : null;
var holdDuration:Int = (holdEndTime != null) ? (holdEndTime - time) : 0;
return new ManiaHitObject(time, noteD, holdDuration);
});
}
}

View file

@ -0,0 +1,123 @@
package funkin.data.song.importer;
typedef StepManiaData =
{
var Metadata:
{
var Title:String;
var Artist:String;
var Genre:String;
var Credit:String;
var Banner:String;
var Background:String;
var Offset:Float;
var SampleStart:Float;
};
var TimingPoints:Array<StepTimingPoint>;
var Stops:Array<StepStop>;
var Difficulties:Array<StepDifficulty>;
}
enum StepManiaChartType {
DanceSingle;
DanceDouble;
Unknown;
}
enum StepManiaNoteType {
Tap;
Head;
Tail;
Roll;
Mine;
Fake;
}
class StepNote
{
public var beat:Float;
public var column:Int;
public var type:StepManiaNoteType;
public function new(t:String, beat:Float, column:Int)
{
this.beat = beat;
this.column = column;
switch (t) {
case "2":
this.type = StepManiaNoteType.Head;
case "3":
this.type = StepManiaNoteType.Tail;
case "4":
this.type = StepManiaNoteType.Roll;
case "M":
this.type = StepManiaNoteType.Mine;
case "F":
this.type = StepManiaNoteType.Fake;
default:
this.type = StepManiaNoteType.Tap;
}
}
}
class StepDifficulty
{
public var name:String;
public var charter:String;
public var difficultyRating:Int;
public var type:StepManiaChartType;
public var notes:Array<StepNote>;
public function parseChartType(chartTypeStr:String):StepManiaChartType
{
switch (chartTypeStr) {
case "dance-single":
return StepManiaChartType.DanceSingle;
case "dance-double":
return StepManiaChartType.DanceDouble;
default:
return StepManiaChartType.Unknown;
}
}
public function new(name:String, charter:String, difficultyRating:Int, type:String)
{
this.name = name;
this.charter = charter;
this.difficultyRating = difficultyRating;
this.type = parseChartType(type);
}
}
class StepTimingPoint
{
public var bpm:Float;
public var startBeat:Float = Math.NEGATIVE_INFINITY;
public var endBeat:Float = Math.POSITIVE_INFINITY;
public var startTimestamp:Float = 0;
public var endTimestamp:Float = 0;
public function new(bpm:Float, startBeat:Float)
{
this.bpm = bpm;
this.startBeat = startBeat;
}
}
// Not implemented, but if any chart uses them then the chart will break.
// IE this messes with the timing of the notes.
class StepStop
{
public var startBeat:Float = Math.NEGATIVE_INFINITY;
public var duration:Float = 0;
public var startTimestamp:Float = Math.NEGATIVE_INFINITY;
public function new(startBeat:Float, duration:Float)
{
this.startBeat = startBeat;
this.duration = duration;
}
}

View file

@ -0,0 +1,613 @@
package funkin.data.song.importer;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.importer.StepManiaData.StepTimingPoint;
import funkin.data.song.importer.StepManiaData.StepDifficulty;
import funkin.data.song.importer.StepManiaData.StepManiaChartType;
import funkin.data.song.importer.StepManiaData.StepNote;
import funkin.data.song.importer.StepManiaData.StepStop;
import funkin.data.song.importer.StepManiaData.StepManiaNoteType;
enum StepStateEnum
{
Metadata;
TimingPoints;
Stops;
Notes;
}
class StepManiaImporter
{
private static var readDiffMetadata:Map<String, String> = new Map<String, String>();
static function parseMetadataLine(line:String, result:StepManiaData):StepManiaData
{
var parts:Array<String> = line.split(":");
if (parts.length != 2) return result;
var key:String = StringTools.trim(parts[0]);
var value:String = StringTools.trim(parts[1]);
// remove trailing ; on value
if (StringTools.endsWith(value, ";")) value = value.substr(0, value.length - 1);
switch (key)
{
case "TITLE":
result.Metadata.Title = value;
case "ARTIST":
result.Metadata.Artist = value;
case "GENRE":
result.Metadata.Genre = value;
case "CREDIT":
if (result.Metadata.Credit == "") result.Metadata.Credit = value;
else // .ssc
readDiffMetadata.set("CREDIT", value);
case "BANNER":
result.Metadata.Banner = value;
case "BACKGROUND":
result.Metadata.Background = value;
case "OFFSET":
result.Metadata.Offset = Std.parseFloat(value);
case "SAMPLESTART":
result.Metadata.SampleStart = Std.parseFloat(value);
// .ssc
case "STEPSTYPE":
readDiffMetadata.set("STEPSTYPE", value);
case "DESCRIPTION":
readDiffMetadata.set("DESCRIPTION", value);
case "DIFFICULTY":
readDiffMetadata.set("DIFFICULTY", value);
}
return result;
}
static function parseTimingPointLine(line:String):Array<StepTimingPoint>
{
// Remove #BPMS: prefix if present
if (StringTools.startsWith(line, "#BPMS:")) line = line.substr(6);
var parts:Array<String> = line.split(",");
var stepTimingPoints:Array<StepTimingPoint> = [];
for (i in 0...parts.length)
{
// Split the =
var split = parts[i].split("=");
if (split.length != 2) continue;
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
// Check if split[i + 1] has ; and remove it
var bpmSplit = split[1];
if (StringTools.endsWith(bpmSplit, ";")) bpmSplit = bpmSplit.substr(0, bpmSplit.length - 1);
var tp:StepTimingPoint = new StepTimingPoint(Std.parseFloat(bpmSplit), beat);
stepTimingPoints.push(tp);
}
return stepTimingPoints;
}
static function parseStopsLine(line:String):Array<StepStop>
{
// Remove #STOPS: prefix if present
if (StringTools.startsWith(line, "#STOPS:")) line = line.substr(7);
// Same as timing points
var parts:Array<String> = line.split(",");
var stepStopPoints:Array<StepStop> = [];
for (i in 0...parts.length)
{
// Split the =
var split = parts[i].split("=");
if (split.length != 2) continue;
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
// Check if split[i + 1] has ; and remove it
var durSplit = split[1];
if (StringTools.endsWith(durSplit, ";")) durSplit = durSplit.substr(0, durSplit.length - 1);
var tp:StepStop = new StepStop(beat, Std.parseFloat(durSplit));
stepStopPoints.push(tp);
}
return stepStopPoints;
}
static function parseMeasure(lines:Array<String>, measureIndex:Int):Array<StepNote>
{
var lengthInRows:Float = 192.0 / lines.length;
var rowIndex:Int = 0;
var beat:Float = 0;
var stepNotes:Array<StepNote> = [];
for (i in 0...lines.length)
{
var stepNoteRow:Float = measureIndex * 192 + (lengthInRows * rowIndex);
beat = stepNoteRow / 48.0; // 48 rows per beat
for (j in 0...lines[i].length)
{
var char:String = lines[i].charAt(j);
if (char != "0")
{
var stepNote:StepNote = new StepNote(char, beat, j);
stepNotes.push(stepNote);
}
}
rowIndex++;
}
return stepNotes;
}
static function synchronizeStepTimingPoints(stepStopPoints:Array<StepStop>, stepTimingPoints:Array<StepTimingPoint>):Array<StepTimingPoint>
{
// Initialize startTimestamp/endTimestamp and endBeat for timing points
for (tpIndex in 0...stepTimingPoints.length)
{
var tp = stepTimingPoints[tpIndex];
// ensure fields exist
if (tp.startTimestamp == Math.NEGATIVE_INFINITY) tp.startTimestamp = 0.0;
}
var tpIndex:Int = 1;
var spIndex:Int = 0;
// if we have more than one timing point or any StepStops, synchronize
if (stepTimingPoints.length > 1 || stepStopPoints.length > 0)
{
if (stepStopPoints.length > 1)
{
while (tpIndex < stepTimingPoints.length || spIndex < stepStopPoints.length)
{
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
if (tpIndex == stepTimingPoints.length)
{
var cts:Float = 0;
while (spIndex < stepStopPoints.length)
{
var sp = stepStopPoints[spIndex];
sp.startTimestamp = (prevTp.startTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60)) + cts;
cts += sp.duration;
spIndex++;
}
continue;
}
if (spIndex == stepStopPoints.length)
{
while (tpIndex < stepTimingPoints.length)
{
var tp = stepTimingPoints[tpIndex];
prevTp = stepTimingPoints[tpIndex - 1];
prevTp.endBeat = tp.startBeat;
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tp.endTimestamp = tp.startTimestamp;
tpIndex++;
if (tpIndex == stepTimingPoints.length)
{
tp.endTimestamp = Math.POSITIVE_INFINITY;
tp.endBeat = Math.POSITIVE_INFINITY;
}
}
continue;
}
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
var sp:StepStop = stepStopPoints[spIndex];
prevTp.endBeat = tp.startBeat;
if (sp.startBeat < prevTp.endBeat)
{
sp.startTimestamp = prevTp.endTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60);
prevTp.endTimestamp += sp.duration;
spIndex++;
}
else
{
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tp.endTimestamp = tp.startTimestamp;
tpIndex++;
}
}
}
else
{
while (tpIndex < stepTimingPoints.length)
{
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
prevTp.endBeat = tp.startBeat;
prevTp.endTimestamp = prevTp.startTimestamp + (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tpIndex++;
if (tpIndex == stepTimingPoints.length)
{
tp.endTimestamp = Math.POSITIVE_INFINITY;
tp.endBeat = Math.POSITIVE_INFINITY;
}
}
}
}
return stepTimingPoints;
}
static function pushWorking(workingDiff:StepDifficulty, result:StepManiaData):StepManiaData
{
if (workingDiff != null && workingDiff.notes.length > 0)
{
// Apply .ssc metadata if any
for (metaKey in readDiffMetadata.keys())
{
var metaValue = readDiffMetadata.get(metaKey);
switch (metaKey)
{
case "STEPSTYPE":
workingDiff.type = workingDiff.parseChartType(metaValue);
case "DIFFICULTY":
workingDiff.name = metaValue;
case "CREDIT":
workingDiff.charter = metaValue;
// DESCRIPTION is ignored for now
}
}
result.Difficulties.push(workingDiff);
}
readDiffMetadata.clear();
return result;
}
static function parseBPMS(line:String, result:StepManiaData):StepManiaData
{
var tps = parseTimingPointLine(line);
if (tps != null)
{
for (tp in tps)
result.TimingPoints.push(tp);
}
return result;
}
static function parseStops(line:String, result:StepManiaData):StepManiaData
{
var sps = parseStopsLine(line);
if (sps != null)
{
for (sp in sps)
result.Stops.push(sp);
}
return result;
}
/**
* Parses a StepMania file content into StepManiaData structure.
* @param stepContent The content of the StepMania file as a string.
* @return StepManiaData The parsed StepMania data.
*/
public static function parseStepManiaFile(stepContent:String):StepManiaData
{
readDiffMetadata = new Map<String, String>();
var lines:Array<String> = stepContent.split("\n");
var currentMeasure:Int = 0;
var measure:Array<String> = [];
var workingDiff:StepDifficulty = null;
var state:StepStateEnum = StepStateEnum.Metadata;
var headerLines:Int = 0;
var result:StepManiaData = {
Metadata: {
Title: "",
Artist: "",
Genre: "",
Credit: "",
Banner: "",
Background: "",
Offset: 0,
SampleStart: 0
},
TimingPoints: [],
Stops: [],
Difficulties: []
};
// Parsing metadata
for (line in lines)
{
line = StringTools.trim(line);
if (line == "") continue;
if (StringTools.startsWith(line, "//")) continue; // Comment line
switch (state)
{
case StepStateEnum.Metadata:
if (StringTools.startsWith(line, "#BPMS:"))
{
state = StepStateEnum.TimingPoints;
result = parseBPMS(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
else if (StringTools.startsWith(line, "#STOPS:"))
{
state = StepStateEnum.Stops;
result = parseStops(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
else if (StringTools.startsWith(line, "#NOTES:"))
{
if (workingDiff != null)
{
// Save previous difficulty
result = pushWorking(workingDiff, result);
}
// Start new difficulty
workingDiff = new StepDifficulty("", "", 0, "");
workingDiff.notes = [];
headerLines = 0;
currentMeasure = 0;
measure = [];
state = StepStateEnum.Notes;
}
else if (StringTools.startsWith(line, "#")) result = parseMetadataLine(line.substr(1), result);
case StepStateEnum.TimingPoints:
if (line == ";") state = StepStateEnum.Metadata;
else
{
result = parseBPMS(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
case StepStateEnum.Stops:
if (line == ";") state = StepStateEnum.Metadata;
else
{
result = parseStops(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
case StepStateEnum.Notes:
if (line == "#NOTES:") continue;
// remove comments in line (if any) ie, "0000 // some comment"
var commentIndex = line.indexOf("//");
if (commentIndex != -1) line = StringTools.trim(line.substr(0, commentIndex));
if (StringTools.contains(line, ":")) // header
{
switch (headerLines)
{
case 0:
// First line is chart type
var chartTypeStr = StringTools.trim(line).replace(":", "");
workingDiff.type = workingDiff.parseChartType(chartTypeStr);
case 1:
// Second line is charter
workingDiff.charter = StringTools.trim(line).replace(":", "");
case 2:
// Third line is difficulty name
workingDiff.name = StringTools.trim(line).replace(":", "");
case 3:
// Fourth line is difficulty rating
workingDiff.difficultyRating = Std.parseInt(StringTools.trim(line).replace(":", ""));
// we dont care about the rest lol
}
headerLines++;
continue;
}
// we're reading StepNote data now
// start gathering measures until we hit a ,
if (line == "," || line == ";")
{
// end of measure
var stepNotesInMeasure = parseMeasure(measure, currentMeasure);
for (stepNote in stepNotesInMeasure)
workingDiff.notes.push(stepNote);
measure = [];
currentMeasure++;
// end of StepNotes section
if (line == ";") state = StepStateEnum.Metadata;
}
else
{
measure.push(line);
}
}
}
if (workingDiff != null && workingDiff.notes.length > 0)
{
// Save last difficulty
result = pushWorking(workingDiff, result);
}
// Syncronize timing points with StepStops
result.TimingPoints = synchronizeStepTimingPoints(result.Stops, result.TimingPoints);
return result;
}
static function getStepStopAtBeat(beat:Float, stepStops:Array<StepStop>):StepStop
{
var stepStop:StepStop = null;
for (s in stepStops)
{
if (s.startBeat < beat) stepStop = s;
}
return stepStop;
}
static function beatToTime(beat:Float, offset:Float, stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Float
{
var time:Float = 0;
for (tp in stepTimingPoints)
{
if (tp.startBeat <= beat && tp.endBeat > beat)
{
var stepStop:StepStop = getStepStopAtBeat(beat, stepStops);
var b:Float = tp.startBeat;
var startTime:Float = tp.startTimestamp;
if (stepStop != null)
{
if (stepStop.startBeat > tp.startBeat)
{
b = stepStop.startBeat;
startTime = stepStop.startTimestamp + stepStop.duration;
}
}
var nb:Float = (beat - b) / (tp.bpm / 60);
time = startTime + nb;
}
}
return (time * 1000) - (offset * 1000); // convert to ms
}
static function convertStepNotes(offset:Float, type:StepManiaChartType, stepNotes:Array<StepNote>,
stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Array<SongNoteData>
{
var result:Array<SongNoteData> = [];
var holdArray:Array<Float> = [];
if (type == StepManiaChartType.DanceSingle)
{
holdArray = [-1, -1, -1, -1];
}
else if (type == StepManiaChartType.DanceDouble)
{
holdArray = [-1, -1, -1, -1, -1, -1, -1, -1];
}
else
{
trace("[WARN] Unknown StepMania chart type when converting notes.");
return result;
}
for (stepNote in stepNotes)
{
var time = beatToTime(stepNote.beat, offset, stepTimingPoints, stepStops);
if (stepNote.type == StepManiaNoteType.Head || stepNote.type == StepManiaNoteType.Roll)
{
holdArray[stepNote.column] = time;
continue;
}
var snd:SongNoteData = new SongNoteData(time, stepNote.column, 0);
if (stepNote.type == StepManiaNoteType.Mine) snd.kind = "mine";
else if (stepNote.type == StepManiaNoteType.Fake) snd.kind = "fake";
if (stepNote.type == StepManiaNoteType.Tail)
{
var length:Float = 0;
if (holdArray[stepNote.column] != -1) length = time - holdArray[stepNote.column];
snd.time = holdArray[stepNote.column];
snd.length = length;
holdArray[stepNote.column] = -1;
}
if (type != StepManiaChartType.DanceSingle)
{
if (stepNote.column < 4) snd.data = stepNote.column + 4;
else
snd.data = stepNote.column - 4;
}
else
{
if (snd.data >= 4) snd.data -= 4;
}
result.push(snd);
}
return result;
}
/**
* Migrates StepManiaData to SongMetadata.
* @param songData The StepManiaData to migrate.
* @return SongMetadata The migrated SongMetadata.
*/
public static function migrateChartMetadata(songData:StepManiaData):SongMetadata
{
var metadata:SongMetadata = new SongMetadata(songData.Metadata.Title, songData.Metadata.Artist);
metadata.playData.stage = 'mainStage';
metadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
metadata.generatedBy = 'Chart Editor Import (StepMania)';
metadata.playData.songVariations = [];
// Difficulties
var difficulties:Array<String> = [];
for (diff in songData.Difficulties)
{
if (diff.type == StepManiaChartType.Unknown)
{
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
continue; // skip unknown chart types
}
difficulties.push(diff.name);
metadata.playData.ratings.set(diff.name, diff.difficultyRating);
}
metadata.playData.difficulties = difficulties;
metadata.charter = songData.Metadata.Credit != "" ? songData.Metadata.Credit : null;
// TimeChanges
metadata.timeChanges = [];
for (tp in songData.TimingPoints)
{
var timeChange = new SongTimeChange(tp.startTimestamp * 1000, tp.bpm);
timeChange.beatTime = tp.startBeat;
metadata.timeChanges.push(timeChange);
}
return metadata;
}
/**
* Migrates StepManiaData to SongChartData.
* @param songData The StepManiaData to migrate.
* @return SongChartData The migrated SongChartData.
*/
public static function migrateChartData(songData:StepManiaData):SongChartData {
var scrollsMap:Map<String, Float> = new Map<String, Float>();
var stepNoteMap:Map<String, Array<SongNoteData>> = new Map<String, Array<SongNoteData>>();
for (diff in songData.Difficulties)
{
if (diff.type == StepManiaChartType.Unknown)
{
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
continue; // skip unknown chart types
}
scrollsMap.set(diff.name, Constants.DEFAULT_SCROLLSPEED);
stepNoteMap.set(diff.name, convertStepNotes(songData.Metadata.Offset, diff.type, diff.notes, songData.TimingPoints, songData.Stops));
}
var songChartData:SongChartData = new SongChartData(scrollsMap, [], stepNoteMap);
return songChartData;
}
}

View file

@ -66,7 +66,7 @@ class StageData
updateVersionToLatest();
var writer = new json2object.JsonWriter<StageData>();
return writer.write(this, pretty ? ' ' : null);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
@ -196,7 +196,7 @@ typedef StageDataProp =
/**
* The animation type to use.
* Options: "sparrow", "packer"
* Options: "sparrow", "packer", "animateatlas"
* @default "sparrow"
*/
@:default("sparrow")
@ -228,6 +228,52 @@ typedef StageDataProp =
@:default("#FFFFFF")
@:optional
var color:String;
/**
* Various settings for the prop.
* Only available for texture atlases.
*/
@:optional
var atlasSettings:TextureAtlasData;
};
typedef TextureAtlasData =
{
/**
* If true, the texture atlas will behave as if it was exported as an SWF file.
* Notably, this allows MovieClip symbols to play.
*/
@:optional
var swfMode:Bool;
/**
* If true, filters and masks will be cached when the atlas is loaded, instead of during runtime.
*/
@:optional
var cacheOnLoad:Bool;
/**
* The filter quality.
* Available values are: HIGH, MEDIUM, LOW, and RUDY.
*
* If you're making an atlas sprite in HScript, you pass an Int instead:
*
* HIGH - 0
* MEDIUM - 1
* LOW - 2
* RUDY - 3
*/
@:optional
var filterQuality:Int;
/**
* Whether to apply the stage matrix, if it was exported from a symbol instance.
* Also positions the Texture Atlas as it displays in Animate.
* Turning this on is only recommended if you prepositioned the character in Animate.
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
*/
@:optional
var applyStageMatrix:Bool;
};
typedef StageDataCharacter =

View file

@ -114,4 +114,11 @@ typedef LevelPropData =
@:default([])
@:optional
var animations:Array<AnimationData>;
/**
* Flips the sprite on X axis.
*/
@:default(false)
@:optional
var flipX:Null<Bool>;
}

View file

@ -0,0 +1,26 @@
package funkin.external.android;
#if android
import lime.system.JNI;
/**
* Utility class for keyboard detection.
*/
class KeyboardUtil
{
/**
* Returns `true` if a keyboard is currently connected to the device.
*/
public static var keyboardConnected(get, never):Bool;
@:noCompletion
static function get_keyboardConnected():Bool
{
final method:Null<Dynamic> = JNIUtil.createStaticMethod('funkin/util/KeyboardUtil', 'isKeyboardConnected', '()Z');
if (method == null) return false;
return inline JNI.callStatic(method, []);
}
}
#end

View file

@ -0,0 +1,37 @@
package funkin.extensions;
import android.content.Context;
import android.media.AudioManager;
import android.os.Bundle;
import org.haxe.extension.Extension;
public class AudioSession extends Extension {
private AudioManager audioManager;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
audioManager = (AudioManager) Extension.mainContext.getSystemService(Context.AUDIO_SERVICE);
requestAudioFocus();
}
@Override
public void onPause() {
super.onPause();
if (audioManager != null) {
audioManager.abandonAudioFocus(null);
}
}
@Override
public void onResume() {
super.onResume();
requestAudioFocus();
}
private void requestAudioFocus() {
if (audioManager != null) {
audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
}
}
}

View file

@ -0,0 +1,14 @@
package funkin.util;
import org.haxe.extension.Extension;
public class KeyboardUtil
{
public static boolean isKeyboardConnected()
{
if (Extension.mainContext == null) return false;
// KEYBOARD_UNDEFINED = 0, KEYBOARD_NOKEYS = 1
return Extension.mainContext.getResources().getConfiguration().keyboard > 1;
}
}

View file

@ -3,14 +3,32 @@
#import <TargetConditionals.h>
#import <Foundation/Foundation.h>
#import <AVFAudio/AVFAudio.h>
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
#endif
void Apple_AudioSession_Initialize()
{
#if TARGET_OS_IOS
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
[session setCategory:AVAudioSessionCategorySoloAmbient error:nil];
[session setActive:YES error:nil];
[[NSNotificationCenter defaultCenter] addObserverForName:@"UISceneWillEnterForegroundNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
NSError *resumeError = nil;
[session setCategory:AVAudioSessionCategorySoloAmbient withOptions:0 error:nil];
[session setActive:YES error:&resumeError];
if (resumeError)
NSLog(@"AudioSession resume error: %@", resumeError);
else
NSLog(@"AudioSession resumed and reactivated");
}];
[session setCategory:AVAudioSessionCategoryPlayback mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionAllowBluetoothA2DP | AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers error:&error];
if (@available(iOS 17.0, *))

View file

@ -0,0 +1,35 @@
package funkin.graphics;
import funkin.graphics.FunkinSprite;
import animate.FlxAnimateController;
class FunkinAnimationController extends FlxAnimateController
{
/**
* The sprite that this animation controller is attached to.
*/
var _parentSprite:FunkinSprite;
public function new(sprite:FunkinSprite)
{
super(sprite);
_parentSprite = sprite;
}
/**
* We override `FlxAnimationController`'s `play` method to account for texture atlases.
*/
public override function play(animName:String, force = false, reversed = false, frame = 0):Void
{
if (animName == null || animName == '') animName = _parentSprite.getDefaultSymbol();
if (!_parentSprite.hasAnimation(animName))
{
// Skip if the animation doesn't exist
trace('Animation ${animName} does not exist!');
return;
}
super.play(animName, force, reversed, frame);
}
}

View file

@ -9,28 +9,147 @@ import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData;
import flixel.math.FlxRect;
import flixel.math.FlxPoint;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
import flixel.math.FlxMatrix;
import flixel.graphics.frames.FlxFrame;
import flixel.FlxCamera;
import openfl.system.System;
import flixel.system.FlxAssets.FlxGraphicAsset;
import funkin.FunkinMemory;
import animate.internal.SymbolItem;
import animate.internal.elements.Element;
import animate.internal.elements.AtlasInstance;
import animate.internal.elements.SymbolInstance;
import animate.FlxAnimate;
import animate.FlxAnimateFrames;
import haxe.io.Path;
using StringTools;
typedef AtlasSpriteSettings =
{
/**
* If true, the texture atlas will behave as if it was exported as an SWF file.
* Notably, this allows MovieClip symbols to play.
*/
@:optional
var swfMode:Bool;
/**
* If true, filters and masks will be cached when the atlas is loaded, instead of during runtime.
*/
@:optional
var cacheOnLoad:Bool;
/**
* The filter quality.
* Available values are: HIGH, MEDIUM, LOW, and RUDY.
*
* If you're making an atlas sprite in HScript, you pass an Int instead:
*
* HIGH - 0
* MEDIUM - 1
* LOW - 2
* RUDY - 3
*/
@:optional
var filterQuality:FilterQuality;
/**
* Optional, an array of spritemaps for the atlas to load.
*/
@:optional
var spritemaps:Array<SpritemapInput>;
/**
* Optional, string of the metadata.json contents.
*/
@:optional
var metadataJson:String;
/**
* Optional, force the cache to use a specific key to index the texture atlas.
*/
@:optional
var cacheKey:String;
/**
* If true, the texture atlas will use a new slot in the cache.
*/
@:optional
var uniqueInCache:Bool;
/**
* Optional callback for when a symbol is created.
*/
@:optional
var onSymbolCreate:animate.internal.SymbolItem->Void;
/**
* Whether to apply the stage matrix, if it was exported from a symbol instance.
* Also positions the Texture Atlas as it displays in Animate.
* Turning this on is only recommended if you prepositioned the character in Animate.
* For other cases, it should be turned off to act similarly to a normal FlxSprite.
*/
@:optional
var applyStageMatrix:Bool;
}
/**
* An FlxSprite with additional functionality.
* - A more efficient method for creating solid color sprites.
* - TODO: Better cache handling for textures.
*/
@:nullSafety
class FunkinSprite extends FlxSprite
class FunkinSprite extends FlxAnimate
{
/**
* @param x Starting X position
* @param y Starting Y position
* @param path The asset path for the graphic
* @param atlasSettings The optional settings for the texture atlas
*/
public function new(?x:Float = 0, ?y:Float = 0)
public function new(?x:Float = 0, ?y:Float = 0, ?path:String, ?atlasSettings:AtlasSpriteSettings)
{
super(x, y);
if (path != null)
{
var ext:String = Path.extension(path);
switch (ext)
{
case 'png':
this.loadGraphic(path);
case '':
// Do the opposite of Paths.animateAtlas since that function is called in loadTextureAtlas.
var lib:String = Paths.getLibrary(path);
if (lib == 'preload')
{
path = path.replace('assets/images/', '');
}
else
{
path = path.replace('$lib:assets/$lib/images/', '');
}
this.loadTextureAtlas(path, lib, atlasSettings);
default:
FlxG.log.warn('Texture path $path is not a valid path. Make sure the path points to either an image or a folder with the texture atlas files!');
}
}
}
override function initVars():Void
{
super.initVars();
var newController:FunkinAnimationController = new FunkinAnimationController(this);
animation = newController;
anim = newController;
}
/**
@ -75,6 +194,20 @@ class FunkinSprite extends FlxSprite
return sprite;
}
/**
* Create a new FunkinSprite with an Adobe Animate texture atlas.
* @param x The starting X position.
* @param y The starting Y position.
* @param key The key of the texture to load.
* @return The new FunkinSprite.
*/
public static function createTextureAtlas(x:Float = 0.0, y:Float = 0.0, key:String, ?assetLibrary:Null<String>, ?settings:AtlasSpriteSettings):FunkinSprite
{
var sprite:FunkinSprite = new FunkinSprite(x, y);
sprite.loadTextureAtlas(key, assetLibrary ?? "", settings);
return sprite;
}
/**
* Load a static image as the sprite's texture.
* @param key The key of the texture to load.
@ -83,7 +216,17 @@ class FunkinSprite extends FlxSprite
public function loadTexture(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
if (!Assets.exists(graphicKey, IMAGE))
{
FlxG.log.error('Texture not found, check your path! $graphicKey');
return this;
}
if (!FunkinMemory.isTextureCached(graphicKey))
{
FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
}
loadGraphic(graphicKey);
@ -163,6 +306,63 @@ class FunkinSprite extends FlxSprite
return loadBitmapData(inputBitmap);
}
/**
* Loads an Adobe Animate texture atlas as the sprite's texture.
* @param key The key of the texture to load.
* @param settings Additional settings for loading the atlas.
* @return This sprite, for chaining.
*/
public function loadTextureAtlas(key:Null<String>, ?assetLibrary:Null<String>, ?settings:AtlasSpriteSettings):FunkinSprite
{
if (key == null)
{
throw 'Null path specified for loadTextureAtlas()!';
}
var validatedSettings:AtlasSpriteSettings =
{
swfMode: settings?.swfMode ?? false,
cacheOnLoad: settings?.cacheOnLoad ?? false,
filterQuality: settings?.filterQuality ?? MEDIUM,
spritemaps: settings?.spritemaps ?? null,
metadataJson: settings?.metadataJson ?? null,
cacheKey: settings?.cacheKey ?? null,
uniqueInCache: settings?.uniqueInCache ?? false,
onSymbolCreate: settings?.onSymbolCreate ?? null,
applyStageMatrix: settings?.applyStageMatrix ?? false
};
var assetLibrary:String = assetLibrary ?? "";
var graphicKey:String = "";
if (assetLibrary != "")
{
graphicKey = Paths.animateAtlas(key, assetLibrary);
}
else
{
graphicKey = Paths.animateAtlas(key);
}
// Validate asset path.
if (!Assets.exists('${graphicKey}/Animation.json'))
{
throw 'No Animation.json file exists at the specified path (${graphicKey})';
}
this.applyStageMatrix = validatedSettings.applyStageMatrix ?? false;
frames = FlxAnimateFrames.fromAnimate(graphicKey, validatedSettings.spritemaps, validatedSettings.metadataJson, validatedSettings.cacheKey,
validatedSettings.uniqueInCache, {
swfMode: validatedSettings.swfMode,
cacheOnLoad: validatedSettings.cacheOnLoad,
filterQuality: validatedSettings.filterQuality,
onSymbolCreate: validatedSettings.onSymbolCreate
});
return this;
}
/**
* Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
* @param key The key of the texture to load.
@ -171,7 +371,7 @@ class FunkinSprite extends FlxSprite
public function loadSparrow(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
this.frames = Paths.getSparrowAtlas(key);
@ -186,73 +386,13 @@ class FunkinSprite extends FlxSprite
public function loadPacker(key:String):FunkinSprite
{
var graphicKey:String = Paths.image(key);
if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
if (!FunkinMemory.isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
this.frames = Paths.getPackerAtlas(key);
return this;
}
/**
* Determine whether the texture with the given key is cached.
* @param key The key of the texture to check.
* @return Whether the texture is cached.
*/
public static function isTextureCached(key:String):Bool
{
return FlxG.bitmap.get(key) != null;
}
@:deprecated("Use FunkinMemory.cacheTexture() instead")
public static function cacheTexture(key:String):Void
{
FunkinMemory.cacheTexture(Paths.image(key));
}
@:deprecated("Use FunkinMemory.permanentCacheTexture() instead")
public static function permanentCacheTexture(key:String):Void
{
@:privateAccess FunkinMemory.permanentCacheTexture(Paths.image(key));
}
@:deprecated("Use FunkinMemory.cacheTexture() instead")
public static function cacheSparrow(key:String):Void
{
FunkinMemory.cacheTexture(Paths.image(key));
}
@:deprecated("Use FunkinMemory.cacheTexture() instead")
public static function cachePacker(key:String):Void
{
FunkinMemory.cacheTexture(Paths.image(key));
}
@:deprecated("Use FunkinMemory.preparePurgeTextureCache() instead")
public static function preparePurgeCache():Void
{
FunkinMemory.preparePurgeTextureCache();
}
@:deprecated("Use FunkinMemory.purgeCache() instead")
public static function purgeCache():Void
{
FunkinMemory.purgeCache();
}
static function isGraphicCached(graphic:FlxGraphic):Bool
{
var result = null;
if (graphic == null) return false;
result = FlxG.bitmap.get(graphic.key);
if (result == null) return false;
if (result != graphic)
{
FlxG.log.warn('Cached graphic does not match original: ${graphic.key}');
return false;
}
return true;
}
/**
* @param id The animation ID to check.
* @return Whether the animation is dynamic (has multiple frames). `false` for static, one-frame animations.
@ -266,6 +406,107 @@ class FunkinSprite extends FlxSprite
return animData.numFrames > 1;
}
/**
* Whether or not this sprite has an animation with the given ID.
* @param id The ID of the animation to check.
*/
public function hasAnimation(id:String):Bool
{
var animationList:Array<String> = this.animation?.getNameList() ?? [];
if (animationList.contains(id))
{
return true;
}
else if (this.isAnimate && !animationList.contains(id))
{
return addAnimationIfMissing(id);
}
return false;
}
/**
* Adds an animation if it doesn't exist.
* @param id The animation ID to check.
*/
function addAnimationIfMissing(id:String):Bool
{
@:privateAccess
var symbols:Array<String> = this.library.dictionary.keys().array();
var frameLabels:Array<String> = listAnimations();
if (frameLabels.contains(id))
{
// Animation exists as a frame label but wasn't added, so we add it
anim.addByFrameLabel(id, id, this.library.frameRate, false);
return true;
}
else if (symbols.contains(id))
{
// Animation exists as a symbol but wasn't added, so we add it
anim.addBySymbol(id, id, this.library.frameRate, false);
return true;
}
return false;
}
/**
* Gets every frame on every symbol that starts with the given keyword.
* @param keyword The keyword to search for.
* @return An array of frames.
*/
public function getFramesWithKeyword(keyword:String):Array<animate.internal.Frame>
{
if (!this.isAnimate)
{
trace('WARNING: getFramesWithKeyword() only works texture atlases!');
return [];
}
var symbolItems:Array<animate.internal.SymbolItem> = [];
var frames:Array<animate.internal.Frame> = [];
@:privateAccess
for (symbol in this.library.dictionary.keys())
{
var symbolItem:Null<animate.internal.SymbolItem> = this.library.getSymbol(symbol);
if (symbolItem == null) continue;
if (symbolItem.name.contains(keyword))
{
symbolItems.push(symbolItem);
}
}
for (symbolItem in symbolItems)
{
symbolItem.timeline.forEachLayer((layer) -> {
layer.forEachFrame((frame) -> {
frames.push(frame);
});
});
}
return frames;
}
/**
* Gets the current animation ID.
*/
public function getCurrentAnimation():String
{
return this.animation?.curAnim?.name ?? '';
}
/**
* Whether or not the current animation is finished.
*/
public function isAnimationFinished():Bool
{
return this.animation?.finished ?? false;
}
/**
* Acts similarly to `makeGraphic`, but with improved memory usage,
* at the expense of not being able to paint onto the resulting sprite.
@ -286,6 +527,201 @@ class FunkinSprite extends FlxSprite
return this;
}
/**
* @return A list of all the animations this sprite has available.
*/
public function listAnimations():Array<String>
{
var frameLabels:Array<String> = getFrameLabelList();
var animationList:Array<String> = this.animation?.getNameList() ?? [];
return frameLabels.concat(animationList);
}
/**
* TEXTURE ATLAS-EXCLUSIVE FUNCTIONS
* These functions only work if the sprite's texture is an Adobe Animate texture atlas.
* Calling these functions on non-texture atlases will do nothing.
*/
/**
* Gets a list of frame labels from the default timeline.
*/
public function getFrameLabelList():Array<String>
{
if (!this.isAnimate)
{
trace('WARNING: getFrameLabelList() only works texture atlases!');
return [];
}
var foundLabels:Array<String> = [];
var mainTimeline:Null<animate.internal.Timeline> = this.library.timeline;
for (layer in mainTimeline.layers)
{
@:nullSafety(Off)
for (frame in layer.frames)
{
if (frame.name.rtrim() != '')
{
foundLabels.push(frame.name);
}
}
}
return foundLabels;
}
/**
* Gets a frame label by its name.
* @param name The name of the frame label to retrieve.
* @return The frame label, or null if it doesn't exist.
*/
public function getFrameLabel(name:String):Null<animate.internal.Frame>
{
if (!this.isAnimate)
{
trace('WARNING: getFrameLabel() only works texture atlases!');
return null;
}
for (layer in this.timeline.layers)
{
@:nullSafety(Off)
for (frame in layer.frames)
{
if (frame.name == name)
{
return frame;
}
}
}
return null;
}
/**
* Returns the default symbol in the atlas.
*/
public function getDefaultSymbol():String
{
if (!this.isAnimate)
{
trace('WARNING: getDefaultSymbol() only works texture atlases!');
return '';
}
return library.timeline.name;
}
/**
* Replaces the graphic of a symbol in the atlas.
* @param symbol The symbol to replace.
* @param graphic The new graphic to use.
* @param adjustScale Whether to adjust the scale of new frame to match the old one.
*/
public function replaceSymbolGraphic(symbol:String, ?graphic:Null<FlxGraphicAsset>, ?adjustScale:Bool = true):Void
{
if (!this.isAnimate)
{
trace('WARNING: replaceSymbolGraphic() only works texture atlases!');
return;
}
var elements:Array<Element> = getSymbolElements(symbol);
for (element in elements)
{
var atlasInstance:AtlasInstance = element.toAtlasInstance();
var frame:Null<FlxFrame> = graphic != null ? FlxG.bitmap.add(graphic).imageFrame.frame : null;
atlasInstance.replaceFrame(frame, adjustScale);
element = atlasInstance;
}
}
/**
* Returns the first element of a symbol in the atlas.
* @param symbol The symbol to get elements from.
* @return The first element of the symbol. WARNING: Can be null.
*/
public function getFirstElement(symbol:String):Null<Element>
{
if (!this.isAnimate)
{
trace('WARNING: getFirstElement() only works texture atlases!');
return null;
}
var symbolElements:Array<Element> = getSymbolElements(symbol);
return symbolElements.length > 0 ? symbolElements[0] : null;
}
/**
* Returns the elements of a symbol in the atlas.
* @param symbol The symbol to get elements from.
*/
public function getSymbolElements(symbol:String):Array<Element>
{
if (!this.isAnimate)
{
trace('WARNING: getSymbolElements() only works texture atlases!');
return [];
}
var symbolInstance:Null<SymbolItem> = this.library.getSymbol(symbol);
if (symbolInstance == null)
{
throw 'Symbol not found in atlas: ${symbol}';
return [];
}
var elements:Array<Element> = symbolInstance.timeline.getElementsAtIndex(0);
if (elements?.length == 0)
{
trace('WARNING: No Atlas Elements found for "$symbol" symbol.');
}
return elements ?? [];
}
/**
* Scales an element by a certain multiplier.
* @param element The element to scale.
* @param scale The scale multiplier.
* @param positionOffset The offset to apply to `tx` and `ty` after scaling.
* (Or in other words, the position of the element.)
*/
public function scaleElement(element:Element, scale:Float, positionOffset:Float = 0, scaleEverything:Bool = false):Void
{
if (!this.isAnimate)
{
trace('WARNING: scaleElement() only works texture atlases!');
return;
}
var elementMatrix:FlxMatrix = element.matrix;
if (scaleEverything)
{
elementMatrix.scale(scale, scale);
return;
}
var symbolInstance:SymbolInstance = element.parentFrame.convertToSymbol(0, 1);
var transformPoint:FlxPoint = symbolInstance.transformationPoint;
elementMatrix.a += scale;
elementMatrix.d += scale;
elementMatrix.tx -= transformPoint.x * scale;
elementMatrix.ty -= transformPoint.y * scale;
elementMatrix.tx -= positionOffset;
elementMatrix.ty -= positionOffset;
}
/**
* Ensure scale is applied when cloning a sprite.R
* The default `clone()` method acts kinda weird TBH.
@ -325,77 +761,10 @@ class FunkinSprite extends FlxSprite
return _rect;
}
/**
* Returns the screen position of this object.
*
* @param result Optional arg for the returning point
* @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
* @return The screen position of this object.
*/
public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
override function preparePixelPerfectMatrix(matrix:FlxMatrix)
{
if (result == null) result = FlxPoint.get();
if (camera == null) camera = FlxG.camera;
result.set(x, y);
if (pixelPerfectPosition)
{
_rect.width = _rect.width / this.scale.x;
_rect.height = _rect.height / this.scale.y;
_rect.x = _rect.x / this.scale.x;
_rect.y = _rect.y / this.scale.y;
_rect.round();
_rect.x = _rect.x * this.scale.x;
_rect.y = _rect.y * this.scale.y;
_rect.width = _rect.width * this.scale.x;
_rect.height = _rect.height * this.scale.y;
}
return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
}
override function drawSimple(camera:FlxCamera):Void
{
getScreenPosition(_point, camera).subtractPoint(offset);
if (isPixelPerfectRender(camera))
{
_point.x = _point.x / this.scale.x;
_point.y = _point.y / this.scale.y;
_point.round();
_point.x = _point.x * this.scale.x;
_point.y = _point.y * this.scale.y;
}
_point.copyToFlash(_flashPoint);
camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
}
override function drawComplex(camera:FlxCamera):Void
{
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
_matrix.translate(-origin.x, -origin.y);
_matrix.scale(scale.x, scale.y);
if (bakedRotationAngle <= 0)
{
updateTrig();
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
}
getScreenPosition(_point, camera).subtractPoint(offset);
_point.add(origin.x, origin.y);
_matrix.translate(_point.x, _point.y);
if (isPixelPerfectRender(camera))
{
_matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x;
_matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y;
}
camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
matrix.tx = Math.round(matrix.tx / this.scale.x) * this.scale.x;
matrix.ty = Math.round(matrix.ty / this.scale.y) * this.scale.y;
}
public override function destroy():Void

View file

@ -1,414 +0,0 @@
package funkin.graphics.adobeanimate;
import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
import flixel.graphics.frames.FlxFrame;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.math.FlxPoint;
import flxanimate.animate.FlxKeyFrame;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
*/
@:nullSafety
class FlxAtlasSprite extends FlxAnimate
{
static final SETTINGS:Settings =
{
// ?ButtonSettings:Map<String, flxanimate.animate.FlxAnim.ButtonSettings>,
FrameRate: 24.0,
Reversed: false,
// ?OnComplete:Void -> Void,
ShowPivot: false,
Antialiasing: true,
ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
/**
* Signal dispatched when an animation advances to the next frame.
*/
public var onAnimationFrame:FlxTypedSignal<String->Int->Void> = new FlxTypedSignal();
/**
* Signal dispatched when a non-looping animation finishes playing.
*/
public var onAnimationComplete:FlxTypedSignal<String->Void> = new FlxTypedSignal();
/**
* Signal dispatched when a looping animation finishes playing.
*/
public var onAnimationLoop:FlxTypedSignal<String->Void> = new FlxTypedSignal();
var currentAnimation:String = '';
var canPlayOtherAnims:Bool = true;
@:nullSafety(Off) // null safety HATES new classes atm, it'll be fixed in haxe 4.0.0?
public function new(x:Float, y:Float, ?path:String, ?settings:Settings, ?pathSafety:Bool = true)
{
if (settings == null) settings = SETTINGS;
if (path == null && pathSafety) throw 'Null path specified for FlxAtlasSprite!';
// Validate asset path.
if (!Assets.exists('${path}/Animation.json')
&& pathSafety) throw 'FlxAtlasSprite does not have an Animation.json file at the specified path (${path})';
super(x, y, path, settings);
if (this.anim.stageInstance == null && pathSafety) throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
onAnimationComplete.add(cleanupAnimation);
// This defaults the sprite to play the first animation in the atlas,
// then pauses it. This ensures symbols are intialized properly.
if (this.anim.curInstance != null)
{
this.anim.play('');
this.anim.pause();
}
this.anim.onComplete.add(_onAnimationComplete);
this.anim.onFrame.add(_onAnimationFrame);
}
/**
* @return A list of all the animations this sprite has available.
*/
public function listAnimations():Array<String>
{
var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name];
if (mainSymbol == null)
{
FlxG.log.error('FlxAtlasSprite does not have its main symbol!');
return [];
}
return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull();
}
/**
* @param id A string ID of the animation.
* @return Whether the animation was found on this sprite.
*/
public function hasAnimation(id:String):Bool
{
return getLabelIndex(id) != -1 || anim.symbolDictionary.exists(id);
}
/**
* @return The current animation being played.
*/
public function getCurrentAnimation():String
{
return this.currentAnimation;
}
var _completeAnim:Bool = false;
var fr:Null<FlxKeyFrame> = null;
var looping:Bool = false;
public var ignoreExclusionPref:Array<String> = [];
/**
* Plays an animation.
* @param id A string ID of the animation to play.
* @param restart Whether to restart the animation if it is already playing.
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
* @param loop Whether to loop the animation
* @param startFrame The frame to start the animation on
* NOTE: `loop` and `ignoreOther` are not compatible with each other!
*/
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void
{
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims))
{
if (this.currentAnimation == id && restart) {}
else if (ignoreExclusionPref != null && ignoreExclusionPref.length > 0)
{
var detected:Bool = false;
for (entry in ignoreExclusionPref)
{
if (StringTools.startsWith(id, entry))
{
detected = true;
break;
}
}
if (!detected) return;
}
else
return;
}
if (anim == null) return;
if (id == null || id == '') id = this.currentAnimation;
if (this.currentAnimation == id && !restart)
{
if (!anim.isPlaying)
{
if (fr != null) anim.curFrame = fr.index + startFrame;
else
anim.curFrame = startFrame;
// Resume animation if it's paused.
anim.resume();
}
return;
}
else if (!hasAnimation(id))
{
// Skip if the animation doesn't exist
trace('Animation ' + id + ' not found');
return;
}
this.currentAnimation = id;
anim.onComplete.removeAll();
anim.onComplete.add(function() {
_onAnimationComplete();
});
looping = loop;
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
// Move to the first frame of the animation.
// goToFrameLabel(id);
// trace('Playing animation $id');
if ((id == null || id == "") || this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null))
{
this.anim.play(id, restart, false, startFrame);
this.currentAnimation = anim.curSymbol.name;
fr = null;
}
var frameLabelNames = getFrameLabelNames();
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
if (frameLabelNames != null && frameLabelNames.indexOf(id) != -1)
{
goToFrameLabel(id);
fr = anim.getFrameLabel(id);
anim.curFrame += startFrame;
// Resume animation if it's paused.
anim.resume();
}
}
override public function update(elapsed:Float):Void
{
super.update(elapsed);
}
/**
* Returns true if the animation has finished playing.
* @return Whether the animation has finished playing.
*/
public function isAnimationFinished():Bool
{
return isLoopComplete();
}
/**
* Returns true if the animation has reached the last frame.
* Can be true even if animation is configured to loop.
* @return Whether the animation has reached the last frame.
*/
public function isLoopComplete():Bool
{
if (this.anim == null) return false;
if (!this.anim.isPlaying) return false;
if (fr != null)
{
var curFrame = anim.curFrame;
var startFrame = fr.index;
var endFrame = (fr.index + fr.duration);
return (anim.reversed) ? (curFrame < startFrame) : (curFrame >= endFrame);
}
return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
}
/**
* Stops the current animation.
*/
public function stopAnimation():Void
{
if (this.currentAnimation == null) return;
this.anim.removeAllCallbacksFrom(getNextFrameLabel(this.currentAnimation));
goToFrameIndex(0);
}
function addFrameCallback(label:String, callback:Void->Void):Void
{
var frameLabel = this.anim.getFrameLabel(label);
frameLabel.add(callback);
}
function goToFrameLabel(label:String):Void
{
this.anim.goToFrameLabel(label);
}
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Null<Array<String>>
{
var labels = this.anim.getFrameLabels(layer);
var array = [];
for (label in labels)
{
array.push(label.name);
}
return array;
}
function getNextFrameLabel(label:String):String
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
}
function getLabelIndex(label:String):Int
{
return listAnimations().indexOf(label);
}
function goToFrameIndex(index:Int):Void
{
this.anim.curFrame = index;
}
public function cleanupAnimation(_:String):Void
{
canPlayOtherAnims = true;
// this.currentAnimation = null;
this.anim.pause();
}
function _onAnimationFrame(frame:Int):Void
{
if (currentAnimation != null)
{
onAnimationFrame.dispatch(currentAnimation, frame);
if (isLoopComplete())
{
anim.pause();
if (looping)
{
anim.curFrame = (fr != null) ? fr.index : 0;
anim.resume();
_onAnimationLoop();
}
else if (fr != null && anim.curFrame != anim.length - 1)
{
anim.curFrame--;
_onAnimationComplete();
}
}
}
}
function _onAnimationComplete():Void
{
if (currentAnimation != null)
{
onAnimationComplete.dispatch(currentAnimation);
}
else
{
onAnimationComplete.dispatch('');
}
}
function _onAnimationLoop():Void
{
if (currentAnimation != null)
{
onAnimationLoop.dispatch(currentAnimation);
}
else
{
onAnimationLoop.dispatch('');
}
}
var prevFrames:Map<Int, FlxFrame> = [];
public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void
{
var cond = false;
if (graphic == null) cond = true;
else
{
if ((graphic is String)) cond = !Assets.exists(graphic)
else
cond = false;
}
if (cond)
{
var prevFrame:Null<FlxFrame> = prevFrames.get(index);
if (prevFrame == null) return;
prevFrame.copyTo(frames.getByIndex(index));
return;
}
var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo();
prevFrames.set(index, prevFrame);
@:nullSafety(Off) // TODO: Remove this once flixel.system.frontEnds.BitmapFrontEnd has been null safed
var frame = FlxG.bitmap.add(graphic).imageFrame.frame;
frame.copyTo(frames.getByIndex(index));
// Additional sizing fix.
@:privateAccess
if (true)
{
var frame = frames.getByIndex(index);
frame.tileMatrix.a = prevFrame.frame.width / frame.frame.width;
frame.tileMatrix.d = prevFrame.frame.height / frame.frame.height;
}
}
public function getBasePosition():Null<FlxPoint>
{
// var stagePos = new FlxPoint(anim.stageInstance.matrix.tx, anim.stageInstance.matrix.ty);
var instancePos = new FlxPoint(anim.curInstance.matrix.tx, anim.curInstance.matrix.ty);
var firstElement = anim.curSymbol.timeline?.get(0)?.get(0)?.get(0);
if (firstElement == null) return instancePos;
var firstElementPos = new FlxPoint(firstElement.matrix.tx, firstElement.matrix.ty);
return instancePos + firstElementPos;
}
public function getPivotPosition():Null<FlxPoint>
{
return anim.curInstance.symbol.transformationPoint;
}
public override function destroy():Void
{
for (prevFrameId in prevFrames.keys())
{
replaceFrameGraphic(prevFrameId, null);
}
super.destroy();
}
}

View file

@ -70,6 +70,11 @@ class MeshRender extends FlxStrip
add_tri(a, c, d);
}
public override function draw():Void
{
super.draw();
}
/**
* Build a quad from four points.
*

View file

@ -8,6 +8,7 @@ import funkin.Paths;
import funkin.Preferences;
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
import flixel.system.debug.watch.Tracker;
import haxe.ds.Option;
// These are great.
using Lambda;
@ -23,4 +24,5 @@ using funkin.util.tools.MapTools;
using funkin.util.tools.SongEventDataArrayTools;
using funkin.util.tools.SongNoteDataArrayTools;
using funkin.util.tools.StringTools;
using funkin.util.AnsiUtil;
#end

View file

@ -19,35 +19,20 @@ import flixel.math.FlxPoint;
*/
class Controls extends FlxActionSet
{
/**
/*
* A list of actions that a player would invoke via some input device.
* Uses FlxActions to funnel various inputs to a single action.
*/
var _ui_up = new FunkinAction(Action.UI_UP);
var _ui_left = new FunkinAction(Action.UI_LEFT);
var _ui_right = new FunkinAction(Action.UI_RIGHT);
var _ui_down = new FunkinAction(Action.UI_DOWN);
var _ui_upP = new FunkinAction(Action.UI_UP_P);
var _ui_leftP = new FunkinAction(Action.UI_LEFT_P);
var _ui_rightP = new FunkinAction(Action.UI_RIGHT_P);
var _ui_downP = new FunkinAction(Action.UI_DOWN_P);
var _ui_upR = new FunkinAction(Action.UI_UP_R);
var _ui_leftR = new FunkinAction(Action.UI_LEFT_R);
var _ui_rightR = new FunkinAction(Action.UI_RIGHT_R);
var _ui_downR = new FunkinAction(Action.UI_DOWN_R);
var _note_up = new FunkinAction(Action.NOTE_UP);
var _note_left = new FunkinAction(Action.NOTE_LEFT);
var _note_right = new FunkinAction(Action.NOTE_RIGHT);
var _note_down = new FunkinAction(Action.NOTE_DOWN);
var _note_upP = new FunkinAction(Action.NOTE_UP_P);
var _note_leftP = new FunkinAction(Action.NOTE_LEFT_P);
var _note_rightP = new FunkinAction(Action.NOTE_RIGHT_P);
var _note_downP = new FunkinAction(Action.NOTE_DOWN_P);
var _note_upR = new FunkinAction(Action.NOTE_UP_R);
var _note_leftR = new FunkinAction(Action.NOTE_LEFT_R);
var _note_rightR = new FunkinAction(Action.NOTE_RIGHT_R);
var _note_downR = new FunkinAction(Action.NOTE_DOWN_R);
var _accept = new FunkinAction(Action.ACCEPT);
var _back = new FunkinAction(Action.BACK);
var _pause = new FunkinAction(Action.PAUSE);
@ -142,26 +127,6 @@ class Controls extends FlxActionSet
inline function get_UI_DOWN_R()
return _ui_down.checkJustReleased();
public var UI_UP_GAMEPAD(get, never):Bool;
inline function get_UI_UP_GAMEPAD()
return _ui_up.checkPressedGamepad();
public var UI_LEFT_GAMEPAD(get, never):Bool;
inline function get_UI_LEFT_GAMEPAD()
return _ui_left.checkPressedGamepad();
public var UI_RIGHT_GAMEPAD(get, never):Bool;
inline function get_UI_RIGHT_GAMEPAD()
return _ui_right.checkPressedGamepad();
public var UI_DOWN_GAMEPAD(get, never):Bool;
inline function get_UI_DOWN_GAMEPAD()
return _ui_down.checkPressedGamepad();
public var NOTE_UP(get, never):Bool;
inline function get_NOTE_UP()
@ -225,22 +190,62 @@ class Controls extends FlxActionSet
public var ACCEPT(get, never):Bool;
inline function get_ACCEPT()
return _accept.check();
return _accept.checkPressed();
public var ACCEPT_P(get, never):Bool;
inline function get_ACCEPT_P()
return _accept.checkJustPressed();
public var ACCEPT_R(get, never):Bool;
inline function get_ACCEPT_R()
return _accept.checkJustReleased();
public var BACK(get, never):Bool;
inline function get_BACK()
return _back.check();
return _back.checkPressed();
public var BACK_P(get, never):Bool;
inline function get_BACK_P()
return _back.checkJustPressed();
public var BACK_R(get, never):Bool;
inline function get_BACK_R()
return _back.checkJustReleased();
public var PAUSE(get, never):Bool;
inline function get_PAUSE()
return _pause.check();
return _pause.checkPressed();
public var PAUSE_P(get, never):Bool;
inline function get_PAUSE_P()
return _pause.checkJustPressed();
public var PAUSE_R(get, never):Bool;
inline function get_PAUSE_R()
return _pause.checkJustReleased();
public var RESET(get, never):Bool;
inline function get_RESET()
return _reset.check();
return _reset.checkPressed();
public var RESET_P(get, never):Bool;
inline function get_RESET_P()
return _reset.checkJustPressed();
public var RESET_R(get, never):Bool;
inline function get_RESET_R()
return _reset.checkJustReleased();
public var WINDOW_FULLSCREEN(get, never):Bool;
@ -503,9 +508,8 @@ class Controls extends FlxActionSet
* Calls a function passing each action bound by the specified control
* @param control
* @param func
* @return ->Void)
*/
function forEachBound(control:Control, func:FlxActionDigital->FlxInputState->Void)
function forEachBound(control:Control, func:FunkinAction->FlxInputState->Void):Void
{
switch (control)
{
@ -542,13 +546,21 @@ class Controls extends FlxActionSet
func(_note_down, JUST_PRESSED);
func(_note_down, JUST_RELEASED);
case ACCEPT:
func(_accept, PRESSED);
func(_accept, JUST_PRESSED);
func(_accept, JUST_RELEASED);
case BACK:
func(_back, PRESSED);
func(_back, JUST_PRESSED);
func(_back, JUST_RELEASED);
case PAUSE:
func(_pause, PRESSED);
func(_pause, JUST_PRESSED);
func(_pause, JUST_RELEASED);
case RESET:
func(_reset, PRESSED);
func(_reset, JUST_PRESSED);
func(_reset, JUST_RELEASED);
#if FEATURE_SCREENSHOTS
case WINDOW_SCREENSHOT:
func(_window_screenshot, JUST_PRESSED);
@ -592,7 +604,7 @@ class Controls extends FlxActionSet
}
}
public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int)
public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int):Void
{
if (toAdd == toRemove) return;
@ -606,7 +618,7 @@ class Controls extends FlxActionSet
}
}
function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState)
function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState):Void
{
if (action.inputs.length == 0)
{
@ -657,7 +669,7 @@ class Controls extends FlxActionSet
}
}
function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState)
function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState):Void
{
if (action.inputs.length == 0)
{
@ -685,7 +697,7 @@ class Controls extends FlxActionSet
}
}
public function copyFrom(controls:Controls, ?device:Device)
public function copyFrom(controls:Controls, ?device:Device):Void
{
for (name in controls.byName.keys())
{
@ -712,7 +724,7 @@ class Controls extends FlxActionSet
}
}
inline public function copyTo(controls:Controls, ?device:Device)
inline public function copyTo(controls:Controls, ?device:Device):Void
{
controls.copyFrom(this, device);
}
@ -735,7 +747,7 @@ class Controls extends FlxActionSet
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
*/
public function bindKeys(control:Control, keys:Array<FlxKey>)
public function bindKeys(control:Control, keys:Array<FlxKey>):Void
{
forEachBound(control, function(action, state) addKeys(action, keys, state));
}
@ -744,12 +756,12 @@ class Controls extends FlxActionSet
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
*/
public function unbindKeys(control:Control, keys:Array<FlxKey>)
public function unbindKeys(control:Control, keys:Array<FlxKey>):Void
{
forEachBound(control, function(action, _) removeKeys(action, keys));
}
static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState)
static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState):Void
{
for (key in keys)
{
@ -758,7 +770,7 @@ class Controls extends FlxActionSet
}
}
static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>)
static function removeKeys(action:FlxActionDigital, keys:Array<FlxKey>):Void
{
var i = action.inputs.length;
while (i-- > 0)
@ -919,7 +931,7 @@ class Controls extends FlxActionSet
return [];
}
function removeKeyboard()
function removeKeyboard():Void
{
for (action in this.digitalActions)
{
@ -939,16 +951,6 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id));
}
public function getGamepadIds():Array<Int>
{
return gamepadsAdded;
}
public function getGamepads():Array<FlxGamepad>
{
return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
}
inline function addGamepadLiteral(id:Int, ?buttonMap:Map<Control, Array<FlxGamepadInputID>>):Void
{
gamepadsAdded.push(id);
@ -972,7 +974,7 @@ class Controls extends FlxActionSet
gamepadsAdded.remove(deviceID);
}
public function addDefaultGamepad(id):Void
public function addDefaultGamepad(id:Int):Void
{
addGamepadLiteral(id, [
Control.ACCEPT => getDefaultGamepadBinds(Control.ACCEPT),
@ -1016,32 +1018,32 @@ class Controls extends FlxActionSet
function getDefaultGamepadBinds(control:Control):Array<FlxGamepadInputID>
{
switch (control)
return switch (control)
{
case Control.ACCEPT:
return [#if switch B #else A #end];
[A];
case Control.BACK:
return [#if switch A #else B #end];
[B];
case Control.UI_UP:
return [DPAD_UP, LEFT_STICK_DIGITAL_UP];
[DPAD_UP, LEFT_STICK_DIGITAL_UP];
case Control.UI_DOWN:
return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
[DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
case Control.UI_LEFT:
return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
[DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
case Control.UI_RIGHT:
return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
[DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
case Control.NOTE_UP:
return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
[DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
case Control.NOTE_DOWN:
return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
[DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
case Control.NOTE_LEFT:
return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
[DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
case Control.NOTE_RIGHT:
return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
[DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
case Control.PAUSE:
return [START];
[START];
case Control.RESET:
return [FlxGamepadInputID.BACK]; // Back (i.e. Select)
[FlxGamepadInputID.BACK]; // Back (i.e. Select)
case Control.WINDOW_FULLSCREEN:
[];
#if FEATURE_SCREENSHOTS
@ -1049,19 +1051,19 @@ class Controls extends FlxActionSet
[];
#end
case Control.CUTSCENE_ADVANCE:
return [A];
[A];
case Control.FREEPLAY_FAVORITE:
return [Y]; // Back (i.e. Select)
[Y]; // Back (i.e. Select)
case Control.FREEPLAY_LEFT:
return [LEFT_SHOULDER];
[LEFT_SHOULDER];
case Control.FREEPLAY_RIGHT:
return [RIGHT_SHOULDER];
[RIGHT_SHOULDER];
case Control.FREEPLAY_CHAR_SELECT:
return [X];
[X];
case Control.FREEPLAY_JUMP_TO_TOP:
return [RIGHT_STICK_DIGITAL_UP];
[RIGHT_STICK_DIGITAL_UP];
case Control.FREEPLAY_JUMP_TO_BOTTOM:
return [RIGHT_STICK_DIGITAL_DOWN];
[RIGHT_STICK_DIGITAL_DOWN];
case Control.VOLUME_UP:
[];
case Control.VOLUME_DOWN:
@ -1083,16 +1085,15 @@ class Controls extends FlxActionSet
case Control.DEBUG_DISPLAY:
[];
default:
// Fallthrough.
[];
}
return [];
}
/**
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
*/
public function bindButtons(control:Control, id, buttons)
public function bindButtons(control:Control, id:Int, buttons):Void
{
forEachBound(control, function(action, state) addButtons(action, buttons, state, id));
}
@ -1101,12 +1102,12 @@ class Controls extends FlxActionSet
* Sets all actions that pertain to the binder to trigger when the supplied keys are used.
* If binder is a literal you can inline this
*/
public function unbindButtons(control:Control, gamepadID:Int, buttons)
public function unbindButtons(control:Control, gamepadID:Int, buttons):Void
{
forEachBound(control, function(action, _) removeButtons(action, gamepadID, buttons));
}
inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id)
inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id:Int):Void
{
for (button in buttons)
{
@ -1115,7 +1116,7 @@ class Controls extends FlxActionSet
}
}
static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>)
static function removeButtons(action:FlxActionDigital, gamepadID:Int, buttons:Array<FlxGamepadInputID>):Void
{
var i = action.inputs.length;
while (i-- > 0)
@ -1145,17 +1146,6 @@ class Controls extends FlxActionSet
return list;
}
public function removeDevice(device:Device)
{
switch (device)
{
case Keys:
setKeyboardScheme(None);
case Gamepad(id):
removeGamepad(id);
}
}
/**
* NOTE: When loading controls:
* An EMPTY array means the control is uninitialized and needs to be reset to default.
@ -1238,7 +1228,7 @@ class Controls extends FlxActionSet
return isEmpty ? null : data;
}
static function isDevice(input:FlxActionInput, device:Device)
static function isDevice(input:FlxActionInput, device:Device):Bool
{
return switch (device)
{
@ -1247,7 +1237,7 @@ class Controls extends FlxActionSet
}
}
inline static function isGamepad(input:FlxActionInput, deviceID:Int)
inline static function isGamepad(input:FlxActionInput, deviceID:Int):Bool
{
return input.device == GAMEPAD && (deviceID == FlxInputDeviceID.ALL || input.deviceID == deviceID);
}
@ -1361,14 +1351,8 @@ class FunkinAction extends FlxActionDigital
public function checkMultiFiltered(?filterTriggers:Array<FlxInputState>, ?filterDevices:Array<FlxInputDevice>):Bool
{
if (filterTriggers == null)
{
filterTriggers = [PRESSED, JUST_PRESSED];
}
if (filterDevices == null)
{
filterDevices = [];
}
filterTriggers ??= [PRESSED, JUST_PRESSED];
filterDevices ??= [];
// Perform checkFiltered for each combination.
for (i in filterTriggers)
@ -1399,11 +1383,10 @@ class FunkinAction extends FlxActionDigital
* @param action The action to check for.
* @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`).
* @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`).
* @return bool if our input has been triggered
*/
public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool
{
// The normal
// Make sure we only update the inputs once per frame.
var key = '${filterTrigger}:${filterDevice}';
var cacheEntry = cache.get(key);
@ -1412,20 +1395,22 @@ class FunkinAction extends FlxActionDigital
{
return cacheEntry.value;
}
// Use a for loop instead so we can remove inputs while iterating.
// We don't return early because we need to call check() on ALL inputs.
var result = false;
var len = inputs != null ? inputs.length : 0;
for (i in 0...len)
_x = null;
_y = null;
_timestamp = FlxG.game.ticks;
triggered = false;
var i = inputs?.length ?? 0;
while (i-- > 0) // Iterate backwards, since we may remove items
{
var j = len - i - 1;
var input = inputs[j];
var input = inputs[i];
// Filter out dead inputs.
if (input.destroyed)
{
inputs.splice(j, 1);
inputs.remove(input);
continue;
}
@ -1447,14 +1432,13 @@ class FunkinAction extends FlxActionDigital
// Check whether the input has triggered.
if (input.check(this))
{
result = true;
triggered = true;
}
}
// We need to cache this result.
cache.set(key, {timestamp: FlxG.game.ticks, value: result});
cache.set(key, {timestamp: FlxG.game.ticks, value: triggered});
return result;
return triggered;
}
}
@ -1475,10 +1459,10 @@ enum Control
UI_DOWN;
UI_UP;
UI_RIGHT;
RESET;
ACCEPT;
BACK;
PAUSE;
RESET;
// CUTSCENE
CUTSCENE_ADVANCE;
// FREEPLAY
@ -1509,27 +1493,11 @@ enum abstract Action(String) to String from String
var NOTE_LEFT = "note_left";
var NOTE_RIGHT = "note_right";
var NOTE_DOWN = "note_down";
var NOTE_UP_P = "note_up-press";
var NOTE_LEFT_P = "note_left-press";
var NOTE_RIGHT_P = "note_right-press";
var NOTE_DOWN_P = "note_down-press";
var NOTE_UP_R = "note_up-release";
var NOTE_LEFT_R = "note_left-release";
var NOTE_RIGHT_R = "note_right-release";
var NOTE_DOWN_R = "note_down-release";
// UI
var UI_UP = "ui_up";
var UI_LEFT = "ui_left";
var UI_RIGHT = "ui_right";
var UI_DOWN = "ui_down";
var UI_UP_P = "ui_up-press";
var UI_LEFT_P = "ui_left-press";
var UI_RIGHT_P = "ui_right-press";
var UI_DOWN_P = "ui_down-press";
var UI_UP_R = "ui_up-release";
var UI_LEFT_R = "ui_left-release";
var UI_RIGHT_R = "ui_right-release";
var UI_DOWN_R = "ui_down-release";
var ACCEPT = "accept";
var BACK = "back";
var PAUSE = "pause";

View file

@ -303,7 +303,8 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
onInputPressed.dispatch(
{
noteDirection: getDirectionForKey(key),
timestamp: timestamp
timestamp: timestamp,
keyCode: keyCode
});
_dirPressTimestamps.set(getDirectionForKey(key), timestamp);
}
@ -325,7 +326,8 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
onInputReleased.dispatch(
{
noteDirection: getDirectionForKey(key),
timestamp: timestamp
timestamp: timestamp,
keyCode: keyCode
});
_dirReleaseTimestamps.set(getDirectionForKey(key), timestamp);
}
@ -349,7 +351,8 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
onInputPressed.dispatch(
{
noteDirection: getDirectionForButton(gamepad, buttonId),
timestamp: timestamp
timestamp: timestamp,
keyCode: button // implicit cast to int
});
_dirPressTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
}
@ -373,7 +376,8 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
onInputReleased.dispatch(
{
noteDirection: getDirectionForButton(gamepad, buttonId),
timestamp: timestamp
timestamp: timestamp,
keyCode: button // implicit cast to int
});
_dirReleaseTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
}
@ -491,4 +495,10 @@ typedef PreciseInputEvent =
* The timestamp of the input. Measured in nanoseconds.
*/
timestamp:Int64,
/**
* The key that was used for the input.
* Used to distinguish between multiple inputs for the same direction.
*/
keyCode:Int
};

View file

@ -55,6 +55,7 @@ class TurboButtonHandler extends FlxBasic
function get_allPressed():Bool
{
if (targetGamepad == null) return false;
if (!targetGamepad.connected) return false;
if (inputs == null || inputs.length == 0) return false;
if (inputs.length == 1) return targetGamepad.anyPressed(inputs);

View file

@ -10,6 +10,9 @@ import funkin.mobile.ui.FunkinHitbox;
import funkin.play.notes.NoteDirection;
import openfl.events.KeyboardEvent;
import openfl.events.TouchEvent;
#if android
import funkin.external.android.KeyboardUtil;
#end
/**
* Handles setting up and managing input controls for the game.
@ -125,7 +128,7 @@ class ControlsHandler
@:noCompletion
private static function get_hasExternalInputDevice():Bool
{
return FlxG.gamepads.numActiveGamepads > 0 #if android || extension.androidtools.Tools.isChromebook() #end;
return FlxG.gamepads.numActiveGamepads > 0 #if android || KeyboardUtil.keyboardConnected || extension.androidtools.Tools.isChromebook() #end;
}
@:noCompletion

View file

@ -34,7 +34,7 @@ class PreciseInputHandler
@:privateAccess
if (hint.input?.justPressed ?? false)
{
PreciseInputManager.instance.onInputPressed.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp});
PreciseInputManager.instance.onInputPressed.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp, keyCode: 0});
PreciseInputManager.instance._dirPressTimestamps.set(hint.noteDirection, timestamp);
}
}
@ -50,7 +50,7 @@ class PreciseInputHandler
@:privateAccess
if (hint.input?.justReleased ?? false)
{
PreciseInputManager.instance.onInputReleased.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp});
PreciseInputManager.instance.onInputReleased.dispatch({noteDirection: hint.noteDirection, timestamp: timestamp, keyCode: 0});
PreciseInputManager.instance._dirPressTimestamps.set(hint.noteDirection, timestamp);
}
}

View file

@ -63,16 +63,16 @@ class PolymodErrorHandler
static function logInfo(message:String):Void
{
trace('[INFO-] ${message}');
trace(' INFO '.bg_blue().bold() + ' ${message}');
}
static function logError(message:String):Void
{
trace('[ERROR] ${message}');
trace(' ERROR '.bg_red().bold() + ' ${message}');
}
static function logWarn(message:String):Void
{
trace('[WARN-] ${message}');
trace(' WARNING '.bg_yellow().bold() + ' ${message}');
}
}

View file

@ -105,7 +105,7 @@ class PolymodHandler
createModRoot();
#end
trace('Initializing Polymod (using configured mods)...');
loadModsById(Save.instance.enabledModIds);
loadModsById(Save.instance.enabledModIds.value);
}
/**
@ -194,7 +194,7 @@ class PolymodHandler
loadedModIds = [];
for (mod in loadedModList)
{
trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
loadedModIds.push(mod.id);
}
@ -203,35 +203,35 @@ class PolymodHandler
trace('Installed mods have replaced ${fileList.length} images.');
for (item in fileList)
{
trace(' * $item');
trace(' * $item');
}
fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
trace('Installed mods have added/replaced ${fileList.length} text files.');
for (item in fileList)
{
trace(' * $item');
trace(' * $item');
}
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
trace('Installed mods have replaced ${fileList.length} music files.');
for (item in fileList)
{
trace(' * $item');
trace(' * $item');
}
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
trace('Installed mods have replaced ${fileList.length} sound files.');
for (item in fileList)
{
trace(' * $item');
trace(' * $item');
}
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
trace('Installed mods have replaced ${fileList.length} generic audio files.');
for (item in fileList)
{
trace(' * $item');
trace(' * $item');
}
#end
}
@ -274,6 +274,11 @@ class PolymodHandler
Polymod.addImportAlias('funkin.play.character.CharacterDataParser', funkin.data.character.CharacterData.CharacterDataParser);
Polymod.addImportAlias('funkin.play.character.CharacterData.CharacterDataParser', funkin.data.character.CharacterData.CharacterDataParser);
// `FlxAtlasSprite` was merged into `FunkinSprite` and then removed.
// We add the import alias here so mods don't error out as much.
Polymod.addImportAlias('funkin.graphics.adobeanimate.FlxAtlasSprite', funkin.graphics.FunkinSprite);
Polymod.addImportAlias('funkin.modding.base.ScriptedFlxAtlasSprite', funkin.graphics.ScriptedFunkinSprite);
// `funkin.util.FileUtil` has unrestricted access to the file system.
Polymod.addImportAlias('funkin.util.FileUtil', funkin.util.FileUtilSandboxed);
@ -542,7 +547,7 @@ class PolymodHandler
*/
public static function getEnabledMods():Array<ModMetadata>
{
var modIds:Array<String> = Save.instance.enabledModIds;
var modIds:Array<String> = Save.instance.enabledModIds.value;
var modMetadata:Array<ModMetadata> = getAllMods();
var enabledMods:Array<ModMetadata> = [];
for (item in modMetadata)

View file

@ -1,8 +0,0 @@
package funkin.modding.base;
/**
* A script that can be tied to an FlxAtlasSprite
* Create a scripted class that extends FlxAtlasSprite to use this.
*/
@:hscriptClass
class ScriptedFlxAtlasSprite extends funkin.graphics.adobeanimate.FlxAtlasSprite implements HScriptedClass {}

View file

@ -29,20 +29,20 @@ class ModuleHandler
trace("[MODULEHANDLER] Loading module cache...");
var scriptedModuleClassNames:Array<String> = ScriptedModule.listScriptClasses();
trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
for (moduleCls in scriptedModuleClassNames)
{
var module:Module = ScriptedModule.init(moduleCls, moduleCls);
if (module != null)
{
trace(' Loaded module: ${moduleCls}');
trace(' Loaded module: ${moduleCls}');
// Then store it.
addToModuleCache(module);
}
else
{
trace(' Failed to instantiate module: ${moduleCls}');
trace(' Failed to instantiate module: ${moduleCls}');
}
}
reorderModuleCache();

View file

@ -273,7 +273,7 @@ class GameOverSubState extends MusicBeatSubState
//
// Restart the level when pressing the assigned key.
if ((controls.ACCEPT #if mobile || (TouchUtil.pressAction() && !TouchUtil.overlaps(backButton) && canInput) #end)
if ((controls.ACCEPT_P #if mobile || (TouchUtil.pressAction() && !TouchUtil.overlaps(backButton) && canInput) #end)
&& blueballed
&& !mustNotExit)
{
@ -281,7 +281,7 @@ class GameOverSubState extends MusicBeatSubState
confirmDeath();
}
if (controls.BACK && !mustNotExit && !isEnding) goBack();
if (controls.BACK_P && !mustNotExit && !isEnding) goBack();
if (gameOverMusic != null && gameOverMusic.playing)
{

View file

@ -76,12 +76,13 @@ class GitarooPause extends MusicBeatState
{
if (controls.UI_LEFT_P || controls.UI_RIGHT_P #if mobile || SwipeUtil.justSwipedLeft || SwipeUtil.justSwipedRight #end) changeThing();
if (controls.ACCEPT #if mobile || checkSelectionPress() #end)
if (controls.ACCEPT_P #if mobile || checkSelectionPress() #end)
{
if (replaySelect)
{
FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false;
if (funkin.ui.FullScreenScaleMode.instance != null) funkin.ui.FullScreenScaleMode.instance.onMeasurePostAwait();
FlxG.switchState(() -> new PlayState(previousParams));
}
else

View file

@ -37,6 +37,11 @@ typedef PauseSubStateParams =
* Which mode to start in. Dictates what entries are displayed.
*/
?mode:PauseMode,
/**
* Whether the game paused because the window lost focus.
*/
?lostFocus:Bool
};
/**
@ -151,6 +156,11 @@ class PauseSubState extends MusicBeatSubState
*/
var currentMode:PauseMode;
/**
* Whether the game paused because the window lost focus.
*/
var lostFocus:Bool = false;
// ===============
// Graphics Variables
// ===============
@ -231,6 +241,7 @@ class PauseSubState extends MusicBeatSubState
{
super();
this.currentMode = params?.mode ?? Standard;
this.lostFocus = params?.lostFocus ?? false;
this.onPause = onPause;
}
@ -256,6 +267,8 @@ class PauseSubState extends MusicBeatSubState
startPauseMusic();
if (lostFocus && Preferences.autoPause) pauseMusic.pause();
buildBackground();
buildMetadata();
@ -338,7 +351,7 @@ class PauseSubState extends MusicBeatSubState
function startPauseMusic():Void
{
var pauseMusicPath:String = Paths.music('breakfast$musicSuffix/breakfast$musicSuffix');
pauseMusic = FunkinSound.load(pauseMusicPath, true, true);
pauseMusic = FunkinSound.load(pauseMusicPath, 0, true, true);
if (pauseMusic == null)
{
@ -350,6 +363,24 @@ class PauseSubState extends MusicBeatSubState
pauseMusic.fadeIn(MUSIC_FADE_IN_TIME, 0, MUSIC_FINAL_VOLUME);
}
/**
* Called when the game loses focus. Used to temporarily pause the sound.
*/
public override function onFocusLost():Void
{
super.onFocusLost();
if (Preferences.autoPause) pauseMusic.pause();
}
/**
* Called when the game loses focus. Used to temporarily pause the sound.
*/
public override function onFocus():Void
{
super.onFocus();
if (Preferences.autoPause) pauseMusic.resume();
}
/**
* Render the semi-transparent black background.
*/
@ -581,66 +612,44 @@ class PauseSubState extends MusicBeatSubState
{
if (!allowInput) return;
// Doing this just so it'd look better i guess.
final upP:Bool = controls.UI_UP_P;
final downP:Bool = controls.UI_DOWN_P;
// early return here if we are modifying our offsets stuff w/ shift + up/down
if (handleModifyingOffsets()) return;
#if !mobile
final up:Bool = controls.UI_UP;
final down:Bool = controls.UI_DOWN;
var offset:Int = Preferences.globalOffset ?? 0;
if (FlxG.keys.pressed.SHIFT && (up || down))
{
lastOffsetPress += FlxG.elapsed;
if (!fastOffset)
{
// If the last offset press was more than 0.5 seconds ago, reset the fast offset.
if (lastOffsetPress > 0.5)
{
fastOffset = true;
lastOffsetPress = 0;
}
handleDebugInputs();
if (upP || downP)
{
offset += (upP || up) ? 1 : -1;
offsetText.text = 'Global Offset: ${offset}ms';
}
}
else
{
offset += (upP || up) ? 1 : -1;
offsetText.text = 'Global Offset: ${offset}ms';
}
if (offset > 1500) offset = 1500;
if (offset < -1500) offset = -1500;
Preferences.globalOffset = offset;
return;
}
else
{
// Reset the fast offset if the user is not holding SHIFT.
fastOffset = false;
lastOffsetPress = 0;
}
#end
if (upP)
if (controls.UI_UP_P)
{
changeSelection(-1);
}
if (downP)
if (controls.UI_DOWN_P)
{
changeSelection(1);
}
// we only want justOpened to be true for 1 single frame, when we first get into the pause menu substate
// we early return here so we don't need to check `if (!justOpened)` everywhere
if (justOpened)
{
justOpened = false;
return;
}
handleTouchInputs();
if (controls.ACCEPT_P && currentMenuEntries.length > 0)
{
currentMenuEntries[currentEntry].callback(this);
}
else if (controls.PAUSE_P)
{
resume(this);
}
}
function handleTouchInputs():Void
{
#if FEATURE_TOUCH_CONTROLS
if (!SwipeUtil.justSwipedAny && !justOpened && currentMenuEntries.length > 0)
if (!SwipeUtil.justSwipedAny && currentMenuEntries.length > 0)
{
for (i in 0...menuEntryText.members.length)
{
@ -660,17 +669,62 @@ class PauseSubState extends MusicBeatSubState
}
}
#end
}
if (controls.ACCEPT && currentMenuEntries.length > 0)
/**
* used to both modify/change offsets, but also to early return so we don't interfere with other inputs while doing so
* TODO: refactor to use state design pattern to handle inputs, see MainMenuState
* @return Bool true if we are currently modifying our offsets (by holding shift and pressing UP or DOWN)
*/
function handleModifyingOffsets():Bool
{
#if !mobile
var offset:Int = Preferences.globalOffset ?? 0;
if (FlxG.keys.pressed.SHIFT && (controls.UI_UP || controls.UI_DOWN))
{
currentMenuEntries[currentEntry].callback(this);
lastOffsetPress += FlxG.elapsed;
if (!fastOffset)
{
// If the last offset press was more than 0.5 seconds ago, reset the fast offset.
if (lastOffsetPress > 0.5)
{
fastOffset = true;
lastOffsetPress = 0;
}
if (controls.UI_UP_P || controls.UI_DOWN_P)
{
offset += (controls.UI_UP_P || controls.UI_UP) ? 1 : -1;
offsetText.text = 'Global Offset: ${offset}ms';
}
}
else
{
offset += (controls.UI_UP_P || controls.UI_UP) ? 1 : -1;
offsetText.text = 'Global Offset: ${offset}ms';
}
if (offset > 1500) offset = 1500;
if (offset < -1500) offset = -1500;
Preferences.globalOffset = offset;
return true;
}
else if (controls.PAUSE && !justOpened)
else
{
resume(this);
// Reset the fast offset if the user is not holding SHIFT.
fastOffset = false;
lastOffsetPress = 0;
}
// we only want justOpened to be true for 1 single frame, when we first get into the pause menu substate
justOpened = false;
#end
return false;
}
function handleDebugInputs():Void
{
#if FEATURE_DEBUG_FUNCTIONS
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)

View file

@ -54,7 +54,12 @@ import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.play.stage.Stage;
import funkin.save.Save;
#if FEATURE_CHART_EDITOR
import funkin.ui.debug.charting.ChartEditorState;
#end
#if FEATURE_STAGE_EDITOR
import funkin.ui.debug.stageeditor.StageEditorState;
#end
import funkin.ui.debug.stage.StageOffsetSubState;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatSubState;
@ -333,6 +338,18 @@ class PlayState extends MusicBeatSubState
*/
public var isInCountdown:Bool = false;
/**
* Determines whether opening a substate over this causes the game to pause.
* Enable it before opening a Pause menu or Game Over screen, and disable it
* for stuff like editors and overlays.
*/
public var shouldSubstatePause:Bool = false;
/**
* Whether the game is currently in the Game Over state.
*/
public var isGameOverState:Bool = false;
/**
* Whether the game is currently in Practice Mode.
* If true, player will not gain or lose score from notes.
@ -794,6 +811,10 @@ class PlayState extends MusicBeatSubState
// This state receives draw calls even when a substate is active.
this.persistentDraw = true;
// Make the player unable to pause if they're moving from the chart editor while the focus is still on since the input persists.
@:privateAccess
justUnpaused = isChartingMode && !FlxG.game._lostFocus;
// Stop any pre-existing music.
if (!overrideMusic)
{
@ -1128,7 +1149,7 @@ class PlayState extends MusicBeatSubState
{
// Fallback to properly update the conductor incase the lerp messed up
// Shouldn't be fallen back to unless you're lagging alot
trace('[WARNING] Normal Conductor Update!! are you lagging?');
trace(' WARNING '.bg_yellow().bold() + ' Normal Conductor Update!! are you lagging?');
Conductor.instance.update();
}
}
@ -1146,7 +1167,7 @@ class PlayState extends MusicBeatSubState
#end
// Attempt to pause the game.
if ((controls.PAUSE || androidPause || pauseButtonCheck)) pause();
if ((controls.PAUSE_P || androidPause || pauseButtonCheck)) pause();
#if mobile
if (justUnpaused)
@ -1168,14 +1189,17 @@ class PlayState extends MusicBeatSubState
if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX;
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
// Apply camera zoom + multipliers.
if (subState == null && cameraZoomRate > 0.0) // && !isInCutscene)
{
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier.
if (!debugUnbindCameraZoom) FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera.
var decayRate:Float = 0.95;
var dt:Float = elapsed * 60; //
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
if (subState == null && cameraZoomRate > 0.0)
{
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, Math.pow(decayRate, dt));
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier;
if (!debugUnbindCameraZoom) FlxG.camera.zoom = zoomPlusBop;
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, Math.pow(decayRate, dt));
}
if (currentStage != null && currentStage.getBoyfriend() != null)
@ -1295,7 +1319,12 @@ class PlayState extends MusicBeatSubState
#end
}
function pause(mode:PauseMode = Standard):Void
/**
* Pause the game.
* @param mode Which set of pause menu options to display (distinguishes between standard, charting, and cutscene)
* @param lostFocus Whether the game paused because the window lost focus
*/
function pause(mode:PauseMode = Standard, lostFocus:Bool = false):Void
{
if (!mayPauseGame || justUnpaused || isGamePaused || isPlayerDying) return;
@ -1303,11 +1332,11 @@ class PlayState extends MusicBeatSubState
{
case Conversation:
preparePauseUI();
openPauseSubState(Conversation, camPause, () -> currentConversation?.pauseMusic());
openPauseSubState(Conversation, camPause, lostFocus, () -> currentConversation?.pauseMusic());
case Cutscene:
preparePauseUI();
openPauseSubState(Cutscene, camPause, () -> VideoCutscene.pauseVideo());
openPauseSubState(Cutscene, camPause, lostFocus, () -> VideoCutscene.pauseVideo());
default: // also known as standard
if (!isInCountdown || isInCutscene) return;
@ -1320,6 +1349,7 @@ class PlayState extends MusicBeatSubState
if (!event.eventCanceled)
{
shouldSubstatePause = true;
persistentUpdate = false;
persistentDraw = true;
@ -1338,7 +1368,7 @@ class PlayState extends MusicBeatSubState
boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
}
openPauseSubState(isChartingMode ? Charting : Standard, camPause);
openPauseSubState(isChartingMode ? Charting : Standard, camPause, lostFocus);
}
#if FEATURE_DISCORD_RPC
@ -1365,9 +1395,9 @@ class PlayState extends MusicBeatSubState
#end
}
function openPauseSubState(mode:PauseMode, cam:FlxCamera, ?onPause:Void->Void):Void
function openPauseSubState(mode:PauseMode, cam:FlxCamera, lostFocus:Bool = false, ?onPause:Void->Void):Void
{
final pauseSubState = new PauseSubState({mode: mode}, onPause);
final pauseSubState = new PauseSubState({mode: mode, lostFocus: lostFocus}, onPause);
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
pauseSubState.camera = cam;
@ -1397,6 +1427,8 @@ class PlayState extends MusicBeatSubState
iconP2?.updatePosition();
}
isGameOverState = true;
shouldSubstatePause = true;
// Transition to the game over substate.
var gameOverSubState = new GameOverSubState(
{
@ -1471,11 +1503,7 @@ class PlayState extends MusicBeatSubState
*/
public override function openSubState(subState:FlxSubState):Void
{
// If there is a substate which requires the game to continue,
// then make this a condition.
var shouldPause:Bool = (Std.isOfType(subState, PauseSubState) || Std.isOfType(subState, GameOverSubState));
if (shouldPause)
if (shouldSubstatePause)
{
// Pause the music.
if (FlxG.sound.music != null)
@ -1554,21 +1582,48 @@ class PlayState extends MusicBeatSubState
*/
public override function closeSubState():Void
{
if (Std.isOfType(subState, PauseSubState))
if (shouldSubstatePause)
{
shouldSubstatePause = false;
var event:ScriptEvent = new ScriptEvent(RESUME, true);
dispatchEvent(event);
if (event.eventCanceled) return;
// Pause any sounds that are playing and keep track of them.
// Vocals are also paused here but are not included as they are handled separately.
if (!isGameOverState)
{
FlxG.sound.list.forEachAlive(function(sound:FlxSound) {
if (!sound.active || sound == FlxG.sound.music) return;
// In case it's a scheduled sound
if (Std.isOfType(sound, FunkinSound))
{
var funkinSound:FunkinSound = cast sound;
if (funkinSound != null && !funkinSound.isPlaying) return;
}
if (!sound.playing && sound.time >= 0) return;
sound.pause();
soundsPausedBySubState.add(sound);
});
vocals?.forEach(function(voice:FunkinSound) {
soundsPausedBySubState.remove(voice);
});
}
else
{
vocals?.pause();
}
// Resume vwooshTimer
if (!vwooshTimer.finished) vwooshTimer.active = true;
// Resume music if we paused it.
if (musicPausedBySubState)
{
FlxG.sound.music.play();
if (FlxG.sound.music != null) FlxG.sound.music.play();
musicPausedBySubState = false;
}
@ -1629,10 +1684,7 @@ class PlayState extends MusicBeatSubState
justUnpaused = true;
}
else if (Std.isOfType(subState, Transition))
{
// Do nothing.
}
isGameOverState = false;
super.closeSubState();
}
@ -1708,15 +1760,15 @@ class PlayState extends MusicBeatSubState
{
if (currentConversation != null)
{
pause(Conversation);
pause(Conversation, true);
}
else if (VideoCutscene.isPlaying())
{
pause(Cutscene);
pause(Cutscene, true);
}
else
{
pause();
pause(true);
}
}
super.onFocusLost();
@ -2634,8 +2686,6 @@ class PlayState extends MusicBeatSubState
*/
function onKeyRelease(event:PreciseInputEvent):Void
{
if (isGamePaused) return;
// Do the minimal possible work here.
inputReleaseQueue.push(event);
}
@ -2873,7 +2923,10 @@ class PlayState extends MusicBeatSubState
var input:Null<PreciseInputEvent> = inputPressQueue.shift();
if (input == null) continue;
playerStrumline.pressKey(input.noteDirection);
// Whether this direction is already held by another key.
var isAlreadyHeld = playerStrumline.isKeyHeld(input.noteDirection);
playerStrumline.pressKey(input.noteDirection, input.keyCode);
// Don't credit or penalize inputs in Bot Play.
if (isBotPlayMode) continue;
@ -2881,9 +2934,9 @@ class PlayState extends MusicBeatSubState
var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection];
#if FEATURE_GHOST_TAPPING
if ((!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
if ((!playerStrumline.mayGhostTap()) && notesInDirection.length == 0 && !isAlreadyHeld)
#else
if (notesInDirection.length == 0)
if (notesInDirection.length == 0 && !isAlreadyHeld)
#end
{
// Pressed a wrong key with no notes nearby.
@ -2929,7 +2982,7 @@ class PlayState extends MusicBeatSubState
// Play the strumline animation.
playerStrumline.playStatic(input.noteDirection);
playerStrumline.releaseKey(input.noteDirection);
playerStrumline.releaseKey(input.noteDirection, input.keyCode);
}
playerStrumline.noteVibrations.tryNoteVibration();
@ -3089,7 +3142,18 @@ class PlayState extends MusicBeatSubState
// hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!!
disableKeys = true;
persistentUpdate = false;
openSubState(new StageOffsetSubState());
// The strings have to be get like this otherwise it just NORs when setting the params?
// Or, none of the characters show up, in the case of the pico songs?
var bf:String = currentStage?.getBoyfriend()?.characterId ?? '';
var gf:String = currentStage?.getGirlfriend()?.characterId ?? '';
var dad:String = currentStage?.getDad()?.characterId ?? '';
FlxG.switchState(() -> new StageEditorState(
{
targetStageId: currentStageId,
targetBfChar: bf,
targetGfChar: gf,
targetDadChar: dad
}));
}
#end
@ -3255,7 +3319,7 @@ class PlayState extends MusicBeatSubState
{
currentConversation.advanceConversation();
}
else if ((controls.PAUSE || androidPause || pauseButtonCheck) && !justUnpaused)
else if ((controls.PAUSE_P || androidPause || pauseButtonCheck) && !justUnpaused)
{
pause(Conversation);
}
@ -3263,7 +3327,7 @@ class PlayState extends MusicBeatSubState
else if (VideoCutscene.isPlaying())
{
// This is a video cutscene.
if ((controls.PAUSE || androidPause || pauseButtonCheck) && !justUnpaused)
if ((controls.PAUSE_P || androidPause || pauseButtonCheck) && !justUnpaused)
{
pause(Cutscene);
}

View file

@ -20,11 +20,9 @@ import funkin.audio.FunkinSound;
import funkin.data.freeplay.player.PlayerData.PlayerResultsAnimationData;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.data.song.SongRegistry;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.graphics.FunkinCamera;
import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.modding.base.ScriptedFlxAtlasSprite;
import funkin.play.components.ClearPercentCounter;
import funkin.play.components.TallyCounter;
import funkin.play.scoring.Scoring;
@ -78,7 +76,7 @@ class ResultState extends MusicBeatSubState
var characterAtlasAnimations:Array<
{
sprite:FlxAtlasSprite,
sprite:FunkinSprite,
delay:Float,
forceLoop:Bool,
startFrameLabel:String,
@ -119,8 +117,8 @@ class ResultState extends MusicBeatSubState
// We build a lot of this stuff in the constructor, then place it in create().
// This prevents having to do `null` checks everywhere.
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890().-";
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 61)));
songName.text = params.title;
songName.letterSpacing = -15;
songName.angle = -4.4;
@ -193,8 +191,8 @@ class ResultState extends MusicBeatSubState
add(soundSystem);
// Fetch playable character data. Default to BF on the results screen if we can't find it.
playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId);
playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf');
playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId) ?? 'bf';
playerCharacter = PlayerRegistry.instance.fetchEntry(playerCharacterId);
trace('Got playable character: ${playerCharacter?.getName()}');
// Query JSON data based on the rank, then use that to build the animation(s) the player sees.
@ -222,17 +220,22 @@ class ResultState extends MusicBeatSubState
{
case 'animateatlas':
@:nullSafety(Off)
var animation:FlxAtlasSprite = null;
var animation:FunkinSprite = null;
var xPos = offsets[0] + (FullScreenScaleMode.gameCutoutSize.x / 2);
var yPos = offsets[1];
if (animData.scriptClass != null) animation = ScriptedFlxAtlasSprite.init(animData.scriptClass, xPos, yPos);
if (animData.scriptClass != null) animation = ScriptedFunkinSprite.init(animData.scriptClass, xPos, yPos);
else
animation = new FlxAtlasSprite(xPos, yPos, Paths.animateAtlas(animPath, animLibrary));
animation = FunkinSprite.createTextureAtlas(xPos, yPos, animPath, animLibrary);
if (animation == null) continue;
if (animData?.applyStageMatrix ?? false)
{
animation.applyStageMatrix = true;
}
animation.zIndex = animData.zIndex ?? 500;
animation.scale.set(animData.scale ?? 1.0, animData.scale ?? 1.0);
@ -240,7 +243,7 @@ class ResultState extends MusicBeatSubState
if (!(animData.looped ?? true))
{
// Animation is not looped.
animation.onAnimationComplete.add((_name:String) -> {
animation.anim.onFinish.add((_name:String) -> {
if (animation != null)
{
animation.anim.pause();
@ -249,23 +252,24 @@ class ResultState extends MusicBeatSubState
}
else if (animData.loopFrameLabel != null)
{
animation.onAnimationComplete.add((_name:String) -> {
animation.anim.onFinish.add((_name:String) -> {
if (animation != null)
{
animation.playAnimation(animData.loopFrameLabel ?? '', true, false, true); // unpauses this anim, since it's on PlayOnce!
animation.anim.play(animData.loopFrameLabel ?? '', true); // unpauses this anim, since it's on PlayOnce!
animation.anim.curAnim.looped = true;
}
});
}
else if (animData.loopFrame != null)
{
animation.onAnimationComplete.add((_name:String) -> {
animation.anim.onFinish.add((_name:String) -> {
if (animation != null)
{
animation.anim.curFrame = animData.loopFrame ?? 0;
animation.anim.play(); // unpauses this anim, since it's on PlayOnce!
animation.anim.play("", true, false, animData.loopFrame ?? 0); // unpauses this anim, since it's on PlayOnce!
}
});
}
// Hide until ready to play.
animation.visible = false;
// Queue to play.
@ -660,7 +664,7 @@ class ResultState extends MusicBeatSubState
new FlxTimer().start(atlas.delay, _ -> {
if (atlas.sprite == null) return;
atlas.sprite.visible = true;
atlas.sprite.playAnimation(atlas.startFrameLabel);
atlas.sprite.anim.play(atlas.startFrameLabel);
if (atlas.sound != "")
{
var sndPath:String = Paths.stripLibrary(atlas.sound);
@ -770,7 +774,7 @@ class ResultState extends MusicBeatSubState
}
}
if (controls.PAUSE || controls.ACCEPT #if mobile || TouchUtil.pressAction() #end)
if (controls.PAUSE_P || controls.ACCEPT_P #if mobile || TouchUtil.pressAction() #end)
{
if (busy) return;
if (_parentState is funkin.ui.debug.results.ResultsDebugSubState)

View file

@ -1,35 +1,11 @@
package funkin.play.character;
import flixel.animation.FlxAnimationController;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFrame;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.math.FlxMath;
import flixel.math.FlxPoint.FlxCallbackPoint;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import funkin.graphics.FunkinSprite;
import flixel.system.FlxAssets.FlxGraphicAsset;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.events.ScriptEvent;
import funkin.data.animation.AnimationData;
import funkin.data.character.CharacterData.CharacterRenderType;
import flixel.util.FlxDirectionFlags;
import openfl.display.BitmapData;
import openfl.display.BlendMode;
/**
* Individual animation data for an AnimateAtlasCharacter.
*/
typedef AnimateAtlasAnimation =
{
name:String,
prefix:String,
offsets:Null<Array<Float>>,
looped:Bool,
}
import flixel.math.FlxPoint;
/**
* An AnimateAtlasCharacter is a Character which is rendered by
@ -40,39 +16,13 @@ typedef AnimateAtlasAnimation =
*/
class AnimateAtlasCharacter extends BaseCharacter
{
// BaseCharacter extends FlxSprite but we can't make it also extend FlxAtlasSprite UGH
// I basically copied the code from FlxSpriteGroup to make the FlxAtlasSprite a "child" of this class
var mainSprite:FlxAtlasSprite;
var _skipTransformChildren:Bool = false;
var animations:Map<String, AnimateAtlasAnimation> = new Map<String, AnimateAtlasAnimation>();
var currentAnimName:Null<String> = null;
var animFinished:Bool = false;
var originalSizes(default, never):FlxPoint = new FlxPoint(0, 0);
public function new(id:String)
{
super(id, CharacterRenderType.AnimateAtlas);
}
override function initVars():Void
{
// this.flixelType = SPRITEGROUP;
// TODO: Make `animation` a stub that redirects calls to `mainSprite`?
animation = new FlxAnimationController(this);
offset = new FlxCallbackPoint(offsetCallback);
origin = new FlxCallbackPoint(originCallback);
scale = new FlxCallbackPoint(scaleCallback);
scrollFactor = new FlxCallbackPoint(scrollFactorCallback);
scale.set(1, 1);
scrollFactor.set(1, 1);
initMotionVars();
}
override function onCreate(event:ScriptEvent):Void
{
// Display a custom scope for debugging purposes.
@ -83,756 +33,80 @@ class AnimateAtlasCharacter extends BaseCharacter
try
{
trace('Loading assets for Animate Atlas character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0"));
var atlasSprite:FlxAtlasSprite = loadAtlasSprite();
setSprite(atlasSprite);
loadAtlas();
loadAnimations();
}
catch (e)
{
throw "Exception thrown while building FlxAtlasSprite: " + e;
throw "Exception thrown while building sprite: " + e;
}
trace('[ATLASCHAR] Successfully loaded texture atlas for ${characterId} with ${_data.animations.length} animations.');
super.onCreate(event);
originalSizes.set(this.width, this.height);
}
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void
{
var correctName = correctAnimationName(name);
if (correctName == null)
{
trace('$characterName Could not find Atlas animation: ' + name);
return;
}
var animData = getAnimationData(correctName);
currentAnimName = correctName;
var prefix:String = animData.prefix;
if (prefix == null) prefix = correctName;
var loop:Bool = animData.looped;
this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop);
}
public override function hasAnimation(name:String):Bool
{
return getAnimationData(name) != null;
}
/**
* Returns true if the animation has finished playing.
* Never true if animation is configured to loop.
*/
public override function isAnimationFinished():Bool
{
return mainSprite?.isAnimationFinished() ?? false;
}
function loadAtlasSprite():FlxAtlasSprite
function loadAtlas():Void
{
trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.');
var assetLibrary:String = Paths.getLibrary(_data.assetPath);
var assetPath:String = Paths.stripLibrary(_data.assetPath);
var animLibrary:String = Paths.getLibrary(_data.assetPath);
var animPath:String = Paths.stripLibrary(_data.assetPath);
var assetPath:String = Paths.animateAtlas(animPath, animLibrary);
loadTextureAtlas(assetPath, assetLibrary, getAtlasSettings());
var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, assetPath);
// sprite.onAnimationComplete.removeAll();
sprite.onAnimationComplete.add(this.onAnimationFinished);
return sprite;
}
override function onAnimationFinished(prefix:String):Void
{
super.onAnimationFinished(prefix);
if (!getCurrentAnimation().endsWith(Constants.ANIMATION_HOLD_SUFFIX)
&& hasAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX))
if (_data.isPixel)
{
playAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX);
}
if (getAnimationData() != null && getAnimationData().looped)
{
if (StringTools.endsWith(prefix, "-hold")) trace(prefix);
playAnimation(prefix, true, false);
this.isPixel = true;
this.antialiasing = false;
}
else
{
// Make the game hold on the last frame.
this.mainSprite.cleanupAnimation(prefix);
// currentAnimName = null;
// Fallback to idle!
// playAnimation('idle', true, false);
this.isPixel = false;
this.antialiasing = true;
}
}
function setSprite(sprite:FlxAtlasSprite):Void
{
trace('[ATLASCHAR] Applying sprite properties to ${characterId}');
this.mainSprite = sprite;
mainSprite.ignoreExclusionPref = ["sing"];
// This forces the atlas to recalcuate its width and height
this.mainSprite.alpha = 0.0001;
this.mainSprite.draw();
this.mainSprite.alpha = 1.0;
var feetPos:FlxPoint = feetPosition;
this.updateHitbox();
sprite.x = this.x;
sprite.y = this.y;
sprite.alpha *= alpha;
sprite.flipX = flipX;
sprite.flipY = flipY;
sprite.scrollFactor.copyFrom(scrollFactor);
sprite.cameras = _cameras; // _cameras instead of cameras because get_cameras() will not return null
if (clipRect != null) clipRectTransform(sprite, clipRect);
this.setScale(_data.scale);
}
function loadAnimations():Void
{
trace('[ATLASCHAR] Attempting to load ${_data.animations.length} animations for ${characterId}');
trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
var animData:Array<AnimateAtlasAnimation> = cast _data.animations;
FlxAnimationUtil.addTextureAtlasAnimations(this, _data.animations);
for (anim in animData)
for (anim in _data.animations)
{
// Validate the animation before adding.
var prefix = anim.prefix;
if (!this.mainSprite.hasAnimation(prefix))
if (anim.offsets == null)
{
FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}');
trace('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}');
continue;
setAnimationOffsets(anim.name, 0, 0);
}
else
{
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
}
animations.set(anim.name, anim);
trace('[ATLASCHAR] - Successfully loaded animation ${anim.name} to ${characterId}');
}
trace('[ATLASCHAR] Loaded ${animations.size()} animations for ${characterId}');
}
public override function getCurrentAnimation():String
{
// return this.mainSprite.getCurrentAnimation();
return currentAnimName;
}
function getAnimationData(name:String = null):AnimateAtlasAnimation
{
if (name == null) name = getCurrentAnimation();
return animations.get(name);
}
//
//
// Code copied from FlxSpriteGroup
//
//
/**
* Handy function that allows you to quickly transform one property of sprites in this group at a time.
*
* @param callback Function to transform the sprites. Example:
* `function(sprite, v:Dynamic) { s.acceleration.x = v; s.makeGraphic(10,10,0xFF000000); }`
* @param value Value which will passed to lambda function.
*/
@:generic
public function transformChildren<V>(callback:FlxAtlasSprite->V->Void, value:V):Void
{
if (_skipTransformChildren || this.mainSprite == null) return;
callback(this.mainSprite, value);
}
/**
* Calls `kill()` on the group's members and then on the group itself.
* You can revive this group later via `revive()` after this.
*/
public override function kill():Void
{
_skipTransformChildren = true;
super.kill();
_skipTransformChildren = false;
if (this.mainSprite != null)
{
this.mainSprite.kill();
this.mainSprite = null;
}
}
/**
* Revives the group.
*/
public override function revive():Void
{
_skipTransformChildren = true;
super.revive(); // calls set_exists and set_alive
_skipTransformChildren = false;
this.mainSprite.revive();
}
/**
* **WARNING:** A destroyed `FlxBasic` can't be used anymore.
* It may even cause crashes if it is still part of a group or state.
* You may want to use `kill()` instead if you want to disable the object temporarily only and `revive()` it later.
*
* This function is usually not called manually (Flixel calls it automatically during state switches for all `add()`ed objects).
*
* Override this function to `null` out variables manually or call `destroy()` on class members if necessary.
* Don't forget to call `super.destroy()`!
*/
public override function destroy():Void
{
// normally don't have to destroy FlxPoints, but these are FlxCallbackPoints!
offset = FlxDestroyUtil.destroy(offset);
origin = FlxDestroyUtil.destroy(origin);
scale = FlxDestroyUtil.destroy(scale);
scrollFactor = FlxDestroyUtil.destroy(scrollFactor);
this.mainSprite = FlxDestroyUtil.destroy(this.mainSprite);
super.destroy();
}
/**
* Check and see if any sprite in this group is currently on screen.
*
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
* @return Whether the object is on screen or not.
*/
public override function isOnScreen(?camera:FlxCamera):Bool
{
if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible && this.mainSprite.isOnScreen(camera)) return true;
return false;
}
/**
* Checks to see if a point in 2D world space overlaps any `FlxSprite` object from this group.
*
* @param Point The point in world space you want to check.
* @param InScreenSpace Whether to take scroll factors into account when checking for overlap.
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
* @return Whether or not the point overlaps this group.
*/
public override function overlapsPoint(point:FlxPoint, inScreenSpace:Bool = false, camera:FlxCamera = null):Bool
{
var result:Bool = false;
result = this.mainSprite.overlapsPoint(point, inScreenSpace, camera);
return result;
}
/**
* Checks to see if a point in 2D world space overlaps any of FlxSprite object's current displayed pixels.
* This check is ALWAYS made in screen space, and always takes scroll factors into account.
*
* @param Point The point in world space you want to check.
* @param Mask Used in the pixel hit test to determine what counts as solid.
* @param Camera Specify which game camera you want. If `null`, it will just grab the first global camera.
* @return Whether or not the point overlaps this object.
*/
public override function pixelsOverlapPoint(point:FlxPoint, Mask:Int = 0xFF, Camera:FlxCamera = null):Bool
{
var result:Bool = false;
if (this.mainSprite != null && this.mainSprite.exists && this.mainSprite.visible)
{
result = this.mainSprite.pixelsOverlapPoint(point, Mask, Camera);
}
return result;
}
public override function update(elapsed:Float):Void
{
this.mainSprite.update(elapsed);
if (moves) updateMotion(elapsed);
}
public override function draw():Void
{
this.mainSprite.draw();
#if FLX_DEBUG
if (FlxG.debugger.drawDebug) drawDebug();
#end
}
inline function xTransform(sprite:FlxSprite, x:Float):Void
sprite.x += x; // addition
inline function yTransform(sprite:FlxSprite, y:Float):Void
sprite.y += y; // addition
inline function angleTransform(sprite:FlxSprite, angle:Float):Void
sprite.angle += angle; // addition
inline function alphaTransform(sprite:FlxSprite, alpha:Float):Void
{
if (sprite.alpha != 0 || alpha == 0)
{
sprite.alpha *= alpha; // multiplication
}
else
{
sprite.alpha = 1 / alpha; // direct set to avoid stuck sprites
}
}
inline function directAlphaTransform(sprite:FlxSprite, alpha:Float):Void
sprite.alpha = alpha; // direct set
inline function facingTransform(sprite:FlxSprite, facing:FlxDirectionFlags):Void
sprite.facing = facing;
inline function flipXTransform(sprite:FlxSprite, flipX:Bool):Void
sprite.flipX = flipX;
inline function flipYTransform(sprite:FlxSprite, flipY:Bool):Void
sprite.flipY = flipY;
inline function movesTransform(sprite:FlxSprite, moves:Bool):Void
sprite.moves = moves;
inline function pixelPerfectTransform(sprite:FlxSprite, pixelPerfect:Bool):Void
sprite.pixelPerfectRender = pixelPerfect;
inline function gColorTransform(sprite:FlxSprite, color:Int):Void
sprite.color = color;
inline function blendTransform(sprite:FlxSprite, blend:BlendMode):Void
sprite.blend = blend;
inline function immovableTransform(sprite:FlxSprite, immovable:Bool):Void
sprite.immovable = immovable;
inline function visibleTransform(sprite:FlxSprite, visible:Bool):Void
sprite.visible = visible;
inline function activeTransform(sprite:FlxSprite, active:Bool):Void
sprite.active = active;
inline function solidTransform(sprite:FlxSprite, solid:Bool):Void
sprite.solid = solid;
inline function aliveTransform(sprite:FlxSprite, alive:Bool):Void
sprite.alive = alive;
inline function existsTransform(sprite:FlxSprite, exists:Bool):Void
sprite.exists = exists;
inline function cameraTransform(sprite:FlxSprite, camera:FlxCamera):Void
sprite.camera = camera;
inline function camerasTransform(sprite:FlxSprite, cameras:Array<FlxCamera>):Void
sprite.cameras = cameras;
inline function offsetTransform(sprite:FlxSprite, offset:FlxPoint):Void
sprite.offset.copyFrom(offset);
inline function originTransform(sprite:FlxSprite, origin:FlxPoint):Void
sprite.origin.copyFrom(origin);
inline function scaleTransform(sprite:FlxSprite, scale:FlxPoint):Void
sprite.scale.copyFrom(scale);
inline function scrollFactorTransform(sprite:FlxSprite, scrollFactor:FlxPoint):Void
sprite.scrollFactor.copyFrom(scrollFactor);
inline function clipRectTransform(sprite:FlxSprite, clipRect:FlxRect):Void
{
if (clipRect == null)
{
sprite.clipRect = null;
}
else
{
sprite.clipRect = FlxRect.get(clipRect.x - sprite.x + x, clipRect.y - sprite.y + y, clipRect.width, clipRect.height);
}
}
var resS:FlxPoint = new FlxPoint();
/**
* Reset the character so it can be used at the start of the level.
* Call this when restarting the level.
*/
override public function resetCharacter(resetCamera:Bool = true):Void
{
trace("RESETTING ATLAS " + characterName);
// Reset the animation offsets. This will modify x and y to be the absolute position of the character.
// this.animOffsets = [0, 0];
// Now we can set the x and y to be their original values without having to account for animOffsets.
this.resetPosition();
mainSprite.setPosition(originalPosition.x, originalPosition.y);
// Then reapply animOffsets...
// applyAnimationOffsets(getCurrentAnimation());
// Make sure we are playing the idle animation
// ...then update the hitbox so that this.width and this.height are correct.
mainSprite.scale.set(1, 1);
mainSprite.alpha = 0.0001;
mainSprite.width = 0;
mainSprite.height = 0;
this.dance(true); // Force to avoid the old animation playing with the wrong offset at the start of the song.
mainSprite.draw(); // refresh frame
if (resS.x == 0)
{
resS.x = mainSprite.width; // clunky bizz
resS.y = mainSprite.height;
}
mainSprite.alpha = alpha;
mainSprite.width = resS.x;
mainSprite.height = resS.y;
frameWidth = 0;
frameHeight = 0;
scaleCallback(scale);
this.updateHitbox();
// Reset the camera focus point while we're at it.
if (resetCamera) this.resetCameraFocusPoint();
}
inline function offsetCallback(offset:FlxPoint):Void
transformChildren(offsetTransform, offset);
inline function originCallback(origin:FlxPoint):Void
transformChildren(originTransform, origin);
inline function scaleCallback(scale:FlxPoint):Void
transformChildren(scaleTransform, scale);
inline function scrollFactorCallback(scrollFactor:FlxPoint):Void
transformChildren(scrollFactorTransform, scrollFactor);
override function set_camera(value:FlxCamera):FlxCamera
{
if (camera != value) transformChildren(cameraTransform, value);
return super.set_camera(value);
}
override function set_cameras(value:Array<FlxCamera>):Array<FlxCamera>
{
if (cameras != value) transformChildren(camerasTransform, value);
return super.set_cameras(value);
}
override function set_exists(value:Bool):Bool
{
if (exists != value) transformChildren(existsTransform, value);
return super.set_exists(value);
}
override function set_visible(value:Bool):Bool
{
if (exists && visible != value) transformChildren(visibleTransform, value);
return super.set_visible(value);
}
override function set_active(value:Bool):Bool
{
if (exists && active != value) transformChildren(activeTransform, value);
return super.set_active(value);
}
override function set_alive(value:Bool):Bool
{
if (alive != value) transformChildren(aliveTransform, value);
return super.set_alive(value);
}
override function set_x(value:Float):Float
{
if (!exists || x == value) return x; // early return (no need to transform)
transformChildren(xTransform, value - x); // offset
return x = value;
}
override function set_y(value:Float):Float
{
if (exists && y != value) transformChildren(yTransform, value - y); // offset
return y = value;
}
override function set_angle(value:Float):Float
{
if (exists && angle != value) transformChildren(angleTransform, value - angle); // offset
return angle = value;
}
override function set_alpha(value:Float):Float
{
value = value.clamp(0, 1);
if (exists && alpha != value)
{
transformChildren(directAlphaTransform, value);
}
return alpha = value;
}
override function set_facing(value:FlxDirectionFlags):FlxDirectionFlags
{
if (exists && facing != value) transformChildren(facingTransform, value);
return facing = value;
}
override function set_flipX(value:Bool):Bool
{
if (exists && flipX != value) transformChildren(flipXTransform, value);
return flipX = value;
}
override function set_flipY(value:Bool):Bool
{
if (exists && flipY != value) transformChildren(flipYTransform, value);
return flipY = value;
}
override function set_moves(value:Bool):Bool
{
if (exists && moves != value) transformChildren(movesTransform, value);
return moves = value;
}
override function set_immovable(value:Bool):Bool
{
if (exists && immovable != value) transformChildren(immovableTransform, value);
return immovable = value;
}
override function set_solid(value:Bool):Bool
{
if (exists && solid != value) transformChildren(solidTransform, value);
return super.set_solid(value);
}
override function set_color(value:Int):Int
{
if (exists && color != value) transformChildren(gColorTransform, value);
return color = value;
}
override function set_blend(value:BlendMode):BlendMode
{
if (exists && blend != value) transformChildren(blendTransform, value);
return blend = value;
}
override function set_clipRect(rect:FlxRect):FlxRect
{
if (exists) transformChildren(clipRectTransform, rect);
return super.set_clipRect(rect);
}
override function set_pixelPerfectRender(value:Bool):Bool
{
if (exists && pixelPerfectRender != value) transformChildren(pixelPerfectTransform, value);
return super.set_pixelPerfectRender(value);
}
override function set_width(value:Float):Float
{
return value;
var animNames = this.anim.getNameList();
trace('[ATLASCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
}
override function get_width():Float
{
if (this.mainSprite == null) return 0;
return this.mainSprite.width;
}
/**
* Returns the left-most position of the left-most member.
* If there are no members, x is returned.
*
* @since 5.0.0
* @return the left-most position of the left-most member
*/
public function findMinX():Float
{
return this.mainSprite == null ? x : findMinXHelper();
}
function findMinXHelper():Float
{
return this.mainSprite.x;
}
/**
* Returns the right-most position of the right-most member.
* If there are no members, x is returned.
*
* @since 5.0.0
* @return the right-most position of the right-most member
*/
public function findMaxX():Float
{
return this.mainSprite == null ? x : findMaxXHelper();
}
function findMaxXHelper():Float
{
return this.mainSprite.x + this.mainSprite.width;
}
/**
* This functionality isn't supported in SpriteGroup
*/
override function set_height(value:Float):Float
{
return value;
return originalSizes.x;
}
override function get_height():Float
{
if (this.mainSprite == null) return 0;
return this.mainSprite.height;
return originalSizes.y;
}
/**
* Returns the top-most position of the top-most member.
* If there are no members, y is returned.
*
* @since 5.0.0
* @return the top-most position of the top-most member
* Get the configuration for the texture atlas.
* @return The configuration for the texture atlas.
*/
public function findMinY():Float
public function getAtlasSettings():AtlasSpriteSettings
{
return this.mainSprite == null ? y : findMinYHelper();
return cast _data.atlasSettings;
}
function findMinYHelper():Float
{
return this.mainSprite.y;
}
/**
* Returns the top-most position of the top-most member.
* If there are no members, y is returned.
*
* @since 5.0.0
* @return the bottom-most position of the bottom-most member
*/
public function findMaxY():Float
{
return this.mainSprite == null ? y : findMaxYHelper();
}
function findMaxYHelper():Float
{
return this.mainSprite.y + this.mainSprite.height;
}
/**
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite
{
#if FLX_DEBUG
throw "This function is not supported in FlxSpriteGroup";
#end
return this;
}
/**
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function loadGraphic(Graphic:FlxGraphicAsset, Animated:Bool = false, Width:Int = 0, Height:Int = 0, Unique:Bool = false,
?Key:String):FlxSprite
{
return this;
}
/**
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override function loadRotatedGraphic(Graphic: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 FlxSpriteGroup";
#end
return this;
}
/**
* This functionality isn't supported in SpriteGroup
* @return this sprite group
*/
public override 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 FlxSpriteGroup";
#end
return this;
}
override function set_pixels(value:BitmapData):BitmapData
{
return value;
}
override function set_frame(value:FlxFrame):FlxFrame
{
return value;
}
override function get_pixels():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 SpriteGroup
*/
override inline function resetHelpers():Void {}
/**
* This functionality isn't supported in SpriteGroup
*/
public override inline function stamp(Brush:FlxSprite, X:Int = 0, Y:Int = 0):Void {}
override function set_frames(Frames:FlxFramesCollection):FlxFramesCollection
{
return Frames;
}
/**
* This functionality isn't supported in SpriteGroup
*/
override inline function updateColorTransform():Void {}
}

View file

@ -337,7 +337,7 @@ class BaseCharacter extends Bopper
{
if (PlayState.instance.iconP1 == null)
{
trace('[WARN] Player 1 health icon not found!');
trace(' WARNING '.bold().bg_yellow() + ' Player 1 health icon not found!');
return;
}
PlayState.instance.iconP1.configure(_data?.healthIcon);
@ -347,7 +347,7 @@ class BaseCharacter extends Bopper
{
if (PlayState.instance.iconP2 == null)
{
trace('[WARN] Player 2 health icon not found!');
trace(' WARNING '.bold().bg_yellow() + ' Player 2 health icon not found!');
return;
}
PlayState.instance.iconP2.configure(_data?.healthIcon);

View file

@ -0,0 +1,138 @@
package funkin.play.character;
import funkin.graphics.FunkinSprite;
import funkin.util.assets.FlxAnimationUtil;
import animate.FlxAnimateFrames;
import funkin.modding.events.ScriptEvent;
import funkin.data.animation.AnimationData;
import funkin.data.character.CharacterData.CharacterRenderType;
import flixel.math.FlxPoint;
/**
* This render type is the most complex, and is used by characters which use
* multiple Adobe Animate texture atlases. This render type concatenates multiple
* texture atlases into a single sprite.
*
* BaseCharacter has game logic, MultiAnimateAtlasCharacter has only rendering logic.
* KEEP THEM SEPARATE!
*/
class MultiAnimateAtlasCharacter extends BaseCharacter
{
var originalSizes(default, never):FlxPoint = new FlxPoint(0, 0);
public function new(id:String)
{
super(id, CharacterRenderType.MultiAnimateAtlas);
}
override function onCreate(event:ScriptEvent):Void
{
// Display a custom scope for debugging purposes.
#if FEATURE_DEBUG_TRACY
cpp.vm.tracy.TracyProfiler.zoneScoped('MultiAnimateAtlasCharacter.create(${this.characterId})');
#end
try
{
trace('Loading assets for Multi-Animate Atlas character "${characterId}"', flixel.util.FlxColor.fromString("#89CFF0"));
loadAtlases();
loadAnimations();
}
catch (e)
{
throw "Exception thrown while building sprite: " + e;
}
trace('[MULTIATLASCHAR] Successfully loaded texture atlases for ${characterId} with ${_data.animations.length} animations.');
super.onCreate(event);
originalSizes.set(this.width, this.height);
}
function loadAtlases():Void
{
trace('[MULTIATLASCHAR] Loading sprite atlases for ${characterId}.');
var assetList:Array<String> = [];
for (anim in _data.animations)
{
if (anim.assetPath != null && !assetList.contains(anim.assetPath))
{
assetList.push(anim.assetPath);
}
}
var baseAssetLibrary:String = Paths.getLibrary(_data.assetPath);
var baseAssetPath:String = Paths.stripLibrary(_data.assetPath);
loadTextureAtlas(baseAssetPath, baseAssetLibrary, getAtlasSettings());
for (asset in assetList)
{
var subAssetLibrary:String = Paths.getLibrary(asset);
var subAssetPath:String = Paths.stripLibrary(asset);
var clone:FunkinSprite = FunkinSprite.createTextureAtlas(0, 0, subAssetPath, subAssetLibrary, cast _data.atlasSettings);
var subTexture:FlxAnimateFrames = clone.library;
trace('Concatenating texture atlas: ${asset}');
subTexture.parent.destroyOnNoUse = false;
this.library.addAtlas(subTexture);
}
if (_data.isPixel)
{
this.isPixel = true;
this.antialiasing = false;
}
else
{
this.isPixel = false;
this.antialiasing = true;
}
this.setScale(_data.scale);
}
function loadAnimations():Void
{
trace('[MULTIATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
FlxAnimationUtil.addTextureAtlasAnimations(this, _data.animations);
for (anim in _data.animations)
{
if (anim.offsets == null)
{
setAnimationOffsets(anim.name, 0, 0);
}
else
{
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
}
}
var animNames = this.anim.getNameList();
trace('[MULTIATLASCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
}
override function get_width():Float
{
return originalSizes.x;
}
override function get_height():Float
{
return originalSizes.y;
}
/**
* Get the configuration for the texture atlas.
* @return The configuration for the texture atlas.
*/
public function getAtlasSettings():AtlasSpriteSettings
{
return cast _data.atlasSettings;
}
}

View file

@ -126,9 +126,4 @@ class MultiSparrowCharacter extends BaseCharacter
var animNames = this.animation.getNameList();
trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
}
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void
{
super.playAnimation(name, restart, ignoreOther, reverse);
}
}

View file

@ -26,6 +26,14 @@ class ScriptedSparrowCharacter extends SparrowCharacter implements polymod.hscri
@:hscriptClass
class ScriptedMultiSparrowCharacter extends MultiSparrowCharacter implements polymod.hscript.HScriptedClass {}
/**
* A script that can be tied to a MultiAnimateAtlasCharacter, which persists across states.
* Create a scripted class that extends MultiAnimateAtlasCharacter,
* then call `super('charId')` in the constructor to use this.
*/
@:hscriptClass
class ScriptedMultiAnimateAtlasCharacter extends MultiAnimateAtlasCharacter implements polymod.hscript.HScriptedClass {}
/**
* A script that can be tied to a PackerCharacter, which persists across states.
* Create a scripted class that extends PackerCharacter,

View file

@ -49,8 +49,6 @@ class SparrowCharacter extends BaseCharacter
{
this.isPixel = true;
this.antialiasing = false;
// pixelPerfectRender = true;
// pixelPerfectPosition = true;
}
else
{

View file

@ -193,6 +193,7 @@ class HealthIcon extends FunkinSprite
this.size.set(1.0, 1.0);
this.iconOffset.set();
this.flipX = false;
this.updatePosition();
}
else
{
@ -212,6 +213,7 @@ class HealthIcon extends FunkinSprite
}
this.flipX = data.flipX ?? false; // Face the OTHER way by default, since that is more common.
this.updatePosition();
}
}
@ -222,7 +224,12 @@ class HealthIcon extends FunkinSprite
{
super.update(elapsed);
if (bopEvery != 0) this.angle = MathUtil.smoothLerpPrecision(this.angle, 0, elapsed, 0.512);
if (bopEvery != 0)
{
var dt:Float = elapsed * 60;
this.angle = MathUtil.smoothLerpPrecision(this.angle, 0, dt, 0.512);
}
this.updatePosition();
}
@ -436,35 +443,6 @@ class HealthIcon extends FunkinSprite
this.antialiasing = !isPixel;
}
/**
* @return Name of the current animation being played by this health icon.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
/**
* @param id The name of the animation to check for.
* @return Whether this sprite posesses the given animation.
* Only true if the animation was successfully loaded from the XML.
*/
public function hasAnimation(id:String):Bool
{
if (this.animation == null) return false;
return this.animation.getByName(id) != null;
}
/**
* @return Whether the current animation is in the finished state.
*/
public function isAnimationFinished():Bool
{
return this.animation.finished;
}
/**
* Plays the animation with the given name.
* @param name The name of the animation to play.

View file

@ -33,7 +33,7 @@ class Subtitles extends FlxSpriteGroup
background.alpha = 0.5;
add(background);
subtitleText = new SubtitlesText(0, 0, 30, Paths.font('vcr.ttf'));
subtitleText = new SubtitlesText(0, 0, 30, 'VCR OSD Mono');
add(subtitleText);
setText([], true);
@ -165,6 +165,11 @@ class SubtitlesText extends FlxText
}
return Text;
}
override function applyFormats(_:openfl.text.TextFormat, __:Bool = false):Void
{
// This function shouldn't get called because it messes up `htmlText`.
}
}
/**

View file

@ -301,7 +301,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
}
else
{
trace('[WARNING] Unexpected state transition from ${this.state}');
trace(' WARNING '.bg_yellow().bold() + ' Unexpected state transition from ${this.state}');
this.state = ConversationState.Idle;
}
}

View file

@ -201,12 +201,6 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
applyAnimationOffsets(correctName);
}
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
/**
* Ensure that a given animation exists before playing it.
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.

View file

@ -197,7 +197,7 @@ class FocusCameraSongEvent extends SongEvent
{
name: 'ease',
title: 'Easing Type',
defaultValue: 'linear',
defaultValue: 'CLASSIC',
type: SongEventFieldType.ENUM,
keys: [
'Linear' => 'linear',

View file

@ -61,7 +61,7 @@ class SetHealthIconSongEvent extends SongEvent
trace('Applying Opponent health icon via song event: ${healthIconData.id}');
PlayState.instance.iconP2.configure(healthIconData);
default:
trace('[WARN] Unknown character index: ' + data.value.char);
trace(' WARNING '.bold().bg_yellow() + ' Unknown character index: ' + data.value.char);
}
}

View file

@ -2,6 +2,7 @@ package funkin.play.notes;
import flixel.util.FlxSignal.FlxTypedSignal;
import flixel.FlxG;
import funkin.play.notes.NoteVibrationsHandler.NoteStatus;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.group.FlxSpriteGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
@ -9,11 +10,6 @@ import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxSort;
import funkin.graphics.FunkinSprite;
import funkin.play.notes.NoteHoldCover;
import funkin.play.notes.NoteSplash;
import funkin.play.notes.NoteSprite;
import funkin.play.notes.SustainTrail;
import funkin.play.notes.NoteVibrationsHandler;
import funkin.data.song.SongData.SongNoteData;
import funkin.util.SortUtil;
import funkin.util.GRhythmUtil;
@ -69,7 +65,8 @@ class Strumline extends FlxSpriteGroup
function get_renderDistanceMs():Float
{
if (useCustomRenderDistance) return customRenderDistanceMs;
// Only divide by lower scroll speeds to fix renderDistance being too short. Dividing by higher scroll speeds breaks the input system by hitting later notes first!
// Only divide by lower scroll speeds to fix renderDistance being too short.
// Dividing by higher scroll speeds breaks the input system by hitting later notes first!
return FlxG.height / Constants.PIXELS_PER_MS / (scrollSpeed < 1 ? scrollSpeed : 1);
}
@ -146,7 +143,9 @@ class Strumline extends FlxSpriteGroup
* The strumline notes (the receptors) themselves.
*/
public var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
/**
* Hold note covers.
*/
@ -159,17 +158,26 @@ class Strumline extends FlxSpriteGroup
var noteSpacingScale:Float = 1;
/**
* The scale of the strumline. Use this to resize it rather than setting the scale directly.
*/
public var strumlineScale(default, null):FlxPoint;
#if FEATURE_GHOST_TAPPING
var ghostTapTimer:Float = 0.0;
#end
/**
* Handles note vibrations for this strumline
*/
public var noteVibrations:NoteVibrationsHandler = new NoteVibrationsHandler();
final inArrowContorlSchemeMode:Bool = #if mobile (Preferences.controlsScheme == FunkinHitboxControlSchemes.Arrows
final inArrowControlSchemeMode:Bool = #if mobile (Preferences.controlsScheme == FunkinHitboxControlSchemes.Arrows
&& !ControlsHandler.usingExternalInputDevice) #else false #end;
/**
* Whether the strumline is downscroll.
*/
public var isDownscroll:Bool = #if mobile (Preferences.controlsScheme == FunkinHitboxControlSchemes.Arrows
&& !ControlsHandler.usingExternalInputDevice)
|| #end Preferences.downscroll;
@ -187,7 +195,12 @@ class Strumline extends FlxSpriteGroup
*/
public var nextNoteIndex:Int = -1;
var heldKeys:Array<Bool> = [];
/**
* Indicates which keys are pressed for which directions.
* The direction is pressed as long as at least one key is held,
* and released when no keys are held.
*/
var heldKeys:Array<Array<Int>>;
static final BACKGROUND_PAD:Int = 16;
@ -229,7 +242,7 @@ class Strumline extends FlxSpriteGroup
var backgroundWidth:Float = KEY_COUNT * Strumline.NOTE_SPACING + BACKGROUND_PAD * 2;
#if mobile
if (inArrowContorlSchemeMode && isPlayer)
if (inArrowControlSchemeMode && isPlayer)
{
backgroundWidth = backgroundWidth * 1.84;
}
@ -240,7 +253,7 @@ class Strumline extends FlxSpriteGroup
this.background.scrollFactor.set(0, 0);
this.background.x = -BACKGROUND_PAD;
#if mobile
if (inArrowContorlSchemeMode && isPlayer) this.background.x -= 100;
if (inArrowControlSchemeMode && isPlayer) this.background.x -= 100;
#end
this.add(this.background);
@ -261,9 +274,10 @@ class Strumline extends FlxSpriteGroup
this.strumlineNotes.add(child);
}
this.heldKeys = [];
for (i in 0...KEY_COUNT)
{
heldKeys.push(false);
this.heldKeys[i] = [];
}
strumlineScale.set(1, 1);
@ -458,7 +472,7 @@ class Strumline extends FlxSpriteGroup
* Enter mini mode, which displays only small strumline notes
* @param scale scale of strumline
*/
public function enterMiniMode(scale:Float = 1)
public function enterMiniMode(scale:Float = 1):Void
{
forEach(function(obj:flixel.FlxObject):Void {
if (obj != strumlineNotes) obj.visible = false;
@ -467,11 +481,15 @@ class Strumline extends FlxSpriteGroup
this.strumlineScale.set(scale, scale);
}
public function strumlineScaleCallback(Scale:FlxPoint)
/**
* Called whenever the `strumlineScale` value is updated.
* @param Scale The new value.
*/
function strumlineScaleCallback(scale:FlxPoint):Void
{
strumlineNotes.forEach(function(note:StrumlineNote):Void {
var styleScale = noteStyle.getStrumlineScale();
note.scale.set(styleScale * Scale.x, styleScale * Scale.y);
note.scale.set(styleScale * scale.x, styleScale * scale.y);
});
setNoteSpacing(noteSpacingScale);
}
@ -545,6 +563,9 @@ class Strumline extends FlxSpriteGroup
}
}
/**
* Called every frame to update the position and hitbox of each child note.
*/
public function updateNotes():Void
{
if (noteData.length == 0) return;
@ -566,7 +587,6 @@ class Strumline extends FlxSpriteGroup
{
// Note is in the past, skip it.
nextNoteIndex = noteIndex + 1;
// trace("Strumline: Skipping note at index " + noteIndex + " with strum time " + note.time);
continue;
}
if (note.time > renderWindowStart) break; // Note is too far ahead to render
@ -813,19 +833,29 @@ class Strumline extends FlxSpriteGroup
/**
* Called when a key is pressed.
* @param dir The direction of the key that was pressed.
* @param keyCode The key input used to press the direction. Used to distinguish when two keys for the same direction are pressed.
*/
public function pressKey(dir:NoteDirection):Void
public function pressKey(dir:NoteDirection, keyCode:Int):Void
{
heldKeys[dir] = true;
heldKeys[dir].push(keyCode);
}
/**
* Called when a key is released.
* @param dir The direction of the key that was released.
* @param keyCode The key input used to press the direction. Used to distinguish when two keys for the same direction are pressed.
* If null, all keys for the direction are released.
*/
public function releaseKey(dir:NoteDirection):Void
public function releaseKey(dir:NoteDirection, ?keyCode:Int):Void
{
heldKeys[dir] = false;
if (keyCode == null)
{
heldKeys[dir].clear();
}
else
{
heldKeys[dir].remove(keyCode);
}
}
/**
@ -835,7 +865,7 @@ class Strumline extends FlxSpriteGroup
*/
public function isKeyHeld(dir:NoteDirection):Bool
{
return heldKeys[dir];
return heldKeys[dir].length > 0;
}
/**
@ -869,7 +899,7 @@ class Strumline extends FlxSpriteGroup
cover.kill();
}
heldKeys = [false, false, false, false];
heldKeys = [[], [], [], []];
for (dir in DIRECTIONS)
{
@ -1113,7 +1143,7 @@ class Strumline extends FlxSpriteGroup
var trueScale = new FlxPoint(strumlineScale.x, strumlineScale.y);
#if mobile
if (inArrowContorlSchemeMode)
if (inArrowControlSchemeMode)
{
final amplification:Float = (FlxG.width / FlxG.height) / (FlxG.initialWidth / FlxG.initialHeight);
trueScale.set(strumlineScale.x - ((FlxG.height / FlxG.width) * 0.2) * amplification,
@ -1300,7 +1330,7 @@ class Strumline extends FlxSpriteGroup
{
var pos:Float = 0;
#if mobile
if (inArrowContorlSchemeMode && isPlayer) pos = 35 * (FlxG.width / FlxG.height) / (FlxG.initialWidth / FlxG.initialHeight);
if (inArrowControlSchemeMode && isPlayer) pos = 35 * (FlxG.width / FlxG.height) / (FlxG.initialWidth / FlxG.initialHeight);
#end
return switch (direction)
{

View file

@ -175,22 +175,6 @@ class StrumlineNote extends FunkinSprite
}
}
/**
* Returns the name of the animation that is currently playing.
* If no animation is playing (usually this means the sprite is BROKEN!),
* returns an empty string to prevent NPEs.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
public function isAnimationFinished():Bool
{
return this.animation.finished;
}
static final DEFAULT_OFFSET:Int = 13;
/**

View file

@ -43,7 +43,7 @@ class NoteKind implements INoteScriptedClass
* Only accessible in scripts
* Defaults to true
*/
public var scoreable:Bool = true;
public var scoreable(default, default):Bool = true;
public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array<NoteKindParam>, ?noanim:Bool, ?suffix:String)
{
@ -62,13 +62,27 @@ class NoteKind implements INoteScriptedClass
/**
* Retrieve all notes of this kind
* @param visibleCheck If true, only visible notes will be returned
* @return Array<NoteSprite>
*/
function getNotes():Array<NoteSprite>
function getNotes(visibleCheck:Bool = false):Array<NoteSprite>
{
var allNotes:Array<NoteSprite> = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members);
return allNotes.filter(function(note:NoteSprite) {
return note != null && note.noteData.kind == this.noteKind;
return note != null && note.noteData.kind == this.noteKind && (!visibleCheck || note.visible);
});
}
/**
* Retrieve all notes NOT of this kind
* @param visibleCheck If true, only visible notes will be returned
* @return Array<NoteSprite>
*/
function getOtherNotes(visibleCheck:Bool = false):Array<NoteSprite>
{
var allNotes:Array<NoteSprite> = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members);
return allNotes.filter(function(note:NoteSprite) {
return note != null && note.noteData.kind != this.noteKind && (!visibleCheck || note.visible);
});
}

View file

@ -65,12 +65,12 @@ class NoteKindManager
if (kind != null)
{
trace(' Loaded built-in note kind: ${kind.noteKind}');
trace(' Loaded built-in note kind: ${kind.noteKind}');
noteKinds.set(kind.noteKind, kind);
}
else
{
trace(' Failed to load built-in note kind: ${noteKindClsName}');
trace(' Failed to load built-in note kind: ${noteKindClsName}');
}
}
}

View file

@ -86,6 +86,9 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
target.antialiasing = !(_data.assets?.note?.isPixel ?? false);
var noteOffsets:Array<Float> = getNoteOffsets();
target.offset.set(noteOffsets[0], noteOffsets[1]);
// Apply the animations.
buildNoteAnimations(target);
@ -115,7 +118,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return null;
}
if (!FunkinSprite.isTextureCached(Paths.image(noteAssetPath)))
if (!FunkinMemory.isTextureCached(Paths.image(noteAssetPath)))
{
FlxG.log.warn('Note texture is not cached: ${noteAssetPath}');
}
@ -180,6 +183,11 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return _data.assets?.note?.scale ?? fallback?.getNoteScale() ?? 1.0;
}
public function getNoteOffsets():Array<Float>
{
return _data?.assets?.note?.offsets ?? fallback?.getNoteOffsets() ?? [0.0, 0.0];
}
function fetchNoteAnimationData(dir:NoteDirection):Null<AnimationData>
{
var result:Null<AnimationData> = switch (dir)
@ -896,7 +904,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return null;
}
if (!FunkinSprite.isTextureCached(Paths.image(splashAssetPath)))
if (!FunkinMemory.isTextureCached(Paths.image(splashAssetPath)))
{
FlxG.log.warn('Note Splash texture not cached: ${splashAssetPath}');
}
@ -1046,7 +1054,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return null;
}
if (!FunkinSprite.isTextureCached(Paths.image(holdCoverAssetPath)))
if (!FunkinMemory.isTextureCached(Paths.image(holdCoverAssetPath)))
{
FlxG.log.warn('Hold Note Cover texture not cached: ${holdCoverAssetPath}');
}

View file

@ -154,7 +154,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
{
if (!validateVariationId(vari))
{
trace(' [WARN] Variation id "$vari" is invalid, skipping...');
trace(' WARNING '.bold().bg_yellow() + ' Variation id "$vari" is invalid, skipping...');
continue;
}
@ -162,19 +162,19 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
if (variMeta != null)
{
_metadata.set(variMeta.variation, variMeta);
trace(' Loaded variation: $vari');
trace(' Loaded variation: $vari');
}
else
{
FlxG.log.warn('[SONG] Failed to load variation metadata (${id}:${vari}), is the path correct?');
trace(' FAILED to load variation: $vari');
trace(' FAILED to load variation: $vari');
}
}
}
if (_metadata.size() == 0)
{
trace('[WARN] Could not find song data for songId: $id');
trace(' WARNING '.bold().bg_yellow() + ' Could not find song data for songId: $id');
return;
}
@ -193,15 +193,15 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
* @param validScore Whether the song is elegible for highscores.
* @return The constructed song object.
*/
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>,
includeScript:Bool = true, validScore:Bool = false):Song
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variation:String, charts:Map<String, SongChartData>, includeScript:Bool = true,
validScore:Bool = false):Song
{
@:privateAccess
var result:Null<Song> = null;
if (includeScript && SongRegistry.instance.isScriptedEntry(songId))
if (includeScript && SongRegistry.instance.isScriptedEntry(songId, {variation: variation}))
{
var songClassName:Null<String> = SongRegistry.instance.getScriptedEntryClassName(songId);
var songClassName:Null<String> = SongRegistry.instance.getScriptedEntryClassName(songId, {variation: variation});
@:privateAccess
if (songClassName != null) result = SongRegistry.instance.createScriptedEntry(songClassName);
}

View file

@ -203,13 +203,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
}
}
public function hasAnimation(id:String):Bool
{
if (this.animation == null) return false;
return this.animation.getByName(id) != null;
}
/**
* Ensure that a given animation exists before playing it.
* Will gracefully check for name, then name with stripped suffixes, then fail to play.
@ -327,27 +320,11 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
this.animOffsets = offsets;
}
public function isAnimationFinished():Bool
{
return this.animation?.finished ?? false;
}
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
{
animationOffsets.set(name, [xOffset, yOffset]);
}
/**
* Returns the name of the animation that is currently playing.
* If no animation is playing (usually this means the character is BROKEN!),
* returns an empty string to prevent NPEs.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
// override getScreenPosition (used by FlxSprite's draw method) to account for animation offsets.
override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
{

View file

@ -161,13 +161,13 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
for (dataProp in _data.props)
{
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
var isSolidColor = dataProp.assetPath.startsWith('#');
var isAnimated = dataProp.animations.length > 0;
var propSprite:StageProp;
if (dataProp.danceEvery != 0)
if (dataProp.danceEvery != 0 || isAnimated)
{
propSprite = new Bopper(dataProp.danceEvery);
}
@ -183,6 +183,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
{
case 'packer':
propSprite.loadPacker(dataProp.assetPath);
case 'animateatlas':
propSprite.loadTextureAtlas(dataProp.assetPath, _data.directory, cast dataProp.atlasSettings);
default: // 'sparrow'
propSprite.loadSparrow(dataProp.assetPath);
}
@ -215,7 +217,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
if (propSprite.frames == null || propSprite.frames.numFrames == 0)
{
@:privateAccess
trace(' ERROR: Could not build texture for prop. Check the asset path (${Paths.currentLevel ?? 'default'}, ${dataProp.assetPath}).');
trace(' ERROR: Could not build texture for prop. Check the asset path (${Paths.currentLevel ?? 'default'}, ${dataProp.assetPath}).');
continue;
}
@ -268,6 +270,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
cast(propSprite, Bopper).setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]);
}
}
case 'animateatlas':
FlxAnimationUtil.addTextureAtlasAnimations(propSprite, dataProp.animations);
default: // 'sparrow'
FlxAnimationUtil.addAtlasAnimations(propSprite, dataProp.animations);
if (Std.isOfType(propSprite, Bopper))
@ -293,9 +297,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
}
}
if (dataProp.startingAnimation != null)
if (dataProp.startingAnimation != null && propSprite is Bopper)
{
propSprite.animation.play(dataProp.startingAnimation);
cast(propSprite, Bopper).playAnimation(dataProp.startingAnimation);
}
if (Std.isOfType(propSprite, BaseCharacter))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
package funkin.save;
import flixel.util.FlxSignal.FlxTypedSignal;
@:nullSafety
class SaveProperty<T>
{
var _value:T;
var _getter:Null<Void->T>;
var _setter:Null<T->Void>;
var _autoFlush:Bool;
public var onChange(default, null):FlxTypedSignal<T->Void>;
public var value(get, set):T;
function get_value():T
{
return _getter != null ? _getter() : _value;
}
function set_value(newValue:T):T
{
var oldValue:T = get_value();
if (oldValue != newValue)
{
if (_setter != null) _setter(newValue);
else
_value = newValue;
if (_autoFlush) Save.system.flush();
trace('[Save Property]: Changing value of $oldValue TO $newValue');
onChange.dispatch(newValue);
}
return newValue;
}
public function new(initialValue:T, ?getter:Void->T, ?setter:T->Void, autoFlush:Bool = true)
{
_value = initialValue;
_getter = getter;
_setter = setter;
_autoFlush = autoFlush;
onChange = new FlxTypedSignal<T->Void>();
}
public function bind(callback:T->Void, fireImmediately:Bool = true):Void
{
onChange.add(callback);
if (fireImmediately) callback(get_value());
}
public function bindOnce(callback:T->Void, fireImmediately:Bool = true):Void
{
onChange.addOnce(callback);
if (fireImmediately) callback(get_value());
}
public function unbind(callback:T->Void):Void
{
onChange.remove(callback);
}
public function unbindAll():Void
{
onChange.removeAll();
}
public function destroy():Void
{
onChange.destroy();
_getter = null;
_setter = null;
}
@:op(A == B)
public function equals(other:T):Bool
{
return get_value() == other;
}
@:op(A != B)
public function notEquals(other:T):Bool
{
return get_value() != other;
}
public function toString():String
{
return Std.string(get_value());
}
}

View file

@ -0,0 +1,81 @@
package funkin.save;
import funkin.save.migrator.RawSaveData_v1_0_0;
import funkin.save.migrator.SaveDataMigrator;
import flixel.util.FlxSave;
/**
* A bit more of the backend and nitty gritty of FNF's save system
*/
class SaveSystem
{
public function new():Void {}
/**
* Call this to make sure the save data is written to disk.
*/
public function flush():Void
{
FlxG.save.flush();
}
public function clearSlot(slot:Int):Save
{
FlxG.save.bind(Constants.SAVE_NAME + slot, Constants.SAVE_PATH);
if (FlxG.save.status == EMPTY) return new Save();
// Archive the save data just in case.
// Not reliable but better than nothing.
var backupSlot:Int = Save.system.archiveBadSaveData(FlxG.save.data);
FlxG.save.erase();
return new Save();
}
public function fetchLegacySaveData():Option<RawSaveData_v1_0_0>
{
trace("[SAVE] Checking for legacy save data...");
var legacySave:FlxSave = new FlxSave();
legacySave.bind(Constants.SAVE_NAME_LEGACY, Constants.SAVE_PATH_LEGACY);
if (legacySave.isEmpty())
{
trace("[SAVE] No legacy save data found.");
return None;
}
else
{
trace("[SAVE] Legacy save data found.");
return Some(cast legacySave.data);
}
}
public function archiveBadSaveData(data:Dynamic):Int
{
// We want to save this somewhere so we can try to recover it for the user in the future!
final RECOVERY_SLOT_START = 1000;
return writeToAvailableSlot(RECOVERY_SLOT_START, data);
}
function writeToAvailableSlot(slot:Int, data:Dynamic):Int
{
trace('[SAVE] Finding slot to write data to (starting with ${slot})...');
var targetSaveData:FlxSave = new FlxSave();
targetSaveData.bind(Constants.SAVE_NAME + slot, Constants.SAVE_PATH);
while (!targetSaveData.isEmpty())
{
// Keep trying to bind to slots until we find an empty slot.
trace('[SAVE] Slot ${slot} is taken, continuing...');
slot++;
targetSaveData.bind(Constants.SAVE_NAME + slot, Constants.SAVE_PATH);
}
trace('[SAVE] Writing data to slot ${slot}...');
targetSaveData.mergeData(data, true);
trace('[SAVE] Data written to slot ${slot}!');
return slot;
}
}

View file

@ -19,7 +19,7 @@ class SaveDataMigrator
{
trace('[SAVE] No version found in save data! Returning blank data.');
trace(inputData);
return new Save(Save.getDefault());
return new Save(Save.getDefaultData());
}
else
{
@ -28,7 +28,7 @@ class SaveDataMigrator
if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
{
// Import the structured data.
var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData);
var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefaultData(), inputData);
var save:Save = new Save(saveDataWithDefaults);
return save;
}
@ -38,11 +38,13 @@ class SaveDataMigrator
}
else
{
var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.';
var slot:Int = Save.archiveBadSaveData(inputData);
var fullMessage:String = 'An error occurred migrating your save data.\n${message}\nInvalid data has been moved to save slot ${slot}.';
funkin.util.WindowUtil.showError("Save Data Failure", fullMessage);
return new Save(Save.getDefault());
var slot:Int = Save.system.archiveBadSaveData(inputData);
var message:String = 'An error occurred migrating your save data.'
+ '\nError migrating save data, expected ${Save.SAVE_DATA_VERSION}.'
+ '\nInvalid data has been moved to save slot ${slot}.';
funkin.util.WindowUtil.showError("Save Data Failure", message);
return new Save(Save.getDefaultData());
}
}
}
@ -50,7 +52,7 @@ class SaveDataMigrator
static function migrate_v2_0_0(inputData:Dynamic):Save
{
// Import the structured data.
var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData);
var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefaultData(), inputData);
// Reset these values to valid ones.
saveDataWithDefaults.optionsChartEditor.chartEditorLiveInputStyle = funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle.None;
@ -68,12 +70,12 @@ class SaveDataMigrator
{
var inputSaveData:RawSaveData_v1_0_0 = cast inputData;
var result:Save = new Save(Save.getDefault());
var result:Save = new Save(Save.getDefaultData());
result.volume = inputSaveData.volume;
result.mute = inputSaveData.mute;
result.volume.value = inputSaveData.volume;
result.mute.value = inputSaveData.mute;
result.ngSessionId = inputSaveData.sessionId;
result.ngSessionId.value = inputSaveData.sessionId;
// TODO: Port over the save data from the legacy save data format.
migrateLegacyScores(result, inputSaveData);

View file

@ -4,10 +4,13 @@ import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.math.FlxPoint;
import flixel.util.FlxAxes;
import flixel.util.FlxHorizontalAlign;
import flixel.util.FlxVerticalAlign;
import flixel.FlxG;
import openfl.display.Bitmap;
import openfl.display.BitmapData;
import funkin.util.MathUtil;
import funkin.graphics.FunkinCamera;
class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
{
@ -95,6 +98,15 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
@:noCompletion
private static var cutoutBitmaps:Array<Bitmap> = [null, null];
@:noCompletion
private static var mustAwait:Bool = false;
@:noCompletion
private static var awaitedSize:FlxPoint = FlxPoint.get(0, 0);
@:noCompletion
private static var finishingAwait:Bool = false;
/**
* Constructor for `FullScreenScaleMode`.
*
@ -119,6 +131,61 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
*/
override public function onMeasure(Width:Int, Height:Int):Void
{
if (mustAwait)
{
onMeasureAwait(Width, Height);
}
else
{
onMeasureInstant(Width, Height);
mustAwait = true;
}
}
/**
* Locks the game to the current aspect ratio and assignes the requested resolution as awaited for later.
* @param Width The width of the screen.
* @param Height The height of the screen.
*/
public function onMeasureAwait(Width:Int, Height:Int):Void
{
horizontalAlign = CENTER;
verticalAlign = CENTER;
updateGameSize(FlxG.width, FlxG.height);
updateDeviceSize(Width, Height);
#if mobile
updateDeviceNotch(funkin.mobile.util.ScreenUtil.getNotchRect());
#end
updateScaleOffset();
updateGamePosition();
awaitedSize.set(Width, Height);
}
/**
* Unlock the game resolution and swap into the awaited one.
*/
public function onMeasurePostAwait():Void
{
if (awaitedSize.x == 0 && awaitedSize.y == 0) return;
horizontalAlign = enabled ? LEFT : CENTER;
verticalAlign = enabled ? TOP : CENTER;
onMeasureInstant(Math.ceil(awaitedSize.x), Math.ceil(awaitedSize.y));
FlxG.cameras.reset(new FunkinCamera('default'));
awaitedSize.set(0, 0);
}
/**
* Instantly apply the measured resolution to the game
* @param Width The width of the screen.
* @param Height The height of the screen.
*/
public function onMeasureInstant(Width:Int, Height:Int):Void
{
finishingAwait = true;
untyped FlxG.width = FlxG.initialWidth;
untyped FlxG.height = FlxG.initialHeight;
@ -132,6 +199,8 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
updateGamePosition();
adjustGameSize();
finishingAwait = false;
}
/**
@ -204,7 +273,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
{
if (bitmap == null)
{
trace("[WARNING] Tried to remove a cutout bar but there don't seem to be any.");
trace(" WARNING ".bg_yellow().bold() + " Tried to remove a cutout bar but there don't seem to be any.");
continue;
}
@ -265,12 +334,50 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
override public function updateScaleOffset():Void
{
scale.x = ratioAxis == X ? logicalSize.x / FlxG.width : deviceSize.x / FlxG.width;
scale.y = ratioAxis == Y ? logicalSize.y / FlxG.height : deviceSize.y / FlxG.height;
if (finishingAwait)
{
scale.x = ratioAxis == X ? logicalSize.x / FlxG.width : deviceSize.x / FlxG.width;
scale.y = ratioAxis == Y ? logicalSize.y / FlxG.height : deviceSize.y / FlxG.height;
}
else
{
scale.x = deviceSize.x / FlxG.width;
scale.y = deviceSize.y / FlxG.height;
if (scale.x > scale.y) scale.x = scale.y;
else
scale.y = scale.x;
}
updateOffsetX();
updateOffsetY();
}
override function updateOffsetX():Void
{
offset.x = switch (horizontalAlign)
{
case FlxHorizontalAlign.LEFT:
0;
case FlxHorizontalAlign.CENTER:
Math.ceil(finishingAwait ? (deviceSize.x - gameSize.x) : (deviceSize.x - (gameSize.x * scale.x)) * 0.5);
case FlxHorizontalAlign.RIGHT:
deviceSize.x - gameSize.x;
}
}
override function updateOffsetY():Void
{
offset.y = switch (verticalAlign)
{
case FlxVerticalAlign.TOP:
0;
case FlxVerticalAlign.CENTER:
Math.ceil(finishingAwait ? (deviceSize.y - gameSize.y) : (deviceSize.y - (gameSize.y * scale.y)) * 0.5);
case FlxVerticalAlign.BOTTOM:
deviceSize.y - gameSize.y;
}
}
#if mobile
private function updateDeviceNotch(notch:lime.math.Rectangle):Void
{
@ -319,7 +426,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
var gameHeight:Float = gameSize.y / scale.y;
#if desktop
if (MathUtil.gcd(FlxG.width, Math.ceil(gameHeight)) == 1)
if (MathUtil.gcd(FlxG.width, Math.ceil(gameHeight)) == 1 || maxRatioAxis != ratioAxis)
{
gameSize.y -= cutoutSize.y;
offset.y = Math.ceil((deviceSize.y - gameSize.y) * 0.5);
@ -357,7 +464,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
var gameWidth:Float = gameSize.x / scale.x;
#if desktop
if (MathUtil.gcd(Math.ceil(gameWidth), FlxG.height) == 1)
if (MathUtil.gcd(Math.ceil(gameWidth), FlxG.height) == 1 || maxRatioAxis != ratioAxis)
{
gameSize.x -= cutoutSize.x;
offset.x = Math.ceil((deviceSize.x - gameSize.x) * 0.5);
@ -409,6 +516,7 @@ class FullScreenScaleMode extends flixel.system.scaleModes.BaseScaleMode
if (instance != null)
{
mustAwait = false;
instance.horizontalAlign = enabled ? LEFT : CENTER;
instance.verticalAlign = enabled ? TOP : CENTER;
instance.onMeasure(FlxG.stage.stageWidth, FlxG.stage.stageHeight);

View file

@ -217,7 +217,7 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
#end
// Todo: bypass popup blocker on firefox
if (controls.ACCEPT) accept();
if (controls.ACCEPT_P) accept();
return;
}

View file

@ -14,6 +14,7 @@ import funkin.modding.module.ModuleHandler;
import funkin.util.SortUtil;
import funkin.util.WindowUtil;
import funkin.input.Controls;
import funkin.ui.FullScreenScaleMode;
#if mobile
import funkin.graphics.FunkinCamera;
import funkin.mobile.ui.FunkinHitbox;
@ -55,6 +56,8 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
public function new()
{
if (FullScreenScaleMode.instance != null) FullScreenScaleMode.instance.onMeasurePostAwait();
super();
initCallbacks();

View file

@ -48,7 +48,7 @@ class Page<T:PageName> extends FlxGroup
function updateEnabled(elapsed:Float)
{
if (canExit && controls.BACK)
if (canExit && controls.BACK_P)
{
exit();
FunkinSound.playOnce(Paths.sound('cancelMenu'));

View file

@ -40,7 +40,7 @@ class PixelatedIcon extends FlxFilteredSprite
if (!Assets.exists(Paths.image(charPath)))
{
trace('[WARN] Character ${char} has no freeplay icon.');
trace(' WARNING '.bold().bg_yellow() + ' Character ${char} has no freeplay icon.');
this.visible = false;
return;
}

View file

@ -1,18 +0,0 @@
package funkin.ui.charSelect;
import flixel.FlxSprite;
@:nullSafety
class CharIcon extends FlxSprite
{
public var locked:Bool = false;
public function new(x:Float, y:Float, locked:Bool = false)
{
super(x, y);
this.locked = locked;
makeGraphic(128, 128);
}
}

View file

@ -1,50 +0,0 @@
package funkin.ui.charSelect;
import openfl.display.BitmapData;
import openfl.filters.DropShadowFilter;
import openfl.filters.ConvolutionFilter;
import funkin.graphics.shaders.StrokeShader;
@:nullSafety
class CharIconCharacter extends CharIcon
{
// public var dropShadowFilter:DropShadowFilter;
var matrixFilter:Array<Float> = [
1, 1, 1,
1, 1, 1,
1, 1, 1
];
var divisor:Int = 1;
var bias:Int = 0;
// var convolutionFilter:ConvolutionFilter;
// public var noDropShadow:BitmapData;
// public var withDropShadow:BitmapData;
// var strokeShader:StrokeShader;
public function new(path:String)
{
super(0, 0, false);
loadGraphic(Paths.image('freeplay/icons/' + path + 'pixel'));
setGraphicSize(128, 128);
updateHitbox();
antialiasing = false;
// strokeShader = new StrokeShader();
// shader = strokeShader;
// noDropShadow = pixels.clone();
// dropShadowFilter = new DropShadowFilter(5, 45, 0, 1, 0, 0);
// convolutionFilter = new ConvolutionFilter(3, 3, matrixFilter, divisor, bias);
// pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), dropShadowFilter);
// pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), convolutionFilter);
// withDropShadow = pixels.clone();
// pixels = noDropShadow.clone();
}
}

View file

@ -1,4 +0,0 @@
package funkin.ui.charSelect;
@:nullSafety
class CharIconLocked extends CharIcon {}

View file

@ -0,0 +1,45 @@
package funkin.ui.charSelect;
import animate.FlxAnimateFrames;
import flixel.FlxG;
/**
* Utility class for handling the atlases loaded by CharSelect & co. in an efficient way.
* TODO: Maybe this should be a general utility class instead?
*/
class CharSelectAtlasHandler
{
static final framesCache:Map<String, FlxAnimateFrames> = [];
public static function loadAtlas(path:String, ?settings:FlxAnimateSettings):Null<FlxAnimateFrames>
{
if (framesCache.exists(path)) return framesCache.get(path);
var result:FlxAnimateFrames = FlxAnimateFrames.fromAnimate(Paths.animateAtlas(path),
{
swfMode: settings?.swfMode ?? true,
filterQuality: settings?.filterQuality ?? MEDIUM,
cacheOnLoad: settings?.cacheOnLoad ?? false
});
if (result == null)
{
FlxG.log.error('Failed to load atlas at path $path!');
return null;
}
result.parent.destroyOnNoUse = false;
framesCache.set(path, result);
return result;
}
public static function clearAtlasCache():Void
{
for (frames in framesCache.iterator())
{
// NOTE: Doing this already calls checkUseCount!
frames.parent.destroyOnNoUse = true;
}
framesCache.clear();
}
}

View file

@ -0,0 +1,136 @@
package funkin.ui.charSelect;
import funkin.graphics.FunkinSprite;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import flixel.math.FlxPoint;
import openfl.display.BlendMode;
import flixel.group.FlxSpriteContainer.FlxTypedSpriteContainer;
import funkin.util.MathUtil;
class CharSelectCursors extends FlxTypedSpriteContainer<FunkinSprite>
{
/**
* The main cursor sprite for this class.
*/
public var main:FunkinSprite;
var lightBlue:FunkinSprite;
var darkBlue:FunkinSprite;
var cursorConfirmed:FunkinSprite;
var cursorDenied:FunkinSprite;
public function new()
{
super();
darkBlue = new FunkinSprite(0, 0);
lightBlue = new FunkinSprite(0, 0);
main = new FunkinSprite(0, 0);
cursorConfirmed = new FunkinSprite(0, 0);
cursorDenied = new FunkinSprite(0, 0);
darkBlue.loadGraphic(Paths.image('charSelect/charSelector'));
lightBlue.loadGraphic(Paths.image('charSelect/charSelector'));
main.loadGraphic(Paths.image('charSelect/charSelector'));
darkBlue.color = 0xFF3C74F7;
lightBlue.color = 0xFF3EBBFF;
main.color = 0xFFFFFF00;
FlxTween.color(main, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: PINGPONG});
darkBlue.blend = BlendMode.SCREEN;
lightBlue.blend = BlendMode.SCREEN;
add(darkBlue);
add(lightBlue);
add(main);
cursorConfirmed.frames = Paths.getSparrowAtlas("charSelect/charSelectorConfirm");
cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED instance 1", 24, true);
cursorConfirmed.visible = false;
add(cursorConfirmed);
cursorDenied.frames = Paths.getSparrowAtlas("charSelect/charSelectorDenied");
cursorDenied.animation.addByPrefix("idle", "cursor DENIED instance 1", 24, false);
cursorDenied.visible = false;
add(cursorDenied);
scrollFactor.set();
directAlpha = true;
}
public function confirm():Void
{
cursorConfirmed.visible = true;
cursorConfirmed.animation.play("idle", true);
main.visible = lightBlue.visible = darkBlue.visible = false;
}
public function resetDeny():Void
{
cursorDenied.visible = false;
}
public function deny():Void
{
cursorDenied.visible = true;
cursorDenied.animation.play('idle', true);
cursorDenied.animation.onFinish.add((_) -> {
cursorDenied.visible = false;
});
}
public function unconfirm():Void
{
cursorConfirmed.visible = false;
main.visible = lightBlue.visible = darkBlue.visible = true;
}
/**
* Snaps the cursors to the given position.
* @param intendedPosition The position to snap to as a `FlxPoint`.
*/
public function snapToLocation(intendedPosition:FlxPoint):Void
{
main.x = intendedPosition.x;
main.y = intendedPosition.y;
lightBlue.x = main.x;
lightBlue.y = main.y;
darkBlue.x = intendedPosition.x;
darkBlue.y = intendedPosition.y;
cursorConfirmed.x = main.x - 2;
cursorConfirmed.y = main.y - 4;
cursorDenied.x = main.x - 2;
cursorDenied.y = main.y - 4;
}
/**
* Lerps the cursors to the given position.
* @param intendedPosition The position to lerp to as a `FlxPoint`.
*/
public function lerpToLocation(intendedPosition:FlxPoint):Void
{
main.x = MathUtil.snap(MathUtil.smoothLerpPrecision(main.x, intendedPosition.x, FlxG.elapsed, 0.1), intendedPosition.x, 1);
main.y = MathUtil.snap(MathUtil.smoothLerpPrecision(main.y, intendedPosition.y, FlxG.elapsed, 0.1), intendedPosition.y, 1);
lightBlue.x = MathUtil.smoothLerpPrecision(lightBlue.x, main.x, FlxG.elapsed, 0.202);
lightBlue.y = MathUtil.smoothLerpPrecision(lightBlue.y, main.y, FlxG.elapsed, 0.202);
darkBlue.x = MathUtil.smoothLerpPrecision(darkBlue.x, intendedPosition.x, FlxG.elapsed, 0.404);
darkBlue.y = MathUtil.smoothLerpPrecision(darkBlue.y, intendedPosition.y, FlxG.elapsed, 0.404);
cursorConfirmed.x = main.x - 2;
cursorConfirmed.y = main.y - 4;
cursorDenied.x = main.x - 2;
cursorDenied.y = main.y - 4;
}
}

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