diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml index 38a504442..0cc544cf7 100644 --- a/.github/actions/setup-haxeshit/action.yml +++ b/.github/actions/setup-haxeshit/action.yml @@ -3,18 +3,30 @@ description: "sets up haxe shit, using HMM!" runs: using: "composite" steps: - - uses: krdlab/setup-haxe@v1.5.1 - with: - haxe-version: 4.3.1 - - name: Config haxelib - run: | - haxelib config - shell: bash - - name: Installing Haxe lol - run: | - haxe -version - haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git development - haxelib version - haxelib --global install hmm - haxelib --global run hmm install --quiet - shell: bash + - uses: krdlab/setup-haxe@v1.5.1 + with: + haxe-version: 4.3.1 + - name: Config haxelib + run: | + haxelib config + shell: bash + - name: Installing Haxe lol + run: | + haxe -version + haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git development + haxelib version + haxelib --global install hmm + shell: bash + - name: dependency install cache + id: cache-hmm + uses: actions/cache@v3 + with: + path: .haxelib + key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }} + restore-keys: | + ${{ runner.os }}-hmm- + - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} + name: hmm install + run: | + haxelib --global run hmm install + shell: bash diff --git a/.github/actions/upload-itch/action.yml b/.github/actions/upload-itch/action.yml index 5abc31b16..7a4b45427 100644 --- a/.github/actions/upload-itch/action.yml +++ b/.github/actions/upload-itch/action.yml @@ -36,9 +36,9 @@ runs: ./butler -V shell: bash - name: Upload game to itch.io - env: + env: BUTLER_API_KEY: ${{inputs.butler-key}} run: | ./butler login - ./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF##*/} - shell: bash \ No newline at end of file + ./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME} + shell: bash diff --git a/.github/hooks/README.md b/.github/hooks/README.md new file mode 100644 index 000000000..544fbf365 --- /dev/null +++ b/.github/hooks/README.md @@ -0,0 +1,5 @@ +# Git Hooks +These work even on Windows because of Git Bash. + +## Setup +`git config core.hooksPath .github/hooks` diff --git a/.github/hooks/post-checkout b/.github/hooks/post-checkout new file mode 100644 index 000000000..12358c998 --- /dev/null +++ b/.github/hooks/post-checkout @@ -0,0 +1,2 @@ +#!/bin/sh +git submodule update --init --recursive diff --git a/.github/hooks/post-merge b/.github/hooks/post-merge new file mode 100644 index 000000000..12358c998 --- /dev/null +++ b/.github/hooks/post-merge @@ -0,0 +1,2 @@ +#!/bin/sh +git submodule update --init --recursive diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push new file mode 100644 index 000000000..ec4c820ac --- /dev/null +++ b/.github/hooks/pre-push @@ -0,0 +1,5 @@ +#!/bin/sh +if git diff --cached --submodule | grep -q "^+"; then + echo "WARNING: You have unpushed changes in submodules." + exit 1 +fi diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index ed509b44d..809a8b94b 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -26,9 +26,11 @@ jobs: - uses: actions/checkout@v3 with: submodules: 'recursive' + token: ${{ secrets.GH_RO_PAT }} - uses: ./.github/actions/setup-haxeshit - name: Build game run: | + sudo apt-get update sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev haxelib run lime build html5 -release --times ls @@ -48,28 +50,47 @@ jobs: - uses: actions/checkout@v3 with: submodules: 'recursive' + token: ${{ secrets.GH_RO_PAT }} - uses: ./.github/actions/setup-haxeshit + - name: Make HXCPP cache dir + shell: bash + run: | + mkdir -p ${{ runner.temp }}\\hxcpp_cache + - name: Restore build cache + id: cache-build-win + uses: actions/cache@v3 + with: + path: | + .haxelib + export + ${{ runner.temp }}\\hxcpp_cache + key: ${{ runner.os }}-build-win-${{ github.ref_name }} + restore-keys: | + ${{ runner.os }}-build-win- - name: Build game run: | haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER dir + env: + HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache" - uses: ./.github/actions/upload-itch with: - butler-key: ${{ secrets.BUTLER_API_KEY}} + butler-key: ${{ secrets.BUTLER_API_KEY }} build-dir: export/release/windows/bin target: win - test-unit-win: - needs: create-nightly-win - runs-on: windows-latest - permissions: - contents: write - actions: write - steps: - - uses: actions/checkout@v3 - with: - submodules: 'recursive' - - uses: ./.github/actions/setup-haxeshit - - name: Run unit tests - run: | - cd ./tests/unit/ - ./start-win-native.bat +# test-unit-win: +# needs: create-nightly-win +# runs-on: windows-latest +# permissions: +# contents: write +# actions: write +# steps: +# - uses: actions/checkout@v3 +# with: +# submodules: 'recursive' +# token: ${{ secrets.GH_RO_PAT }} +# - uses: ./.github/actions/setup-haxeshit +# - name: Run unit tests +# run: | +# cd ./tests/unit/ +# ./start-win-native.bat diff --git a/.gitignore b/.gitignore index d4aba58ac..b2fe731ea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ APIStuff.hx dump/ export/ -RECOVER_*.fla \ No newline at end of file +RECOVER_*.fla +shitAudio/ diff --git a/art b/art index 1656bea53..d1aa2c6e8 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit 1656bea5370c65879aaeb323e329f403c78071c5 +Subproject commit d1aa2c6e81c0ddff8af3d6aac4700590cc5b0ef4 diff --git a/assets b/assets index a62e7e50d..8dfc578a0 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit a62e7e50d59c14d256c75da651b79dea77e1620e +Subproject commit 8dfc578a03fb88c6b93777a2518ee9cd2213dadd diff --git a/hmm.json b/hmm.json index 47460facf..3f420ac48 100644 --- a/hmm.json +++ b/hmm.json @@ -32,7 +32,7 @@ "name": "flxanimate", "type": "git", "dir": null, - "ref": "a9136359271cae6ea3016b7fd9023c5c42562933", + "ref": "dd2903f7dc7024335b981edf2a770760cec912e1", "url": "https://github.com/ninjamuffin99/flxanimate" }, { @@ -97,8 +97,8 @@ "name": "json2object", "type": "git", "dir": null, - "ref": "429986134031cbb1980f09d0d3d642b4b4cbcd6a", - "url": "https://github.com/elnabo/json2object" + "ref": "f4df19cfa196f85eece55c3367021fc965f1fa9a", + "url": "https://github.com/EliteMasterEric/json2object" }, { "name": "lime", @@ -139,7 +139,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08", + "ref": "de9395d2f367a80f93f082e1b639b9cde2258bf1", "url": "https://github.com/EliteMasterEric/openfl" }, { @@ -160,4 +160,4 @@ "version": "0.11.0" } ] -} \ No newline at end of file +} diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index 00d7422c8..95c9b9a00 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -1,43 +1,48 @@ package funkin; -import funkin.ui.StickerSubState; import flash.text.TextField; +import flixel.addons.display.FlxGridOverlay; +import flixel.addons.transition.FlxTransitionableState; +import flixel.addons.ui.FlxInputText; import flixel.FlxCamera; import flixel.FlxGame; import flixel.FlxSprite; import flixel.FlxState; -import flixel.addons.display.FlxGridOverlay; -import flixel.addons.transition.FlxTransitionableState; -import flixel.addons.ui.FlxInputText; -import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup; import flixel.input.touch.FlxTouch; import flixel.math.FlxAngle; import flixel.math.FlxMath; import flixel.math.FlxPoint; +import flixel.system.debug.watch.Tracker.TrackerProfile; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; -import funkin.data.song.SongRegistry; -import funkin.save.Save; -import funkin.save.Save.SaveScoreData; import flixel.util.FlxSpriteUtil; import flixel.util.FlxTimer; import funkin.Controls.Control; +import funkin.data.level.LevelRegistry; +import funkin.data.song.SongRegistry; import funkin.freeplayStuff.BGScrollingText; +import funkin.freeplayStuff.DifficultyStars; import funkin.freeplayStuff.DJBoyfriend; import funkin.freeplayStuff.FreeplayScore; import funkin.freeplayStuff.LetterSort; import funkin.freeplayStuff.SongMenuItem; +import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.play.HealthIcon; import funkin.play.PlayState; -import funkin.shaderslmfao.AngleMask; -import funkin.shaderslmfao.PureColor; -import funkin.shaderslmfao.StrokeShader; import funkin.play.PlayStatePlaylist; import funkin.play.song.Song; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; +import funkin.shaderslmfao.AngleMask; +import funkin.shaderslmfao.HSVShader; +import funkin.shaderslmfao.PureColor; +import funkin.shaderslmfao.StrokeShader; +import funkin.ui.StickerSubState; import lime.app.Future; import lime.utils.Assets; @@ -45,7 +50,6 @@ class FreeplayState extends MusicBeatSubState { var songs:Array = []; - // var selector:FlxText; var curSelected:Int = 0; var curDifficulty:Int = 1; @@ -71,6 +75,7 @@ class FreeplayState extends MusicBeatSubState var grpSongs:FlxTypedGroup; var grpCapsules:FlxTypedGroup; + var curCapsule:SongMenuItem; var curPlaying:Bool = false; var dj:DJBoyfriend; @@ -103,8 +108,6 @@ class FreeplayState extends MusicBeatSubState openSubState(stickerSubState); stickerSubState.degenStickers(); - - // resetSubState(); } #if discord_rpc @@ -116,44 +119,27 @@ class FreeplayState extends MusicBeatSubState #if debug isDebug = true; - addSong('Test', 'tutorial', 'bf-pixel'); - addSong('Pyro', 'weekend1', 'darnell'); #end - var initSonglist = CoolUtil.coolTextFile(Paths.txt('freeplaySonglist')); - - for (i in 0...initSonglist.length) - { - songs.push(new FreeplaySongData(initSonglist[i], 'tutorial', 'gf')); - } - if (FlxG.sound.music != null) { if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu')); } - // if (StoryMenuState.weekUnlocked[2] || isDebug) - addWeek(['Bopeebo', 'Fresh', 'Dadbattle'], 'week1', ['dad']); + // Add a null entry that represents the RANDOM option + songs.push(null); - // if (StoryMenuState.weekUnlocked[2] || isDebug) - addWeek(['Spookeez', 'South', 'Monster'], 'week2', ['spooky', 'spooky', 'monster']); - - // if (StoryMenuState.weekUnlocked[3] || isDebug) - addWeek(['Pico', 'Philly-Nice', 'Blammed'], 'week3', ['pico']); - - // if (StoryMenuState.weekUnlocked[4] || isDebug) - addWeek(['Satin-Panties', 'High', 'MILF'], 'week4', ['mom']); - - // if (StoryMenuState.weekUnlocked[5] || isDebug) - addWeek(['Cocoa', 'Eggnog', 'Winter-Horrorland'], 'week5', ['parents-christmas', 'parents-christmas', 'monster-christmas']); - - // if (StoryMenuState.weekUnlocked[6] || isDebug) - addWeek(['Senpai', 'Roses', 'Thorns'], 'week6', ['senpai', 'senpai', 'spirit']); - - // if (StoryMenuState.weekUnlocked[7] || isDebug) - addWeek(['Ugh', 'Guns', 'Stress'], 'week7', ['tankman']); - - addWeek(["Darnell", "lit-up", "2hot", "blazin"], 'weekend1', ['darnell']); + // programmatically adds the songs via LevelRegistry and SongRegistry + for (coolWeek in LevelRegistry.instance.listBaseGameLevelIds()) + { + for (songId in LevelRegistry.instance.parseEntryData(coolWeek).songs) + { + var metadata = SongRegistry.instance.parseEntryMetadata(songId); + var char = metadata.playData.characters.opponent; + var songName = metadata.songName; + addSong(songId, songName, coolWeek, char); + } + } // LOAD MUSIC @@ -171,7 +157,7 @@ class FreeplayState extends MusicBeatSubState FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); add(pinkBack); - var orangeBackShit:FlxSprite = new FlxSprite(84, FlxG.height * 0.68).makeGraphic(Std.int(pinkBack.width), 50, 0xFFffd400); + var orangeBackShit:FlxSprite = new FlxSprite(84, 440).makeGraphic(Std.int(pinkBack.width), 75, 0xFFfeda00); add(orangeBackShit); var alsoOrangeLOL:FlxSprite = new FlxSprite(0, orangeBackShit.y).makeGraphic(100, Std.int(orangeBackShit.height), 0xFFffd400); @@ -193,9 +179,11 @@ class FreeplayState extends MusicBeatSubState add(grpTxtScrolls); grpTxtScrolls.visible = false; - var moreWays:BGScrollingText = new BGScrollingText(0, 200, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width); + FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ["x", "y", "speed", "size"])); + + var moreWays:BGScrollingText = new BGScrollingText(0, 160, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width, true, 43); moreWays.funnyColor = 0xFFfff383; - moreWays.speed = 4; + moreWays.speed = 6.8; grpTxtScrolls.add(moreWays); exitMovers.set([moreWays], @@ -204,9 +192,9 @@ class FreeplayState extends MusicBeatSubState speed: 0.4, }); - var funnyScroll:BGScrollingText = new BGScrollingText(0, 250, "BOYFRIEND", FlxG.width / 2); + var funnyScroll:BGScrollingText = new BGScrollingText(0, 220, "BOYFRIEND", FlxG.width / 2, false, 60); funnyScroll.funnyColor = 0xFFff9963; - funnyScroll.speed = -1; + funnyScroll.speed = -3.8; grpTxtScrolls.add(funnyScroll); exitMovers.set([funnyScroll], @@ -217,7 +205,8 @@ class FreeplayState extends MusicBeatSubState wait: 0 }); - var txtNuts:BGScrollingText = new BGScrollingText(0, 300, "PROTECT YO NUTS", FlxG.width / 2); + var txtNuts:BGScrollingText = new BGScrollingText(0, 285, "PROTECT YO NUTS", FlxG.width / 2, true, 43); + txtNuts.speed = 3.5; grpTxtScrolls.add(txtNuts); exitMovers.set([txtNuts], { @@ -225,9 +214,9 @@ class FreeplayState extends MusicBeatSubState speed: 0.4, }); - var funnyScroll2:BGScrollingText = new BGScrollingText(0, 340, "BOYFRIEND", FlxG.width / 2); + var funnyScroll2:BGScrollingText = new BGScrollingText(0, 335, "BOYFRIEND", FlxG.width / 2, false, 60); funnyScroll2.funnyColor = 0xFFff9963; - funnyScroll2.speed = -1.2; + funnyScroll2.speed = -3.8; grpTxtScrolls.add(funnyScroll2); exitMovers.set([funnyScroll2], @@ -236,9 +225,9 @@ class FreeplayState extends MusicBeatSubState speed: 0.5, }); - var moreWays2:BGScrollingText = new BGScrollingText(0, 400, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width); + var moreWays2:BGScrollingText = new BGScrollingText(0, 397, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width, true, 43); moreWays2.funnyColor = 0xFFfff383; - moreWays2.speed = 4.4; + moreWays2.speed = 6.8; grpTxtScrolls.add(moreWays2); exitMovers.set([moreWays2], @@ -247,9 +236,9 @@ class FreeplayState extends MusicBeatSubState speed: 0.4 }); - var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y, "BOYFRIEND", FlxG.width / 2); - funnyScroll3.funnyColor = 0xFFff9963; - funnyScroll3.speed = -0.8; + var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y + 10, "BOYFRIEND", FlxG.width / 2, 60); + funnyScroll3.funnyColor = 0xFFfea400; + funnyScroll3.speed = -3.8; grpTxtScrolls.add(funnyScroll3); exitMovers.set([funnyScroll3], @@ -258,7 +247,7 @@ class FreeplayState extends MusicBeatSubState speed: 0.3 }); - dj = new DJBoyfriend(0, -100); + dj = new DJBoyfriend(640, 366); exitMovers.set([dj], { x: -dj.width * 1.6, @@ -314,6 +303,49 @@ class FreeplayState extends MusicBeatSubState grpDifficulties.group.members[curDifficulty].visible = true; + var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll")); + albumArt.visible = false; + add(albumArt); + + exitMovers.set([albumArt], + { + x: FlxG.width, + speed: 0.4, + wait: 0 + }); + + var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1')); + var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite')); + var difficultyStars:DifficultyStars = new DifficultyStars(140, 39); + + difficultyStars.stars.visible = false; + albumTitle.visible = false; + albumArtist.visible = false; + + exitMovers.set([albumTitle], + { + x: FlxG.width, + speed: 0.2, + wait: 0.1 + }); + + exitMovers.set([albumArtist], + { + x: FlxG.width * 1.1, + speed: 0.2, + wait: 0.2 + }); + exitMovers.set([difficultyStars], + { + x: FlxG.width * 1.2, + speed: 0.2, + wait: 0.3 + }); + + add(albumTitle); + add(albumArtist); + add(difficultyStars); + var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 64, FlxColor.BLACK); overhangStuff.y -= overhangStuff.height; add(overhangStuff); @@ -357,6 +389,28 @@ class FreeplayState extends MusicBeatSubState txtCompletion.visible = false; add(txtCompletion); + var letterSort:LetterSort = new LetterSort(400, 75); + add(letterSort); + letterSort.visible = false; + + exitMovers.set([letterSort], + { + y: -100, + speed: 0.3 + }); + + letterSort.changeSelectionCallback = (str) -> { + switch (str) + { + case "fav": + generateSongList({filterType: FAVORITE}, true); + case "ALL": + generateSongList(null, true); + default: + generateSongList({filterType: REGEXP, filterData: str}, true); + } + }; + exitMovers.set([fp, txtCompletion, fnfHighscoreSpr], { x: FlxG.width, @@ -364,6 +418,23 @@ class FreeplayState extends MusicBeatSubState }); dj.onIntroDone.add(function() { + // when boyfriend hits dat shiii + + albumArt.visible = true; + albumArt.anim.play(""); + albumArt.anim.onComplete = function() { + albumArt.anim.pause(); + }; + + new FlxTimer().start(1, function(_) { + albumTitle.visible = true; + }); + + new FlxTimer().start(35 / 24, function(_) { + albumArtist.visible = true; + difficultyStars.stars.visible = true; + }); + FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut}); var diffSelLeft = new DifficultySelector(20, grpDifficulties.y - 10, false, controls); @@ -372,33 +443,14 @@ class FreeplayState extends MusicBeatSubState add(diffSelLeft); add(diffSelRight); + letterSort.visible = true; + exitMovers.set([diffSelLeft, diffSelRight], { x: -diffSelLeft.width * 2, speed: 0.26 }); - var letterSort:LetterSort = new LetterSort(300, 100); - add(letterSort); - - exitMovers.set([letterSort], - { - y: -100, - speed: 0.3 - }); - - letterSort.changeSelectionCallback = (str) -> { - switch (str) - { - case "fav": - generateSongList({filterType: FAVORITE}, true); - case "ALL": - generateSongList(null, true); - default: - generateSongList({filterType: STARTSWITH, filterData: str}, true); - } - }; - new FlxTimer().start(1 / 24, function(handShit) { fnfHighscoreSpr.visible = true; fnfFreeplay.visible = true; @@ -411,53 +463,28 @@ class FreeplayState extends MusicBeatSubState new FlxTimer().start(1.5 / 24, function(bold) { sillyStroke.width = 0; sillyStroke.height = 0; + changeSelection(); }); }); pinkBack.color = 0xFFffd863; - // fnfFreeplay.visible = true; bgDad.visible = true; orangeBackShit.visible = true; alsoOrangeLOL.visible = true; grpTxtScrolls.visible = true; }); - generateSongList(); - - // FlxG.sound.playMusic(Paths.music('title'), 0); - // FlxG.sound.music.fadeIn(2, 0, 0.8); - // selector = new FlxText(); - - // selector.size = 40; - // selector.text = ">"; - // add(selector); + generateSongList(null, false); var swag:Alphabet = new Alphabet(1, 0, "swag"); - // JUST DOIN THIS SHIT FOR TESTING!!! - /* - var md:String = Markdown.markdownToHtml(Assets.getText('CHANGELOG.md')); - - var texFel:TextField = new TextField(); - texFel.width = FlxG.width; - texFel.height = FlxG.height; - // texFel. - texFel.htmlText = md; - - FlxG.stage.addChild(texFel); - - trace(md); - */ - var funnyCam = new FlxCamera(0, 0, FlxG.width, FlxG.height); funnyCam.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(funnyCam); typing = new FlxInputText(100, 100); - // add(typing); typing.callback = function(txt, action) { - // generateSongList(new EReg(txt.trim(), "ig")); trace(action); }; @@ -468,17 +495,24 @@ class FreeplayState extends MusicBeatSubState public function generateSongList(?filterStuff:SongFilter, force:Bool = false) { - curSelected = 0; + curSelected = 1; - grpCapsules.clear(); + for (cap in grpCapsules.members) + cap.kill(); - // var regexp:EReg = regexp; var tempSongs:Array = songs; if (filterStuff != null) { switch (filterStuff.filterType) { + case REGEXP: + // filterStuff.filterData has a string with the first letter of the sorting range, and the second one + // this creates a filter to return all the songs that start with a letter between those two + var filterRegexp = new EReg("^[" + filterStuff.filterData + "].*", "i"); + tempSongs = tempSongs.filter(str -> { + return filterRegexp.match(str.songName); + }); case STARTSWITH: tempSongs = tempSongs.filter(str -> { return str.songName.toLowerCase().startsWith(filterStuff.filterData); @@ -494,74 +528,57 @@ class FreeplayState extends MusicBeatSubState } } + var hsvShader:HSVShader = new HSVShader(); + + var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); + randomCapsule.init(FlxG.width, 0, "Random"); + randomCapsule.onConfirm = function() { + trace("RANDOM SELECTED"); + }; + randomCapsule.y = randomCapsule.intendedY(0) + 10; + randomCapsule.targetPos.x = randomCapsule.x; + randomCapsule.alpha = 0.5; + randomCapsule.songText.visible = false; + randomCapsule.favIcon.visible = false; + randomCapsule.initJumpIn(0, force); + randomCapsule.hsvShader = hsvShader; + grpCapsules.add(randomCapsule); + for (i in 0...tempSongs.length) { - var funnyMenu:SongMenuItem = new SongMenuItem(FlxG.width, (i * 150) + 160, tempSongs[i].songName); + if (tempSongs[i] == null) continue; + + var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem); + + funnyMenu.init(FlxG.width, 0, tempSongs[i].songName); + if (tempSongs[i].songCharacter != null) funnyMenu.setCharacter(tempSongs[i].songCharacter); + funnyMenu.onConfirm = function() { + capsuleOnConfirmDefault(funnyMenu); + }; + funnyMenu.y = funnyMenu.intendedY(i + 1) + 10; funnyMenu.targetPos.x = funnyMenu.x; funnyMenu.ID = i; - funnyMenu.alpha = 0.5; + funnyMenu.capsule.alpha = 0.5; funnyMenu.songText.visible = false; funnyMenu.favIcon.visible = tempSongs[i].isFav; + funnyMenu.hsvShader = hsvShader; - // fp.updateScore(0); - - var maxTimer:Float = Math.min(i, 4); - - new FlxTimer().start((1 / 24) * maxTimer, function(doShit) { - funnyMenu.doJumpIn = true; - }); - - new FlxTimer().start((0.09 * maxTimer) + 0.85, function(lerpTmr) { - funnyMenu.doLerp = true; - }); - - if (!force) - { - new FlxTimer().start(((0.20 * maxTimer) / (1 + maxTimer)) + 0.75, function(swagShi) { - funnyMenu.songText.visible = true; - funnyMenu.alpha = 1; - }); - } + if (i < 8) funnyMenu.initJumpIn(Math.min(i, 4), force); else - { - funnyMenu.songText.visible = true; - funnyMenu.alpha = 1; - } + funnyMenu.forcePosition(); grpCapsules.add(funnyMenu); - - var songText:Alphabet = new Alphabet(0, (70 * i) + 30, tempSongs[i].songName, true, false); - songText.x += 100; - songText.isMenuItem = true; - songText.targetY = i; - - // grpSongs.add(songText); - - // songText.x += 40; - // DONT PUT X IN THE FIRST PARAMETER OF new ALPHABET() !! - // songText.screenCenter(X); } + FlxG.console.registerFunction("changeSelection", changeSelection); + changeSelection(); changeDiff(); } - public function addSong(songName:String, levelId:String, songCharacter:String) + public function addSong(songId:String, songName:String, levelId:String, songCharacter:String) { - songs.push(new FreeplaySongData(songName, levelId, songCharacter)); - } - - public function addWeek(songs:Array, levelId:String, ?songCharacters:Array) - { - if (songCharacters == null) songCharacters = ['bf']; - - var num:Int = 0; - for (song in songs) - { - addSong(song, levelId, songCharacters[num]); - - if (songCharacters.length != 1) num++; - } + songs.push(new FreeplaySongData(songId, songName, levelId, songCharacter)); } var touchY:Float = 0; @@ -584,28 +601,31 @@ class FreeplayState extends MusicBeatSubState if (FlxG.keys.justPressed.F) { - var realShit = curSelected; - songs[curSelected].isFav = !songs[curSelected].isFav; - if (songs[curSelected].isFav) + if (songs[curSelected] != null) { - FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, - { - ease: FlxEase.elasticOut, - onComplete: _ -> { - grpCapsules.members[realShit].favIcon.visible = true; - grpCapsules.members[realShit].favIcon.animation.play("fav"); - } + var realShit = curSelected; + songs[curSelected].isFav = !songs[curSelected].isFav; + if (songs[curSelected].isFav) + { + FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, + { + ease: FlxEase.elasticOut, + onComplete: _ -> { + grpCapsules.members[realShit].favIcon.visible = true; + grpCapsules.members[realShit].favIcon.animation.play("fav"); + } + }); + } + else + { + grpCapsules.members[realShit].favIcon.animation.play('fav', false, true); + new FlxTimer().start((1 / 24) * 14, _ -> { + grpCapsules.members[realShit].favIcon.visible = false; }); - } - else - { - grpCapsules.members[realShit].favIcon.animation.play('fav', false, true); - new FlxTimer().start((1 / 24) * 14, _ -> { - grpCapsules.members[realShit].favIcon.visible = false; - }); - new FlxTimer().start((1 / 24) * 24, _ -> { - FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut}); - }); + new FlxTimer().start((1 / 24) * 24, _ -> { + FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut}); + }); + } } } @@ -648,16 +668,7 @@ class FreeplayState extends MusicBeatSubState FlxG.watch.addQuick("LENGTH", length); FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle))); - // trace("ANGLE", Math.round(FlxAngle.asDegrees(angle))); } - - /* switch (inputID) - { - case FlxObject.UP: - return - case FlxObject.DOWN: - } - */ } if (FlxG.touches.getFirst() != null) @@ -693,7 +704,6 @@ class FreeplayState extends MusicBeatSubState touchY = touch.screenY; if (dyTouch != 0) dyTouch < 0 ? changeSelection(1) : changeSelection(-1); - // changeSelection(1); } } else @@ -816,82 +826,23 @@ class FreeplayState extends MusicBeatSubState { FlxG.switchState(new MainMenuState()); } - // - // close(); }); } if (accepted) { - // if (Assets.exists()) - - var poop:String = songs[curSelected].songName.toLowerCase(); - - // does not work properly, always just accidentally sets it to normal anyways! - /* if (!Assets.exists(Paths.json(songs[curSelected].songName + '/' + poop))) - { - // defaults to normal if HARD / EASY doesn't exist - // does not account if NORMAL doesn't exist! - FlxG.log.warn("CURRENT DIFFICULTY IS NOT CHARTED, DEFAULTING TO NORMAL!"); - poop = Highscore.formatSong(songs[curSelected].songName.toLowerCase(), 1); - curDifficulty = 1; - }*/ - - PlayStatePlaylist.isStoryMode = false; - var songId:String = songs[curSelected].songName.toLowerCase(); - var targetSong:Song = SongRegistry.instance.fetchEntry(songId); - var targetDifficulty:String = switch (curDifficulty) - { - case 0: - 'easy'; - case 1: - 'normal'; - case 2: - 'hard'; - default: 'normal'; - }; - - // TODO: Implement additional difficulties into the interface properly. - if (FlxG.keys.pressed.E) - { - targetDifficulty = 'erect'; - } - - // TODO: Implement Pico into the interface properly. - var targetCharacter:String = 'bf'; - if (FlxG.keys.pressed.P) - { - targetCharacter = 'pico'; - } - - PlayStatePlaylist.campaignId = songs[curSelected].levelId; - - // Visual and audio effects. - FlxG.sound.play(Paths.sound('confirmMenu')); - dj.confirm(); - - if (targetSong != null) - { - // Load and cache the song's charts. - // TODO: Do this in the loading state. - targetSong.cacheCharts(true); - } - - new FlxTimer().start(1, function(tmr:FlxTimer) { - LoadingState.loadAndSwitchState(new PlayState( - { - targetSong: targetSong, - targetDifficulty: targetDifficulty, - targetCharacter: targetCharacter, - }), true); - }); + grpCapsules.members[curSelected].onConfirm(); } } @:haxe.warning("-WDeprecated") override function switchTo(nextState:FlxState):Bool { - clearDaCache(songs[curSelected].songName); + var daSong = songs[curSelected]; + if (daSong != null) + { + clearDaCache(daSong.songName); + } return super.switchTo(nextState); } @@ -915,9 +866,18 @@ class FreeplayState extends MusicBeatSubState default: 'normal'; }; - var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songName, targetDifficulty); - intendedScore = songScore.score; - intendedCompletion = songScore.accuracy; + var daSong = songs[curSelected]; + if (daSong != null) + { + var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, targetDifficulty); + intendedScore = songScore?.score ?? 0; + intendedCompletion = songScore?.accuracy ?? 0.0; + } + else + { + intendedScore = 0; + intendedCompletion = 0.0; + } grpDifficulties.group.forEach(function(spr) { spr.visible = false; @@ -947,19 +907,66 @@ class FreeplayState extends MusicBeatSubState } } + function capsuleOnConfirmDefault(cap:SongMenuItem):Void + { + PlayStatePlaylist.isStoryMode = false; + + var songId:String = cap.songTitle.toLowerCase(); + var targetSong:Song = SongRegistry.instance.fetchEntry(songId); + var targetDifficulty:String = switch (curDifficulty) + { + case 0: + 'easy'; + case 1: + 'normal'; + case 2: + 'hard'; + default: 'normal'; + }; + + // TODO: Implement additional difficulties into the interface properly. + if (FlxG.keys.pressed.E) + { + targetDifficulty = 'erect'; + } + + // TODO: Implement Pico into the interface properly. + var targetCharacter:String = 'bf'; + if (FlxG.keys.pressed.P) + { + targetCharacter = 'pico'; + } + + PlayStatePlaylist.campaignId = songs[curSelected].levelId; + + // Visual and audio effects. + FlxG.sound.play(Paths.sound('confirmMenu')); + dj.confirm(); + + // Load and cache the song's charts. + // TODO: Do this in the loading state. + targetSong.cacheCharts(true); + + new FlxTimer().start(1, function(tmr:FlxTimer) { + LoadingState.loadAndSwitchState(new PlayState( + { + targetSong: targetSong, + targetDifficulty: targetDifficulty, + targetCharacter: targetCharacter, + }), true); + }); + } + function changeSelection(change:Int = 0) { - // fp.updateScore(12345); - - NGio.logEvent('Fresh'); - // NGio.logEvent('Fresh'); FlxG.sound.play(Paths.sound('scrollMenu'), 0.4); + // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName)); curSelected += change; - if (curSelected < 0) curSelected = grpCapsules.members.length - 1; - if (curSelected >= grpCapsules.members.length) curSelected = 0; + if (curSelected < 0) curSelected = grpCapsules.countLiving() - 1; + if (curSelected >= grpCapsules.countLiving()) curSelected = 0; var targetDifficulty:String = switch (curDifficulty) { @@ -972,28 +979,40 @@ class FreeplayState extends MusicBeatSubState default: 'normal'; }; - var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songName, targetDifficulty); - intendedScore = songScore.score; - intendedCompletion = songScore.accuracy; - - #if PRELOAD_ALL - // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0); - #end - - var bullShit:Int = 0; + var daSong = songs[curSelected]; + if (daSong != null) + { + var songScore:SaveScoreData = Save.get().getSongScore(daSong.songId, targetDifficulty); + intendedScore = songScore?.score ?? 0; + intendedCompletion = songScore?.accuracy ?? 0.0; + } + else + { + intendedScore = 0; + intendedCompletion = 0.0; + } for (index => capsule in grpCapsules.members) { - capsule.selected = false; + index += 1; - capsule.targetPos.y = ((index - curSelected) * 150) + 160; + capsule.selected = index == curSelected + 1; + + capsule.targetPos.y = capsule.intendedY(index - curSelected); capsule.targetPos.x = 270 + (60 * (Math.sin(index - curSelected))); - // capsule.targetPos.x = 320 + (40 * (index - curSelected)); if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure } - if (grpCapsules.members.length > 0) grpCapsules.members[curSelected].selected = true; + if (grpCapsules.countLiving() > 0) + { + if (curSelected == 0) + { + FlxG.sound.playMusic(Paths.music('freeplay/freeplayRandom'), 0); + FlxG.sound.music.fadeIn(2, 0, 0.8); + } + grpCapsules.members[curSelected].selected = true; + } } } @@ -1033,7 +1052,10 @@ class DifficultySelector extends FlxSprite whiteShader.colorSet = true; + scale.x = scale.y = 0.5; + new FlxTimer().start(2 / 24, function(tmr) { + scale.x = scale.y = 1; whiteShader.colorSet = false; updateHitbox(); }); @@ -1049,20 +1071,23 @@ typedef SongFilter = enum abstract FilterType(String) { var STARTSWITH; + var REGEXP; var FAVORITE; var ALL; } class FreeplaySongData { + public var songId:String = ""; public var songName:String = ""; public var levelId:String = ""; public var songCharacter:String = ""; public var isFav:Bool = false; - public function new(song:String, levelId:String, songCharacter:String, isFav:Bool = false) + public function new(songId:String, songName:String, levelId:String, songCharacter:String, isFav:Bool = false) { - this.songName = song; + this.songId = songId; + this.songName = songName; this.levelId = levelId; this.songCharacter = songCharacter; this.isFav = isFav; diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index ee2dfe5fd..07a15dae1 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -6,9 +6,6 @@ import openfl.utils.Assets as OpenFlAssets; class Paths { - public static var SOUND_EXT = #if web "mp3" #else "ogg" #end; - public static var VIDEO_EXT = "mp4"; - static var currentLevel:String; static public function setCurrentLevel(name:String) @@ -52,9 +49,9 @@ class Paths return getPath(file, type, library); } - public static inline function animateAtlas(path:String, library:String) + public static inline function animateAtlas(path:String, ?library:String) { - return getLibraryPathForce('images/$path', library); + return getLibraryPath('images/$path', library); } inline static public function txt(key:String, ?library:String) @@ -84,7 +81,7 @@ class Paths static public function sound(key:String, ?library:String) { - return getPath('sounds/$key.$SOUND_EXT', SOUND, library); + return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library); } inline static public function soundRandom(key:String, min:Int, max:Int, ?library:String) @@ -94,24 +91,24 @@ class Paths inline static public function music(key:String, ?library:String) { - return getPath('music/$key.$SOUND_EXT', MUSIC, library); + return getPath('music/$key.${Constants.EXT_SOUND}', MUSIC, library); } inline static public function videos(key:String, ?library:String) { - return getPath('videos/$key.$VIDEO_EXT', BINARY, library); + return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library); } inline static public function voices(song:String, ?suffix:String = '') { if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files - return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT'; + return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}'; } inline static public function inst(song:String, ?suffix:String = '') { - return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT'; + return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}'; } inline static public function image(key:String, ?library:String) diff --git a/source/funkin/audio/FlxStreamSound.hx b/source/funkin/audio/FlxStreamSound.hx new file mode 100644 index 000000000..a572ad436 --- /dev/null +++ b/source/funkin/audio/FlxStreamSound.hx @@ -0,0 +1,49 @@ +package funkin.audio; + +import flash.media.Sound; +#if flash11 +import flash.utils.ByteArray; +#end +import flixel.sound.FlxSound; +import flixel.system.FlxAssets.FlxSoundAsset; +import openfl.Assets; +#if (openfl >= "8.0.0") +import openfl.utils.AssetType; +#end + +/** + * a FlxSound that just overrides loadEmbedded to allow for "streamed" sounds to load with better performance! + */ +class FlxStreamSound extends FlxSound +{ + public function new() + { + super(); + } + + override public function loadEmbedded(EmbeddedSound:FlxSoundAsset, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound + { + if (EmbeddedSound == null) return this; + + cleanup(true); + + if ((EmbeddedSound is Sound)) + { + _sound = EmbeddedSound; + } + else if ((EmbeddedSound is Class)) + { + _sound = Type.createInstance(EmbeddedSound, []); + } + else if ((EmbeddedSound is String)) + { + if (Assets.exists(EmbeddedSound, AssetType.SOUND) + || Assets.exists(EmbeddedSound, AssetType.MUSIC)) _sound = Assets.getMusic(EmbeddedSound); + else + FlxG.log.error('Could not find a Sound asset with an ID of \'$EmbeddedSound\'.'); + } + + // NOTE: can't pull ID3 info from embedded sound currently + return init(Looped, AutoDestroy, OnComplete); + } +} diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 24d0de476..70615069b 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -4,9 +4,6 @@ import openfl.Assets; import funkin.util.assets.DataAssets; import funkin.util.VersionUtil; import haxe.Constraints.Constructible; -import json2object.Position; -import json2object.Position.Line; -import json2object.Error; /** * The entry's constructor function must take a single argument, the entry's ID. @@ -179,6 +176,15 @@ abstract class BaseRegistry & Constructible; + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + */ + public abstract function parseEntryDataRaw(contents:String, ?fileName:String):Null; + /** * Read, parse, and validate the JSON data and produce the corresponding data object, * accounting for old versions of the data. @@ -226,79 +232,12 @@ abstract class BaseRegistry & Constructible; - function printErrors(errors:Array, id:String = ''):Void + function printErrors(errors:Array, id:String = ''):Void { trace('[${registryId}] Failed to parse entry data: ${id}'); for (error in errors) - printError(error); - } - - function printError(error:Error):Void - { - switch (error) - { - case IncorrectType(vari, expected, pos): - trace(' Expected field "$vari" to be of type "$expected".'); - printPos(pos); - case IncorrectEnumValue(value, expected, pos): - trace(' Invalid enum value (expected "$expected", got "$value")'); - printPos(pos); - case InvalidEnumConstructor(value, expected, pos): - trace(' Invalid enum constructor (epxected "$expected", got "$value")'); - printPos(pos); - case UninitializedVariable(vari, pos): - trace(' Uninitialized variable "$vari"'); - printPos(pos); - case UnknownVariable(vari, pos): - trace(' Unknown variable "$vari"'); - printPos(pos); - case ParserError(message, pos): - trace(' Parsing error: ${message}'); - printPos(pos); - case CustomFunctionException(e, pos): - if (Std.isOfType(e, String)) - { - trace(' ${e}'); - } - else - { - printUnknownError(e); - } - printPos(pos); - default: - printUnknownError(error); - } - } - - function printUnknownError(e:Dynamic):Void - { - switch (Type.typeof(e)) - { - case TClass(c): - trace(' [${Type.getClassName(c)}] ${e.toString()}'); - case TEnum(c): - trace(' [${Type.getEnumName(c)}] ${e.toString()}'); - default: - trace(' [${Type.typeof(e)}] ${e.toString()}'); - } - } - - /** - * TODO: Figure out the nicest way to print this. - * Maybe look up how other JSON parsers format their errors? - * @see https://github.com/elnabo/json2object/blob/master/src/json2object/Position.hx - */ - function printPos(pos:Position):Void - { - if (pos.lines[0].number == pos.lines[pos.lines.length - 1].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}'); - } + DataError.printError(error); } } diff --git a/source/funkin/data/DataError.hx b/source/funkin/data/DataError.hx new file mode 100644 index 000000000..87c99fff5 --- /dev/null +++ b/source/funkin/data/DataError.hx @@ -0,0 +1,75 @@ +package funkin.data; + +import json2object.Position; +import json2object.Position.Line; +import json2object.Error; + +class DataError +{ + public static function printError(error:Error):Void + { + switch (error) + { + case IncorrectType(vari, expected, pos): + trace(' Expected field "$vari" to be of type "$expected".'); + printPos(pos); + case IncorrectEnumValue(value, expected, pos): + trace(' Invalid enum value (expected "$expected", got "$value")'); + printPos(pos); + case InvalidEnumConstructor(value, expected, pos): + trace(' Invalid enum constructor (epxected "$expected", got "$value")'); + printPos(pos); + case UninitializedVariable(vari, pos): + trace(' Uninitialized variable "$vari"'); + printPos(pos); + case UnknownVariable(vari, pos): + trace(' Unknown variable "$vari"'); + printPos(pos); + case ParserError(message, pos): + trace(' Parsing error: ${message}'); + printPos(pos); + case CustomFunctionException(e, pos): + if (Std.isOfType(e, String)) + { + trace(' ${e}'); + } + else + { + printUnknownError(e); + } + printPos(pos); + default: + printUnknownError(error); + } + } + + public static function printUnknownError(e:Dynamic):Void + { + switch (Type.typeof(e)) + { + case TClass(c): + trace(' [${Type.getClassName(c)}] ${e.toString()}'); + case TEnum(c): + trace(' [${Type.getEnumName(c)}] ${e.toString()}'); + default: + trace(' [${Type.typeof(e)}] ${e.toString()}'); + } + } + + /** + * TODO: Figure out the nicest way to print this. + * Maybe look up how other JSON parsers format their errors? + * @see https://github.com/elnabo/json2object/blob/master/src/json2object/Position.hx + */ + static function printPos(pos:Position):Void + { + if (pos.lines[0].number == pos.lines[pos.lines.length - 1].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}'); + } + } +} diff --git a/source/funkin/data/DataParse.hx b/source/funkin/data/DataParse.hx index f6b5dd659..4a422b368 100644 --- a/source/funkin/data/DataParse.hx +++ b/source/funkin/data/DataParse.hx @@ -1,7 +1,13 @@ package funkin.data; +import funkin.data.song.importer.FNFLegacyData.LegacyNote; import hxjsonast.Json; +import hxjsonast.Tools; import hxjsonast.Json.JObjectField; +import haxe.ds.Either; +import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection; +import funkin.data.song.importer.FNFLegacyData.LegacyNoteData; +import funkin.data.song.importer.FNFLegacyData.LegacyScrollSpeeds; /** * `json2object` has an annotation `@:jcustomparse` which allows for mutation of parsed values. @@ -39,36 +45,40 @@ class DataParse */ public static function dynamicValue(json:Json, name:String):Dynamic { - return jsonToDynamic(json); + return Tools.getValue(json); } /** - * Parser which outputs a Dynamic value, which must be an object with properties. - * @param json - * @param name - * @return Dynamic + * Parser which outputs a `Either, LegacyNoteData>`. + * Used by the FNF legacy JSON importer. */ - public static function dynamicObject(json:Json, name:String):Dynamic + public static function eitherLegacyNoteData(json:Json, name:String):Either, LegacyNoteData> { switch (json.value) { + case JArray(values): + return Either.Left(legacyNoteSectionArray(json, name)); case JObject(fields): - return jsonFieldsToDynamicObject(fields); + return Either.Right(cast Tools.getValue(json)); default: - throw 'Expected property $name to be an object, but it was ${json.value}.'; + throw 'Expected property $name to be note data, but it was ${json.value}.'; } } - static function jsonToDynamic(json:Json):Null + /** + * Parser which outputs a `Either`. + * Used by the FNF legacy JSON importer. + */ + public static function eitherLegacyScrollSpeeds(json:Json, name:String):Either { - return switch (json.value) + switch (json.value) { - case JString(s): s; - case JNumber(n): Std.parseInt(n); - case JBool(b): b; - case JNull: null; - case JObject(fields): jsonFieldsToDynamicObject(fields); - case JArray(values): jsonArrayToDynamicArray(values); + case JNumber(f): + return Either.Left(Std.parseFloat(f)); + case JObject(fields): + return Either.Right(cast Tools.getValue(json)); + default: + throw 'Expected property $name to be scroll speeds, but it was ${json.value}.'; } } @@ -82,7 +92,7 @@ class DataParse var result:Dynamic = {}; for (field in fields) { - Reflect.setField(result, field.name, jsonToDynamic(field.value)); + Reflect.setField(result, field.name, Tools.getValue(field.value)); } return result; } @@ -94,6 +104,67 @@ class DataParse */ static function jsonArrayToDynamicArray(jsons:Array):Array> { - return [for (json in jsons) jsonToDynamic(json)]; + return [for (json in jsons) Tools.getValue(json)]; + } + + static function legacyNoteSectionArray(json:Json, name:String):Array + { + switch (json.value) + { + case JArray(values): + return [for (value in values) legacyNoteSection(value, name)]; + default: + throw 'Expected property to be an array, but it was ${json.value}.'; + } + } + + static function legacyNoteSection(json:Json, name:String):LegacyNoteSection + { + switch (json.value) + { + case JObject(fields): + return cast Tools.getValue(json); + default: + throw 'Expected property $name to be an object, but it was ${json.value}.'; + } + } + + public static function legacyNoteData(json:Json, name:String):LegacyNoteData + { + switch (json.value) + { + case JObject(fields): + return cast Tools.getValue(json); + default: + throw 'Expected property $name to be an object, but it was ${json.value}.'; + } + } + + public static function legacyNotes(json:Json, name:String):Array + { + switch (json.value) + { + case JArray(values): + return [for (value in values) legacyNote(value, name)]; + default: + throw 'Expected property $name to be an array of notes, but it was ${json.value}.'; + } + } + + public static function legacyNote(json:Json, name:String):LegacyNote + { + switch (json.value) + { + case JArray(values): + // var time:Null = values[0] == null ? null : Tools.getValue(values[0]); + // var data:Null = values[1] == null ? null : Tools.getValue(values[1]); + // var length:Null = values[2] == null ? null : Tools.getValue(values[2]); + // var alt:Null = values[3] == null ? null : Tools.getValue(values[3]); + + // return new LegacyNote(time, data, length, alt); + return null; + default: + throw 'Expected property $name to be a note, but it was ${json.value}.'; + } } } diff --git a/source/funkin/data/DataWrite.hx b/source/funkin/data/DataWrite.hx index 2ff7672da..41993107f 100644 --- a/source/funkin/data/DataWrite.hx +++ b/source/funkin/data/DataWrite.hx @@ -1,8 +1,17 @@ package funkin.data; +import funkin.util.SerializerUtil; + /** * `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON. * * Functions must be of the signature `(T) -> String`, where `T` is the type of the property. */ -class DataWrite {} +class DataWrite +{ + public static function dynamicValue(value:Dynamic):String + { + // Is this cheating? Yes. Do I care? No. + return SerializerUtil.toJSON(value); + } +} diff --git a/source/funkin/data/animation/AnimationData.hx b/source/funkin/data/animation/AnimationData.hx index 2116109db..9765f784c 100644 --- a/source/funkin/data/animation/AnimationData.hx +++ b/source/funkin/data/animation/AnimationData.hx @@ -67,7 +67,6 @@ typedef UnnamedAnimationData = * ONLY for use by MultiSparrow characters. * @default The assetPath of the parent sprite */ - @:default(null) @:optional var assetPath:Null; @@ -85,7 +84,7 @@ typedef UnnamedAnimationData = */ @:default(false) @:optional - var looped:Null; + var looped:Bool; /** * Whether the animation's sprites should be flipped horizontally. diff --git a/source/funkin/data/level/LevelRegistry.hx b/source/funkin/data/level/LevelRegistry.hx index d135e1241..75b0b11f6 100644 --- a/source/funkin/data/level/LevelRegistry.hx +++ b/source/funkin/data/level/LevelRegistry.hx @@ -47,6 +47,26 @@ class LevelRegistry extends BaseRegistry return parser.value; } + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser = new json2object.JsonParser(); + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + function createScriptedEntry(clsName:String):Level { return ScriptedLevel.init(clsName, "unknown"); diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx index bb594bca4..da45da5f2 100644 --- a/source/funkin/data/notestyle/NoteStyleRegistry.hx +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -54,6 +54,26 @@ class NoteStyleRegistry extends BaseRegistry return parser.value; } + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser = new json2object.JsonParser(); + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + function createScriptedEntry(clsName:String):NoteStyle { return ScriptedNoteStyle.init(clsName, "unknown"); diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 59f8fcaf1..d557bd39c 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -1,8 +1,6 @@ package funkin.data.song; import flixel.util.typeLimit.OneOfTwo; -import funkin.play.song.SongMigrator; -import funkin.play.song.SongValidator; import funkin.data.song.SongRegistry; import thx.semver.Version; @@ -47,32 +45,33 @@ class SongMetadata * Defaults to `default` or `''`. Populated later. */ @:jignored - public var variation:String = 'default'; + public var variation:String; - public function new(songName:String, artist:String, variation:String = 'default') + public function new(songName:String, artist:String, ?variation:String) { - this.version = SongMigrator.CHART_VERSION; + this.version = SongRegistry.SONG_METADATA_VERSION; this.songName = songName; this.artist = artist; this.timeFormat = 'ms'; this.divisions = null; this.timeChanges = [new SongTimeChange(0, 100)]; this.looped = false; - this.playData = - { - songVariations: [], - difficulties: ['normal'], - - playableChars: ['bf' => new SongPlayableChar('gf', 'dad')], - - stage: 'mainStage', - noteSkin: 'Normal' - }; + this.playData = new SongPlayData(); + this.playData.songVariations = []; + this.playData.difficulties = []; + this.playData.characters = new SongCharacterData('bf', 'gf', 'dad'); + this.playData.stage = 'mainStage'; + this.playData.noteSkin = 'funkin'; this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; // Variation ID. - this.variation = variation; + this.variation = (variation == null) ? Constants.DEFAULT_VARIATION : variation; } + /** + * Create a copy of this SongMetadata with the same information. + * @param newVariation Set to a new variation ID to change the new metadata. + * @return The cloned SongMetadata + */ public function clone(?newVariation:String = null):SongMetadata { var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); @@ -87,6 +86,22 @@ class SongMetadata return result; } + /** + * Serialize this SongMetadata into a JSON string. + * @return The JSON string. + */ + public function serialize(pretty:Bool = true):String + { + var writer = new json2object.JsonWriter(); + // I believe @:jignored should be iggnored 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); + } + + /** + * Produces a string representation suitable for debugging. + */ public function toString():String { return 'SongMetadata(${this.songName} by ${this.artist}, variation ${this.variation})'; @@ -121,7 +136,6 @@ class SongTimeChange */ @:optional @:alias("b") - // @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_BEAT_TIME) public var beatTime:Null; /** @@ -168,6 +182,9 @@ class SongTimeChange this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets; } + /** + * Produces a string representation suitable for debugging. + */ public function toString():String { return 'SongTimeChange(${this.timeStamp}ms,${this.bpm}bpm)'; @@ -199,7 +216,7 @@ class SongMusicData @:optional @:default(false) - public var looped:Bool; + public var looped:Null; // @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) public var generatedBy:String; @@ -214,11 +231,11 @@ class SongMusicData * Defaults to `default` or `''`. Populated later. */ @:jignored - public var variation:String = 'default'; + public var variation:String = Constants.DEFAULT_VARIATION; public function new(songName:String, artist:String, variation:String = 'default') { - this.version = SongMigrator.CHART_VERSION; + this.version = SongRegistry.SONG_CHART_DATA_VERSION; this.songName = songName; this.artist = artist; this.timeFormat = 'ms'; @@ -227,7 +244,7 @@ class SongMusicData this.looped = false; this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; // Variation ID. - this.variation = variation; + this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation; } public function clone(?newVariation:String = null):SongMusicData @@ -243,53 +260,106 @@ class SongMusicData return result; } + /** + * Produces a string representation suitable for debugging. + */ public function toString():String { return 'SongMusicData(${this.songName} by ${this.artist}, variation ${this.variation})'; } } -typedef SongPlayData = +class SongPlayData { + /** + * The variations this song has. The associated metadata files should exist. + */ public var songVariations:Array; + + /** + * The difficulties contained in this song's chart file. + */ public var difficulties:Array; /** - * Keys are the player characters and the values give info on what opponent/GF/inst to use. + * The characters used by this song. */ - public var playableChars:Map; + public var characters:SongCharacterData; + /** + * The stage used by this song. + */ public var stage:String; + + /** + * The note style used by this song. + * TODO: Rename to `noteStyle`? Renaming values is a breaking change to the metadata format. + */ public var noteSkin:String; + + /** + * The difficulty rating for this song as displayed in Freeplay. + * TODO: Adding this is a non-breaking change to the metadata format. + */ + // public var rating:Int; + + /** + * The album ID for the album to display in Freeplay. + * TODO: Adding this is a non-breaking change to the metadata format. + */ + // public var album:String; + + public function new() {} + + /** + * Produces a string representation suitable for debugging. + */ + public function toString():String + { + return 'SongPlayData(${this.songVariations}, ${this.difficulties})'; + } } -class SongPlayableChar +/** + * Information about the characters used in this variation of the song. + * Create a new variation if you want to change the characters. + */ +class SongCharacterData { - @:alias('g') + @:optional + @:default('') + public var player:String = ''; + @:optional @:default('') public var girlfriend:String = ''; - @:alias('o') @:optional @:default('') public var opponent:String = ''; - @:alias('i') @:optional @:default('') - public var inst:String = ''; + public var instrumental:String = ''; - public function new(girlfriend:String = '', opponent:String = '', inst:String = '') + @:optional + @:default([]) + public var altInstrumentals:Array = []; + + public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '') { + this.player = player; this.girlfriend = girlfriend; this.opponent = opponent; - this.inst = inst; + this.instrumental = instrumental; } + /** + * Produces a string representation suitable for debugging. + */ public function toString():String { - return 'SongPlayableChar(${this.girlfriend}, ${this.opponent}, ${this.inst})'; + return 'SongCharacterData(${this.player}, ${this.girlfriend}, ${this.opponent}, ${this.instrumental}, [${this.altInstrumentals.join(', ')}])'; } } @@ -305,6 +375,9 @@ class SongChartData @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) public var generatedBy:String; + @:jignored + public var variation:String; + public function new(scrollSpeed:Map, events:Array, notes:Map>) { this.version = SongRegistry.SONG_CHART_DATA_VERSION; @@ -346,14 +419,21 @@ class SongChartData return value; } - public function getEvents():Array + /** + * Convert this SongChartData into a JSON string. + */ + public function serialize(pretty:Bool = true):String { - return this.events; + var writer = new json2object.JsonWriter(); + return writer.write(this, pretty ? ' ' : null); } - public function setEvents(value:Array):Array + /** + * Produces a string representation suitable for debugging. + */ + public function toString():String { - return this.events = value; + return 'SongChartData(${this.events.length} events, ${this.notes.size()} difficulties, ${generatedBy})'; } } @@ -387,6 +467,7 @@ class SongEventData @:alias("v") @:optional @:jcustomparse(funkin.data.DataParse.dynamicValue) + @:jcustomwrite(funkin.data.DataWrite.dynamicValue) public var value:Dynamic = null; /** @@ -484,6 +565,9 @@ class SongEventData return this.time <= other.time; } + /** + * Produces a string representation suitable for debugging. + */ public function toString():String { return 'SongEventData(${this.time}ms, ${this.event}: ${this.value})'; @@ -703,6 +787,9 @@ class SongNoteData return this.time <= other.time; } + /** + * Produces a string representation suitable for debugging. + */ public function toString():String { return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}' diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index d15a2b19a..4b9318df2 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -8,6 +8,9 @@ import funkin.util.SerializerUtil; using Lambda; +/** + * Utility functions for working with song data, including note data, event data, metadata, etc. + */ class SongDataUtils { /** diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 9bc1278c8..cf2da14f7 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -1,6 +1,7 @@ package funkin.data.song; import funkin.data.song.SongData; +import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongMetadata; import funkin.play.song.ScriptedSong; @@ -8,6 +9,8 @@ import funkin.play.song.Song; import funkin.util.assets.DataAssets; import funkin.util.VersionUtil; +using funkin.data.song.migrator.SongDataMigrator; + class SongRegistry extends BaseRegistry { /** @@ -15,14 +18,18 @@ class SongRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final SONG_METADATA_VERSION:thx.semver.Version = "2.0.0"; + public static final SONG_METADATA_VERSION:thx.semver.Version = "2.1.0"; - public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; + public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.1.x"; public static final SONG_CHART_DATA_VERSION:thx.semver.Version = "2.0.0"; public static final SONG_CHART_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; + public static final SONG_MUSIC_DATA_VERSION:thx.semver.Version = "2.0.0"; + + public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; + public static var DEFAULT_GENERATEDBY(get, null):String; static function get_DEFAULT_GENERATEDBY():String @@ -30,6 +37,10 @@ class SongRegistry extends BaseRegistry return '${Constants.TITLE} - ${Constants.VERSION}'; } + /** + * TODO: What if there was a Singleton macro which created static functions + * that redirected to the instance? + */ public static final instance:SongRegistry = new SongRegistry(); public function new() @@ -101,12 +112,91 @@ class SongRegistry extends BaseRegistry return parseEntryMetadata(id); } - public function parseEntryMetadata(id:String, variation:String = ""):Null + /** + * Parse, and validate the JSON data and produce the corresponding data object. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String = 'raw'):Null { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. + return parseEntryMetadataRaw(contents); + } + + public function parseEntryMetadata(id:String, ?variation:String):Null + { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); + switch (loadEntryMetadataFile(id, variation)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return cleanMetadata(parser.value, variation); + } + + public function parseEntryMetadataRaw(contents:String, ?fileName:String = 'raw', ?variation:String):Null + { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + + var parser = new json2object.JsonParser(); + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return cleanMetadata(parser.value, variation); + } + + public function parseEntryMetadataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null + { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + + // If a version rule is not specified, do not check against it. + if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE)) + { + return parseEntryMetadata(id, variation); + } + else if (VersionUtil.validateVersion(version, "2.0.x")) + { + return parseEntryMetadata_v2_0_0(id, variation); + } + else + { + throw '[${registryId}] Metadata entry ${id}:${variation} does not support migration to version ${SONG_METADATA_VERSION_RULE}.'; + } + } + + public function parseEntryMetadataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null + { + // If a version rule is not specified, do not check against it. + if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE)) + { + return parseEntryMetadataRaw(contents, fileName); + } + else if (VersionUtil.validateVersion(version, "2.0.x")) + { + return parseEntryMetadataRaw_v2_0_0(contents, fileName); + } + else + { + throw '[${registryId}] Metadata entry "${fileName}" does not support migration to version ${SONG_METADATA_VERSION_RULE}.'; + } + } + + function parseEntryMetadata_v2_0_0(id:String, variation:String = ""):Null + { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + + var parser = new json2object.JsonParser(); switch (loadEntryMetadataFile(id)) { case {fileName: fileName, contents: contents}: @@ -114,6 +204,39 @@ class SongRegistry extends BaseRegistry default: return null; } + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return parser.value.migrate(); + } + + function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null + { + var parser = new json2object.JsonParser(); + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value.migrate(); + } + + public function parseMusicData(id:String, ?variation:String):Null + { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + + var parser = new json2object.JsonParser(); + switch (loadMusicDataFile(id, variation)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } if (parser.errors.length > 0) { @@ -123,48 +246,54 @@ class SongRegistry extends BaseRegistry return parser.value; } - public function parseEntryMetadataWithMigration(id:String, variation:String = '', version:thx.semver.Version):Null + public function parseMusicDataRaw(contents:String, ?fileName:String = 'raw'):Null { - // If a version rule is not specified, do not check against it. - if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE)) + var parser = new json2object.JsonParser(); + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) { - return parseEntryMetadata(id); + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + public function parseMusicDataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null + { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + + // If a version rule is not specified, do not check against it. + if (SONG_MUSIC_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_MUSIC_DATA_VERSION_RULE)) + { + return parseMusicData(id, variation); } else { - throw '[${registryId}] Metadata entry ${id}:${variation == '' ? 'default' : variation} does not support migration to version ${SONG_METADATA_VERSION_RULE}.'; + throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; } } - public function parseMusicData(id:String, variation:String = ""):Null + public function parseMusicDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. - - var parser = new json2object.JsonParser(); - switch (loadMusicDataFile(id)) + // If a version rule is not specified, do not check against it. + if (SONG_MUSIC_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_MUSIC_DATA_VERSION_RULE)) { - case {fileName: fileName, contents: contents}: - parser.fromJson(contents, fileName); - default: - return null; + return parseMusicDataRaw(contents, fileName); } - - if (parser.errors.length > 0) + else { - printErrors(parser.errors, id); - return null; + throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; } - return parser.value; } - public function parseEntryChartData(id:String, variation:String = ''):Null + public function parseEntryChartData(id:String, ?variation:String):Null { - // JsonParser does not take type parameters, - // otherwise this function would be in BaseRegistry. + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + var parser = new json2object.JsonParser(); - switch (loadEntryChartFile(id)) + switch (loadEntryChartFile(id, variation)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); @@ -177,11 +306,28 @@ class SongRegistry extends BaseRegistry printErrors(parser.errors, id); return null; } - return parser.value; + return cleanChartData(parser.value, variation); } - public function parseEntryChartDataWithMigration(id:String, variation:String = '', version:thx.semver.Version):Null + public function parseEntryChartDataRaw(contents:String, ?fileName:String = 'raw', ?variation:String):Null { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + + var parser = new json2object.JsonParser(); + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return cleanChartData(parser.value, variation); + } + + public function parseEntryChartDataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null + { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + // If a version rule is not specified, do not check against it. if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE)) { @@ -189,7 +335,20 @@ class SongRegistry extends BaseRegistry } else { - throw '[${registryId}] Chart entry ${id}:${variation == '' ? 'default' : variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; + throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; + } + } + + public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null + { + // If a version rule is not specified, do not check against it. + if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE)) + { + return parseEntryChartDataRaw(contents, fileName); + } + else + { + throw '[${registryId}] Chart entry "${fileName}" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; } } @@ -203,9 +362,10 @@ class SongRegistry extends BaseRegistry return ScriptedSong.listScriptClasses(); } - function loadEntryMetadataFile(id:String, variation:String = ''):Null + function loadEntryMetadataFile(id:String, ?variation:String):Null { - var entryFilePath:String = Paths.json('$dataFilePath/$id/$id${variation == '' ? '' : '-$variation'}-metadata'); + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); if (!openfl.Assets.exists(entryFilePath)) return null; var rawJson:Null = openfl.Assets.getText(entryFilePath); if (rawJson == null) return null; @@ -213,9 +373,10 @@ class SongRegistry extends BaseRegistry return {fileName: entryFilePath, contents: rawJson}; } - function loadMusicDataFile(id:String, variation:String = ''):Null + function loadMusicDataFile(id:String, ?variation:String):Null { - var entryFilePath:String = Paths.file('music/$id/$id${variation == '' ? '' : '-$variation'}-metadata.json'); + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json'); if (!openfl.Assets.exists(entryFilePath)) return null; var rawJson:String = openfl.Assets.getText(entryFilePath); if (rawJson == null) return null; @@ -223,9 +384,10 @@ class SongRegistry extends BaseRegistry return {fileName: entryFilePath, contents: rawJson}; } - function loadEntryChartFile(id:String, variation:String = ''):Null + function loadEntryChartFile(id:String, ?variation:String):Null { - var entryFilePath:String = Paths.json('$dataFilePath/$id/$id${variation == '' ? '' : '-$variation'}-chart'); + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); if (!openfl.Assets.exists(entryFilePath)) return null; var rawJson:String = openfl.Assets.getText(entryFilePath); if (rawJson == null) return null; @@ -233,20 +395,36 @@ class SongRegistry extends BaseRegistry return {fileName: entryFilePath, contents: rawJson}; } - public function fetchEntryMetadataVersion(id:String, variation:String = ''):Null + public function fetchEntryMetadataVersion(id:String, ?variation:String):Null { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryStr:Null = loadEntryMetadataFile(id, variation)?.contents; var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr); return entryVersion; } - public function fetchEntryChartVersion(id:String, variation:String = ''):Null + public function fetchEntryChartVersion(id:String, ?variation:String):Null { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryStr:Null = loadEntryChartFile(id, variation)?.contents; var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr); return entryVersion; } + function cleanMetadata(metadata:SongMetadata, variation:String):SongMetadata + { + metadata.variation = variation; + + return metadata; + } + + function cleanChartData(chartData:SongChartData, variation:String):SongChartData + { + chartData.variation = variation; + + return chartData; + } + /** * A list of all the story weeks from the base game, in order. * TODO: Should this be hardcoded? diff --git a/source/funkin/data/song/importer/FNFLegacyData.hx b/source/funkin/data/song/importer/FNFLegacyData.hx new file mode 100644 index 000000000..5b75368c9 --- /dev/null +++ b/source/funkin/data/song/importer/FNFLegacyData.hx @@ -0,0 +1,124 @@ +package funkin.data.song.importer; + +import haxe.ds.Either; + +/** + * A data structure representing a song in the old chart format. + * This only works for charts compatible with Week 7, so you'll need a custom program + * to handle importing charts from mods or other engines. + */ +class FNFLegacyData +{ + public var song:LegacySongData; +} + +class LegacySongData +{ + public var player1:String; // Boyfriend + public var player2:String; // Opponent + + @:jcustomparse(funkin.data.DataParse.eitherLegacyScrollSpeeds) + public var speed:Either; + public var stageDefault:String; + public var bpm:Float; + + @:jcustomparse(funkin.data.DataParse.eitherLegacyNoteData) + public var notes:Either, LegacyNoteData>; + public var song:String; // Song name + + public function new() {} + + public function toString():String + { + var notesStr:String = switch (notes) + { + case Left(sections): 'single difficulty w/ ${sections.length} sections'; + case Right(data): + var difficultyCount:Int = 0; + if (data.easy != null) difficultyCount++; + if (data.normal != null) difficultyCount++; + if (data.hard != null) difficultyCount++; + '${difficultyCount} difficulties'; + }; + return 'LegacySongData($player1, $player2, $notesStr)'; + } +} + +typedef LegacyScrollSpeeds = +{ + public var ?easy:Float; + public var ?normal:Float; + public var ?hard:Float; +}; + +typedef LegacyNoteData = +{ + /** + * The easy difficulty. + */ + public var ?easy:Array; + + /** + * The normal difficulty. + */ + public var ?normal:Array; + + /** + * The hard difficulty. + */ + public var ?hard:Array; +}; + +typedef LegacyNoteSection = +{ + /** + * Whether the section is a must-hit section. + * If true, 0-3 are boyfriends notes, 4-7 are opponents notes. + * If false, 0-3 are opponents notes, 4-7 are boyfriends notes. + */ + public var mustHitSection:Bool; + + /** + * Array of note data: + * - Direction + * - Time (ms) + * - Sustain Duration (ms) + * - Note kind (true = "alt", or string) + */ + public var sectionNotes:Array; + + public var ?typeOfSection:Int; + + public var ?lengthInSteps:Int; + + // BPM changes + public var ?changeBPM:Bool; + public var ?bpm:Float; +} + +/** + * Notes in the old format are stored as an Array + * We use a custom parser to manage this. + */ +@:jcustomparse(funkin.data.DataParse.legacyNote) +class LegacyNote +{ + public var time:Float; + public var data:Int; + public var length:Float; + public var alt:Bool; + + public function new(time:Float, data:Int, ?length:Float, ?alt:Bool) + { + this.time = time; + this.data = data; + + this.length = length ?? 0.0; + this.alt = alt ?? false; + } + + public inline function getKind():String + { + return this.alt ? 'alt' : 'normal'; + } +} diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx new file mode 100644 index 000000000..ee68513dc --- /dev/null +++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx @@ -0,0 +1,202 @@ +package funkin.data.song.importer; // import is a reserved word dumbass + +import funkin.data.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongChartData; +import funkin.data.song.SongData.SongCharacterData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongTimeChange; +import funkin.data.song.importer.FNFLegacyData; +import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection; + +class FNFLegacyImporter +{ + public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData + { + var parser = new json2object.JsonParser(); + parser.fromJson(input, fileName); + + if (parser.errors.length > 0) + { + trace('[FNFLegacyImporter] Error parsing JSON data from ' + fileName + ':'); + for (error in parser.errors) + DataError.printError(error); + return null; + } + return parser.value; + } + + /** + * @param data The raw parsed JSON data to migrate, as a Dynamic. + * @param difficulty + * @return SongMetadata + */ + public static function migrateMetadata(songData:FNFLegacyData, difficulty:String = 'normal'):SongMetadata + { + trace('Migrating song metadata from FNF Legacy.'); + + var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default'); + + var hadError:Bool = false; + + // Set generatedBy string for debugging. + songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)'; + + songMetadata.playData.stage = songData?.song?.stageDefault ?? 'mainStage'; + songMetadata.songName = songData?.song?.song ?? 'Import'; + songMetadata.playData.difficulties = []; + + if (songData?.song?.notes != null) + { + switch (songData.song.notes) + { + case Left(notes): + // One difficulty of notes. + songMetadata.playData.difficulties.push(difficulty); + case Right(difficulties): + if (difficulties.easy != null) songMetadata.playData.difficulties.push('easy'); + if (difficulties.normal != null) songMetadata.playData.difficulties.push('normal'); + if (difficulties.hard != null) songMetadata.playData.difficulties.push('hard'); + } + } + + songMetadata.playData.songVariations = []; + + songMetadata.timeChanges = rebuildTimeChanges(songData); + + songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad', 'mom'); + + return songMetadata; + } + + public static function migrateChartData(songData:FNFLegacyData, difficulty:String = 'normal'):SongChartData + { + trace('Migrating song chart data from FNF Legacy.'); + + var songChartData:SongChartData = new SongChartData([difficulty => 1.0], [], [difficulty => []]); + + if (songData?.song?.notes != null) + { + switch (songData.song.notes) + { + case Left(notes): + // One difficulty of notes. + songChartData.notes.set(difficulty, migrateNoteSections(notes)); + case Right(difficulties): + var baseDifficulty = null; + if (difficulties.easy != null) songChartData.notes.set('easy', migrateNoteSections(difficulties.easy)); + if (difficulties.normal != null) songChartData.notes.set('normal', migrateNoteSections(difficulties.normal)); + if (difficulties.hard != null) songChartData.notes.set('hard', migrateNoteSections(difficulties.hard)); + } + } + + // Import event data. + songChartData.events = rebuildEventData(songData); + + switch (songData.song.speed) + { + case Left(speed): + // All difficulties will use the one scroll speed. + songChartData.scrollSpeed.set('default', speed); + case Right(speeds): + if (speeds.easy != null) songChartData.scrollSpeed.set('easy', speeds.easy); + if (speeds.normal != null) songChartData.scrollSpeed.set('normal', speeds.normal); + if (speeds.hard != null) songChartData.scrollSpeed.set('hard', speeds.hard); + } + + return songChartData; + } + + /** + * FNF Legacy doesn't have song events, but without them the song won't look right, + * so we insert camera events when the character changes. + */ + static function rebuildEventData(songData:FNFLegacyData):Array + { + var result:Array = []; + + var noteSections = []; + switch (songData.song.notes) + { + case Left(notes): + // All difficulties will use the one scroll speed. + noteSections = notes; + case Right(difficulties): + if (difficulties.normal != null) noteSections = difficulties.normal; + if (difficulties.hard != null) noteSections = difficulties.normal; + if (difficulties.easy != null) noteSections = difficulties.normal; + } + + if (noteSections == null || noteSections.length == 0) return result; + + // Add camera events. + var lastSectionWasMustHit:Null = null; + for (section in noteSections) + { + // Skip empty sections. + if (section.sectionNotes.length == 0) continue; + + if (section.mustHitSection != lastSectionWasMustHit) + { + lastSectionWasMustHit = section.mustHitSection; + + var firstNote:LegacyNote = section.sectionNotes[0]; + + result.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1})); + } + } + + return result; + } + + /** + * Port over time changes from FNF Legacy. + * If a section contains a BPM change, it will be applied at the timestamp of the first note in that section. + */ + static function rebuildTimeChanges(songData:FNFLegacyData):Array + { + var result:Array = []; + + result.push(new SongTimeChange(0, songData?.song?.bpm ?? Constants.DEFAULT_BPM)); + + var noteSections = []; + switch (songData.song.notes) + { + case Left(notes): + // All difficulties will use the one scroll speed. + noteSections = notes; + case Right(difficulties): + if (difficulties.normal != null) noteSections = difficulties.normal; + if (difficulties.hard != null) noteSections = difficulties.normal; + if (difficulties.easy != null) noteSections = difficulties.normal; + } + + if (noteSections == null || noteSections.length == 0) return result; + + for (noteSection in noteSections) + { + if (noteSection.changeBPM ?? false) + { + var firstNote:LegacyNote = noteSection.sectionNotes[0]; + if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm)); + } + } + + return result; + } + + static function migrateNoteSections(input:Array):Array + { + var result:Array = []; + + for (section in input) + { + for (note in section.sectionNotes) + { + result.push(new SongNoteData(note.time, note.data, note.length, note.getKind())); + } + } + + return result; + } +} diff --git a/source/funkin/data/song/migrator/SongDataMigrator.hx b/source/funkin/data/song/migrator/SongDataMigrator.hx new file mode 100644 index 000000000..b5e08c832 --- /dev/null +++ b/source/funkin/data/song/migrator/SongDataMigrator.hx @@ -0,0 +1,66 @@ +package funkin.data.song.migrator; + +import funkin.data.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongPlayData; +import funkin.data.song.SongData.SongCharacterData; +import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0; +import funkin.data.song.migrator.SongData_v2_0_0.SongPlayData_v2_0_0; +import funkin.data.song.migrator.SongData_v2_0_0.SongPlayableChar_v2_0_0; + +/** + * This class contains functions to migrate older data formats to the current one. + * + * Utilizes static extensions with overloaded inline functions to make migration as easy as `.migrate()`. + * @see https://try.haxe.org/#e1c1cf22 + */ +class SongDataMigrator +{ + public static overload extern inline function migrate(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata + { + return migrate_SongMetadata_v2_0_0(input); + } + + public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata + { + var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation); + result.version = input.version; + result.timeFormat = input.timeFormat; + result.divisions = input.divisions; + result.timeChanges = input.timeChanges; + result.looped = input.looped; + result.playData = migrate_SongPlayData_v2_0_0(input.playData); + result.generatedBy = input.generatedBy; + + return result; + } + + public static overload extern inline function migrate(input:SongData_v2_0_0.SongPlayData_v2_0_0):SongPlayData + { + return migrate_SongPlayData_v2_0_0(input); + } + + public static function migrate_SongPlayData_v2_0_0(input:SongData_v2_0_0.SongPlayData_v2_0_0):SongPlayData + { + var result:SongPlayData = new SongPlayData(); + result.songVariations = input.songVariations; + result.difficulties = input.difficulties; + result.stage = input.stage; + result.noteSkin = input.noteSkin; + + // Fetch the first playable character and migrate it. + var firstCharKey:Null = input.playableChars.size() == 0 ? null : input.playableChars.keys().array()[0]; + var firstCharData:Null = input.playableChars.get(firstCharKey); + + if (firstCharData == null) + { + // Fill in a default playable character. + result.characters = new SongCharacterData('bf', 'gf', 'dad'); + } + else + { + result.characters = new SongCharacterData(firstCharKey, firstCharData.girlfriend, firstCharData.opponent, firstCharData.inst); + } + + return result; + } +} diff --git a/source/funkin/data/song/migrator/SongData_v2_0_0.hx b/source/funkin/data/song/migrator/SongData_v2_0_0.hx new file mode 100644 index 000000000..935e7349c --- /dev/null +++ b/source/funkin/data/song/migrator/SongData_v2_0_0.hx @@ -0,0 +1,122 @@ +package funkin.data.song.migrator; + +import thx.semver.Version; +import funkin.data.song.SongData; + +class SongMetadata_v2_0_0 +{ + // ========== + // MODIFIED VALUES + // =========== + + /** + * In metadata `v2.1.0`, `SongPlayData` was refactored. + */ + public var playData:SongPlayData_v2_0_0; + + /** + * In metadata `v2.1.0`, `variation` was set to `ignore` when writing. + */ + @:optional + @:default('default') + public var variation:String; + + // ========== + // UNMODIFIED VALUES + // ========== + public var version:Version; + + @:default("Unknown") + public var songName:String; + + @:default("Unknown") + public var artist:String; + + @:optional + @:default(96) + public var divisions:Null; // Optional field + + @:optional + @:default(false) + public var looped:Bool; + + public var generatedBy:String; + + public var timeFormat:SongData.SongTimeFormat; + + public var timeChanges:Array; + + public function new() {} + + /** + * Produces a string representation suitable for debugging. + */ + public function toString():String + { + return 'SongMetadata[LEGACY:v2.0.0](${this.songName} by ${this.artist}, variation ${this.variation})'; + } +} + +class SongPlayData_v2_0_0 +{ + // ========== + // MODIFIED VALUES + // =========== + + /** + * In metadata version `v2.1.0`, this was refactored to a single `SongCharacterData` object. + */ + public var playableChars:Map; + + // ========== + // UNMODIFIED VALUES + // ========== + public var songVariations:Array; + public var difficulties:Array; + + public var stage:String; + public var noteSkin:String; + + public function new() {} + + /** + * Produces a string representation suitable for debugging. + */ + public function toString():String + { + return 'SongPlayData[LEGACY:v2.0.0](${this.songVariations}, ${this.difficulties})'; + } +} + +class SongPlayableChar_v2_0_0 +{ + @:alias('g') + @:optional + @:default('') + public var girlfriend:String = ''; + + @:alias('o') + @:optional + @:default('') + public var opponent:String = ''; + + @:alias('i') + @:optional + @:default('') + public var inst:String = ''; + + public function new(girlfriend:String = '', opponent:String = '', inst:String = '') + { + this.girlfriend = girlfriend; + this.opponent = opponent; + this.inst = inst; + } + + /** + * Produces a string representation suitable for debugging. + */ + public function toString():String + { + return 'SongPlayableChar[LEGACY:v2.0.0](${this.girlfriend}, ${this.opponent}, ${this.inst})'; + } +} diff --git a/source/funkin/freeplayStuff/BGScrollingText.hx b/source/funkin/freeplayStuff/BGScrollingText.hx index 9fa6dd49b..586f83822 100644 --- a/source/funkin/freeplayStuff/BGScrollingText.hx +++ b/source/funkin/freeplayStuff/BGScrollingText.hx @@ -7,6 +7,7 @@ import flixel.math.FlxMath; import flixel.text.FlxText; import flixel.util.FlxColor; import flixel.util.FlxSort; +import flixel.util.FlxTimer; // its kinda like marqeee html lol! class BGScrollingText extends FlxSpriteGroup @@ -16,36 +17,53 @@ class BGScrollingText extends FlxSpriteGroup public var widthShit:Float = FlxG.width; public var placementOffset:Float = 20; public var speed:Float = 1; + public var size(default, set):Int = 48; public var funnyColor(default, set):Int = 0xFFFFFFFF; - public function new(x:Float, y:Float, text:String, widthShit:Float = 100) + public function new(x:Float, y:Float, text:String, widthShit:Float = 100, ?bold:Bool = false, ?size:Int = 48) { super(x, y); this.widthShit = widthShit; + if (size != null) this.size = size; grpTexts = new FlxTypedSpriteGroup(); add(grpTexts); - var testText:FlxText = new FlxText(0, 0, 0, text, 48); + var testText:FlxText = new FlxText(0, 0, 0, text, this.size); testText.font = "5by7"; + testText.bold = bold; testText.updateHitbox(); grpTexts.add(testText); - var needed:Int = Math.ceil(widthShit / testText.frameWidth); + var needed:Int = Math.ceil(widthShit / testText.frameWidth) + 1; for (i in 0...needed) { var lmfao:Int = i + 1; - var coolText:FlxText = new FlxText((lmfao * testText.frameWidth) + (lmfao * 20), 0, 0, text, 48); + var coolText:FlxText = new FlxText((lmfao * testText.frameWidth) + (lmfao * 20), 0, 0, text, this.size); + coolText.font = "5by7"; + coolText.bold = bold; coolText.updateHitbox(); grpTexts.add(coolText); } } + function set_size(value:Int):Int + { + if (grpTexts != null) + { + grpTexts.forEach(function(txt:FlxText) { + txt.size = value; + }); + } + this.size = value; + return value; + } + function set_funnyColor(col:Int):Int { grpTexts.forEach(function(txt) { @@ -55,7 +73,7 @@ class BGScrollingText extends FlxSpriteGroup return col; } - override function update(elapsed:Float) + override public function update(elapsed:Float) { for (txt in grpTexts.group) { @@ -66,14 +84,16 @@ class BGScrollingText extends FlxSpriteGroup if (txt.x < -txt.frameWidth) { txt.x = grpTexts.group.members[grpTexts.length - 1].x + grpTexts.group.members[grpTexts.length - 1].frameWidth + placementOffset; + sortTextShit(); } } else { - if (txt.x > widthShit) + if (txt.x > txt.frameWidth * 2) { txt.x = grpTexts.group.members[0].x - grpTexts.group.members[0].frameWidth - placementOffset; + sortTextShit(); } } diff --git a/source/funkin/freeplayStuff/CapsuleText.hx b/source/funkin/freeplayStuff/CapsuleText.hx new file mode 100644 index 000000000..dda687f5e --- /dev/null +++ b/source/funkin/freeplayStuff/CapsuleText.hx @@ -0,0 +1,49 @@ +package funkin.freeplayStuff; + +import openfl.filters.BitmapFilterQuality; +import flixel.text.FlxText; +import flixel.group.FlxSpriteGroup; +import funkin.shaderslmfao.GaussianBlurShader; + +class CapsuleText extends FlxSpriteGroup +{ + public var blurredText:FlxText; + + var whiteText:FlxText; + + public var text(default, set):String; + + public function new(x:Float, y:Float, songTitle:String, size:Float) + { + super(x, y); + + blurredText = initText(songTitle, size); + blurredText.shader = new GaussianBlurShader(1); + whiteText = initText(songTitle, size); + // whiteText.shader = new GaussianBlurShader(0.3); + text = songTitle; + + blurredText.color = 0xFF00ccff; + whiteText.color = 0xFFFFFFFF; + add(blurredText); + add(whiteText); + } + + function initText(songTitle, size:Float):FlxText + { + var text:FlxText = new FlxText(0, 0, 0, songTitle, Std.int(size)); + text.font = "5by7"; + return text; + } + + function set_text(value:String):String + { + blurredText.text = value; + whiteText.text = value; + whiteText.textField.filters = [ + new openfl.filters.GlowFilter(0x00ccff, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), + // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW) + ]; + return value; + } +} diff --git a/source/funkin/freeplayStuff/DJBoyfriend.hx b/source/funkin/freeplayStuff/DJBoyfriend.hx index 5bee4129a..ba0ce464d 100644 --- a/source/funkin/freeplayStuff/DJBoyfriend.hx +++ b/source/funkin/freeplayStuff/DJBoyfriend.hx @@ -3,8 +3,12 @@ package funkin.freeplayStuff; import flixel.FlxSprite; import flixel.util.FlxSignal; import funkin.util.assets.FlxAnimationUtil; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import flixel.system.FlxSound; +import flixel.util.FlxTimer; +import funkin.audio.FlxStreamSound; -class DJBoyfriend extends FlxSprite +class DJBoyfriend extends FlxAtlasSprite { // Represents the sprite's current status. // Without state machines I would have driven myself crazy years ago. @@ -20,20 +24,55 @@ class DJBoyfriend extends FlxSprite // TODO: Switch this class to use SwagSprite instead. public var animOffsets:Map>; - static final SPOOK_PERIOD:Float = 180.0; + var gotSpooked:Bool = false; + + static final SPOOK_PERIOD:Float = 120.0; + static final TV_PERIOD:Float = 180.0; // Time since dad last SPOOKED you. var timeSinceSpook:Float = 0; public function new(x:Float, y:Float) { - super(x, y); + super(x, y, Paths.animateAtlas("freeplay/freeplay-boyfriend", "preload")); animOffsets = new Map>(); - setupAnimations(); + anim.callback = function(name, number) { + switch (name) + { + case "Boyfriend DJ watchin tv OG": + if (number == 85) runTvLogic(); + default: + } + }; - animation.finishCallback = onFinishAnim; + setupAnimations(); + trace(listAnimations()); + + FlxG.debugger.track(this); + FlxG.console.registerObject("dj", this); + + anim.onComplete = onFinishAnim; + + FlxG.console.registerFunction("tv", function() { + currentState = TV; + }); + } + + /* + [remote hand under,boyfriend top head,brim piece,arm cringe l,red lazer,dj arm in,bf fist pump arm,hand raised right,forearm left,fist shaking,bf smile eyes closed face,arm cringe r,bf clenched face,face shrug,boyfriend falling,blue tint 1,shirt sleeve,bf clenched fist,head BF relaxed,blue tint 2,hand down left,blue tint 3,blue tint 4,head less smooshed,blue tint 5,boyfriend freeplay,BF head slight turn,blue tint 6,arm shrug l,blue tint 7,shoulder raised w sleeve,blue tint 8,fist pump face,blue tint 9,foot rested light,hand turnaround,arm chill right,Boyfriend DJ,arm shrug r,head back bf,hat top piece,dad bod,face surprise snap,Boyfriend DJ fist pump,office chair,foot rested right,chest down,office chair upright,body chill,bf dj afk,head mouth open dad,BF Head defalt HAIR BLOWING,hand shrug l,face piece,foot wag,turn table,shoulder up left,turntable lights,boyfriend dj body shirt blowing,body chunk turned,hand down right,dj arm out,hand shrug r,body chest out,rave hand,palm,chill face default,head back semi bf,boyfriend bottom head,DJ arm,shoulder right dad,bf surprise,boyfriend dj body,hs1,Boyfriend DJ watchin tv OG,spinning disk,hs2,arm chill left,boyfriend dj intro,hs3,hs4,chill face extra,hs5,remote hand upright,hs6,pant over table,face surprise,bf arm peace,arm turnaround,bf eyes 1,arm slammed table,eye squit,leg BF,head mid piece,arm backing,arm swoopin in,shoe right lowering,forearm right,hand out,blue tint 10,body falling back,remote thumb press,shoulder,hair spike single,bf bent + arm,crt,foot raised right,dad hand,chill face 1,chill face 2,clenched fist,head SMOOSHED,shoulder left dad,df1,body chunk upright,df2,df3,df4,hat front piece,df5,foot rested right 2,hand in,arm spun,shoe raised left,bf 1 finger hand,bf mouth 1,Boyfriend DJ confirm,forearm down ,hand raised left,remote thumb up] + */ + override public function listAnimations():Array + { + var anims:Array = []; + @:privateAccess + for (animKey in anim.symbolDictionary) + { + anims.push(animKey.name); + } + return anims; } public override function update(elapsed:Float):Void @@ -44,51 +83,68 @@ class DJBoyfriend extends FlxSprite { case Intro: // Play the intro animation then leave this state immediately. - if (getCurrentAnimation() != 'intro') playAnimation('intro', true); + if (getCurrentAnimation() != 'boyfriend dj intro') playFlashAnimation('boyfriend dj intro', true); timeSinceSpook = 0; case Idle: // We are in this state the majority of the time. - if (getCurrentAnimation() != 'idle' || animation.finished) + if (getCurrentAnimation() != 'Boyfriend DJ' || anim.finished) { - if (timeSinceSpook > SPOOK_PERIOD) + if (timeSinceSpook > SPOOK_PERIOD && !gotSpooked) { currentState = Spook; } + else if (timeSinceSpook > TV_PERIOD) + { + currentState = TV; + } else { - playAnimation('idle', false); + playFlashAnimation('Boyfriend DJ', false); } } timeSinceSpook += elapsed; case Confirm: - if (getCurrentAnimation() != 'confirm') playAnimation('confirm', false); + if (getCurrentAnimation() != 'Boyfriend DJ confirm') playFlashAnimation('Boyfriend DJ confirm', false); timeSinceSpook = 0; case Spook: - if (getCurrentAnimation() != 'spook') + if (getCurrentAnimation() != 'bf dj afk') { onSpook.dispatch(); - playAnimation('spook', false); + playFlashAnimation('bf dj afk', false); } timeSinceSpook = 0; + case TV: + if (getCurrentAnimation() != 'Boyfriend DJ watchin tv OG') playFlashAnimation('Boyfriend DJ watchin tv OG', true); + timeSinceSpook = 0; default: // I shit myself. } } - function onFinishAnim(name:String):Void + function onFinishAnim():Void { + var name = anim.curSymbol.name; switch (name) { - case "intro": + case "boyfriend dj intro": // trace('Finished intro'); currentState = Idle; onIntroDone.dispatch(); - case "idle": + case "Boyfriend DJ": // trace('Finished idle'); - case "spook": + case "bf dj afk": // trace('Finished spook'); currentState = Idle; - case "confirm": + case "Boyfriend DJ confirm": + + case "Boyfriend DJ watchin tv OG": + var frame:Int = FlxG.random.bool(33) ? 112 : 166; + if (FlxG.random.bool(10)) + { + frame = 60; + // boyfriend switches channel code? + } + anim.play("Boyfriend DJ watchin tv OG", true, false, frame); // trace('Finished confirm'); } } @@ -100,19 +156,66 @@ class DJBoyfriend extends FlxSprite function setupAnimations():Void { - frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk')); + // frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk')); - animation.addByPrefix('intro', "boyfriend dj intro", 24, false); - addOffset('intro', 0, 0); + // animation.addByPrefix('intro', "boyfriend dj intro", 24, false); + addOffset('boyfriend dj intro', 8, 3); - animation.addByPrefix('idle', "Boyfriend DJ0", 24, false); - addOffset('idle', -4, -426); + // animation.addByPrefix('idle', "Boyfriend DJ0", 24, false); + addOffset('Boyfriend DJ', 0, 0); - animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false); - addOffset('confirm', 40, -451); + // animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false); + addOffset('Boyfriend DJ confirm', 0, 0); - animation.addByPrefix('spook', "bf dj afk0", 24, false); - addOffset('spook', -3, -272); + // animation.addByPrefix('spook', "bf dj afk0", 24, false); + addOffset('bf dj afk', 0, 0); + } + + var cartoonSnd:FlxStreamSound; + + public var playingCartoon:Bool = false; + + public function runTvLogic() + { + if (cartoonSnd == null) + { + // tv is OFF, but getting turned on + FlxG.sound.play(Paths.sound('tv_on')); + + cartoonSnd = new FlxStreamSound(); + FlxG.sound.defaultSoundGroup.add(cartoonSnd); + } + else + { + // plays it smidge after the click + new FlxTimer().start(0.1, function(_) { + FlxG.sound.play(Paths.sound('channel_switch')); + }); + } + // cartoonSnd.loadEmbedded(Paths.sound("cartoons/peck")); + // cartoonSnd.play(); + + loadCartoon(); + } + + function loadCartoon() + { + cartoonSnd.loadEmbedded(Paths.sound(getRandomFlashToon()), false, false, function() { + anim.play("Boyfriend DJ watchin tv OG", true, false, 60); + }); + cartoonSnd.play(true, FlxG.random.float(0, cartoonSnd.length)); + } + + var cartoonList:Array = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/")); + + function getRandomFlashToon():String + { + var randomFile = FlxG.random.getObject(cartoonList); + + randomFile = randomFile.replace("assets/sounds/", ""); + randomFile = randomFile.substring(0, randomFile.length - 4); + + return randomFile; } public function confirm():Void @@ -125,15 +228,15 @@ class DJBoyfriend extends FlxSprite animOffsets[name] = [x, y]; } - public function getCurrentAnimation():String + override public function getCurrentAnimation():String { - if (this.animation == null || this.animation.curAnim == null) return ""; - return this.animation.curAnim.name; + if (this.anim == null || this.anim.curSymbol == null) return ""; + return this.anim.curSymbol.name; } - public function playAnimation(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void + public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void { - animation.play(AnimName, Force, Reversed, Frame); + anim.play(id, Force, Reverse, Frame); applyAnimOffset(); } @@ -156,4 +259,5 @@ enum DJBoyfriendState Idle; Confirm; Spook; + TV; } diff --git a/source/funkin/freeplayStuff/DifficultyStars.hx b/source/funkin/freeplayStuff/DifficultyStars.hx new file mode 100644 index 000000000..8611727be --- /dev/null +++ b/source/funkin/freeplayStuff/DifficultyStars.hx @@ -0,0 +1,106 @@ +package funkin.freeplayStuff; + +import flixel.group.FlxSpriteGroup; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.shaderslmfao.HSVShader; + +class DifficultyStars extends FlxSpriteGroup +{ + /** + * Internal handler var for difficulty... ranges from 0... to 15 + * 0 is 1 star... 15 is 0 stars! + */ + var curDifficulty(default, set):Int = 0; + + /** + * Range between 0 and 15 + */ + public var difficulty(default, set):Int = 1; + + public var stars:FlxAtlasSprite; + + var flames:FreeplayFlames; + + var hsvShader:HSVShader; + + public function new(x:Float, y:Float) + { + super(x, y); + + hsvShader = new HSVShader(); + + flames = new FreeplayFlames(0, 0); + add(flames); + + stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars")); + stars.anim.play("diff stars"); + add(stars); + + stars.shader = hsvShader; + + for (memb in flames.members) + memb.shader = hsvShader; + } + + override function update(elapsed:Float):Void + { + super.update(elapsed); + + // "loops" the current animation + // for clarity, the animation file looks like + // frame : stars + // 0-99: 1 star + // 100-199: 2 stars + // ...... + // 1300-1499: 15 stars + // 1500 : 0 stars + if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100) + { + stars.anim.play("diff stars", true, false, curDifficulty * 100); + } + } + + function set_difficulty(value:Int):Int + { + difficulty = value; + + if (difficulty <= 0) + { + difficulty = 0; + curDifficulty = 15; + } + else if (difficulty <= 15) + { + difficulty = value; + curDifficulty = difficulty - 1; + } + else + { + difficulty = 15; + curDifficulty = difficulty - 1; + } + + if (difficulty > 10) flames.flameCount = difficulty - 10; + else + flames.flameCount = 0; + + return difficulty; + } + + function set_curDifficulty(value:Int):Int + { + curDifficulty = value; + if (curDifficulty == 15) + { + stars.anim.play("diff stars", true, false, 1500); + stars.anim.pause(); + } + else + { + stars.anim.curFrame = Std.int(curDifficulty * 100); + stars.anim.play("diff stars", true, false, curDifficulty * 100); + } + + return curDifficulty; + } +} diff --git a/source/funkin/freeplayStuff/FreeplayFlames.hx b/source/funkin/freeplayStuff/FreeplayFlames.hx new file mode 100644 index 000000000..8f54d210b --- /dev/null +++ b/source/funkin/freeplayStuff/FreeplayFlames.hx @@ -0,0 +1,117 @@ +package funkin.freeplayStuff; + +import flixel.group.FlxSpriteGroup; +import flixel.FlxSprite; +import flixel.util.FlxTimer; + +class FreeplayFlames extends FlxSpriteGroup +{ + var flameX(default, set):Float = 917; + var flameY(default, set):Float = 103; + var flameSpreadX(default, set):Float = 29; + var flameSpreadY(default, set):Float = 6; + + public var flameCount(default, set):Int = 0; + + var flameTimer:Float = 0.25; + + public function new(x:Float, y:Float) + { + super(x, y); + + for (i in 0...5) + { + var flame:FlxSprite = new FlxSprite(flameX + (flameSpreadX * i), flameY + (flameSpreadY * i)); + flame.frames = Paths.getSparrowAtlas("freeplay/freeplayFlame"); + flame.animation.addByPrefix("flame", "fire loop", FlxG.random.int(23, 25), false); + flame.animation.play("flame"); + flame.visible = false; + flameCount = 0; + + // sets the loop... maybe better way to do this lol! + flame.animation.finishCallback = function(_) { + flame.animation.play("flame", true, false, 2); + }; + add(flame); + } + } + + var properPositions:Bool = false; + + override public function update(elapsed:Float):Void + { + super.update(elapsed); + // doesn't work in create()/new() for some reason + // so putting it here bwah! + if (!properPositions) + { + setFlamePositions(); + properPositions = true; + } + } + + function set_flameCount(value:Int):Int + { + this.flameCount = value; + var visibleCount:Int = 0; + for (i in 0...5) + { + if (members[i] == null) continue; + var flame:FlxSprite = members[i]; + if (i < flameCount) + { + if (!flame.visible) + { + new FlxTimer().start(flameTimer * visibleCount, function(_) { + flame.animation.play("flame", true); + flame.visible = true; + }); + visibleCount++; + } + } + else + { + flame.visible = false; + } + } + return this.flameCount; + } + + function setFlamePositions() + { + for (i in 0...5) + { + var flame:FlxSprite = members[i]; + flame.x = flameX + (flameSpreadX * i); + flame.y = flameY + (flameSpreadY * i); + } + } + + function set_flameX(value:Float):Float + { + this.flameX = value; + setFlamePositions(); + return this.flameX; + } + + function set_flameY(value:Float):Float + { + this.flameY = value; + setFlamePositions(); + return this.flameY; + } + + function set_flameSpreadX(value:Float):Float + { + this.flameSpreadX = value; + setFlamePositions(); + return this.flameSpreadX; + } + + function set_flameSpreadY(value:Float):Float + { + this.flameSpreadY = value; + setFlamePositions(); + return this.flameSpreadY; + } +} diff --git a/source/funkin/freeplayStuff/LetterSort.hx b/source/funkin/freeplayStuff/LetterSort.hx index c3b22f973..e6d923c90 100644 --- a/source/funkin/freeplayStuff/LetterSort.hx +++ b/source/funkin/freeplayStuff/LetterSort.hx @@ -4,38 +4,68 @@ import flixel.FlxSprite; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.tweens.FlxTween; +import flixel.tweens.FlxEase; +import flixel.util.FlxColor; +import flixel.util.FlxTimer; +import funkin.graphics.adobeanimate.FlxAtlasSprite; -class LetterSort extends FlxTypedSpriteGroup +class LetterSort extends FlxTypedSpriteGroup { public var letters:Array = []; - var curSelection:Int = 0; + // starts at 2, cuz that's the middle letter on start (accounting for fav and #, it should begin at ALL filter) + var curSelection:Int = 2; public var changeSelectionCallback:String->Void; + var leftArrow:FlxSprite; + var rightArrow:FlxSprite; + var grpSeperators:Array = []; + public function new(x, y) { super(x, y); - var leftArrow:FreeplayLetter = new FreeplayLetter(-20, 0); - leftArrow.animation.play("arrow"); + leftArrow = new FlxSprite(-20, 15).loadGraphic(Paths.image("freeplay/miniArrow")); + // leftArrow.animation.play("arrow"); + leftArrow.flipX = true; add(leftArrow); - for (i in 0...6) + for (i in 0...5) { var letter:FreeplayLetter = new FreeplayLetter(i * 80, 0, i); + letter.x += 50; + letter.y += 50; + letter.ogY = y; + // letter.visible = false; add(letter); letters.push(letter); - if (i == 3) letter.alpha = 0.6; + if (i != 2) letter.scale.x = letter.scale.y = 0.8; - var sep:FreeplayLetter = new FreeplayLetter((i * 80) + 50, 0); - sep.animation.play("seperator"); + var darkness:Float = Math.abs(i - 2) / 6; + + letter.color = letter.color.getDarkened(darkness); + + // don't put the last seperator + if (i == 4) continue; + + var sep:FlxSprite = new FlxSprite((i * 80) + 55, 20).loadGraphic(Paths.image("freeplay/seperator")); + // sep.animation.play("seperator"); + sep.color = letter.color.getDarkened(darkness); add(sep); + + grpSeperators.push(sep); } - // changeSelection(-3); + rightArrow = new FlxSprite(380, 15).loadGraphic(Paths.image("freeplay/miniArrow")); + + // rightArrow.animation.play("arrow"); + add(rightArrow); + + changeSelection(0); } override function update(elapsed:Float) @@ -48,53 +78,168 @@ class LetterSort extends FlxTypedSpriteGroup public function changeSelection(diff:Int = 0) { - for (letter in letters) - letter.changeLetter(diff); + var ezTimer:Int->FlxSprite->Float->Void = function(frameNum:Int, spr:FlxSprite, offsetNum:Float) { + new FlxTimer().start(frameNum / 24, function(_) { + spr.offset.x = offsetNum; + }); + }; - if (changeSelectionCallback != null) changeSelectionCallback(letters[3].arr[letters[3].curLetter]); // bullshit and long lol! + var positions:Array = [-10, -22, 2, 0]; + + if (diff < 0) + { + for (sep in grpSeperators) + { + ezTimer(0, sep, positions[0]); + ezTimer(1, sep, positions[1]); + ezTimer(2, sep, positions[2]); + ezTimer(3, sep, positions[3]); + } + + for (index => letter in letters) + { + letter.offset.x = positions[0]; + + new FlxTimer().start(1 / 24, function(_) { + letter.offset.x = positions[1]; + if (index == 0) letter.visible = false; + }); + + new FlxTimer().start(2 / 24, function(_) { + letter.offset.x = positions[2]; + if (index == 0.) letter.visible = true; + }); + + if (index == 2) + { + ezTimer(3, letter, 0); + // letter.offset.x = 0; + continue; + } + + ezTimer(3, letter, positions[3]); + } + + leftArrow.offset.x = 3; + new FlxTimer().start(2 / 24, function(_) { + leftArrow.offset.x = 0; + }); + } + else if (diff > 0) + { + for (sep in grpSeperators) + { + ezTimer(0, sep, -positions[0]); + ezTimer(1, sep, -positions[1]); + ezTimer(2, sep, -positions[2]); + ezTimer(3, sep, -positions[3]); + } + // same timing and functions and shit as the left one... except to the right!! + + for (index => letter in letters) + { + letter.offset.x = -positions[0]; + + new FlxTimer().start(1 / 24, function(_) { + letter.offset.x = -positions[1]; + if (index == 0) letter.visible = false; + }); + + new FlxTimer().start(2 / 24, function(_) { + letter.offset.x = -positions[2]; + if (index == 0) letter.visible = true; + }); + + if (index == 2) + { + ezTimer(3, letter, 0); + // letter.offset.x = 0; + continue; + } + + ezTimer(3, letter, -positions[3]); + } + + rightArrow.offset.x = -3; + new FlxTimer().start(2 / 24, function(_) { + rightArrow.offset.x = 0; + }); + } + + curSelection += diff; + if (curSelection < 0) curSelection = letters[0].arr.length - 1; + if (curSelection >= letters[0].arr.length) curSelection = 0; + + for (letter in letters) + letter.changeLetter(diff, curSelection); + + if (changeSelectionCallback != null) changeSelectionCallback(letters[2].arr[letters[2].curLetter]); // bullshit and long lol! } } -class FreeplayLetter extends FlxSprite +class FreeplayLetter extends FlxAtlasSprite { public var arr:Array = []; public var curLetter:Int = 0; + public var ogY:Float = 0; + public function new(x:Float, y:Float, ?letterInd:Int) { - super(x, y); + super(x, y, Paths.animateAtlas("freeplay/sortedLetters")); + // frames = Paths.getSparrowAtlas("freeplay/letterStuff"); + // this.anim.play("AB"); + // trace(this.anim.symbolDictionary); - frames = Paths.getSparrowAtlas("freeplay/letterStuff"); - - var alphabet:String = "abcdefghijklmnopqrstuvwxyz"; - arr = alphabet.split(""); - arr.insert(0, "#"); + var alphabet:String = "AB-CD-EH-I L-MN-OR-s-t-UZ"; + arr = alphabet.split("-"); arr.insert(0, "ALL"); arr.insert(0, "fav"); + arr.insert(0, "#"); - for (str in arr) - { - animation.addByPrefix(str, str + " "); // string followed by a space! intentional! - } + // trace(arr); - animation.addByPrefix("arrow", "mini arrow"); - animation.addByPrefix("seperator", "seperator"); + // for (str in arr) + // { + // animation.addByPrefix(str, str + " "); // string followed by a space! intentional! + // } + + // animation.addByPrefix("arrow", "mini arrow"); + // animation.addByPrefix("seperator", "seperator"); if (letterInd != null) { - animation.play(arr[letterInd]); + this.anim.play(arr[letterInd] + " move"); + this.anim.pause(); curLetter = letterInd; } } - public function changeLetter(diff:Int = 0) + public function changeLetter(diff:Int = 0, ?curSelection:Int) { curLetter += diff; if (curLetter < 0) curLetter = arr.length - 1; if (curLetter >= arr.length) curLetter = 0; - animation.play(arr[curLetter]); + var animName:String = arr[curLetter] + " move"; + + switch (arr[curLetter]) + { + case "I L": + animName = "IL move"; + case "s": + animName = "S move"; + case "t": + animName = "T move"; + } + + this.anim.play(animName); + if (curSelection != curLetter) + { + this.anim.pause(); + } + // updateHitbox(); } } diff --git a/source/funkin/freeplayStuff/SongMenuItem.hx b/source/funkin/freeplayStuff/SongMenuItem.hx index 37198f6d7..5fd7eb576 100644 --- a/source/funkin/freeplayStuff/SongMenuItem.hx +++ b/source/funkin/freeplayStuff/SongMenuItem.hx @@ -1,5 +1,8 @@ package funkin.freeplayStuff; +import funkin.shaderslmfao.HSVShader; +import funkin.shaderslmfao.GaussianBlurShader; +import flixel.group.FlxGroup; import flixel.FlxSprite; import flixel.graphics.frames.FlxAtlasFrames; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; @@ -7,17 +10,29 @@ import flixel.group.FlxSpriteGroup; import flixel.math.FlxMath; import flixel.math.FlxPoint; import flixel.text.FlxText; +import flixel.util.FlxTimer; +import funkin.shaderslmfao.Grayscale; class SongMenuItem extends FlxSpriteGroup { - var capsule:FlxSprite; + public var capsule:FlxSprite; - public var selected(default, set):Bool = false; + var pixelIcon:FlxSprite; + + public var selected(default, set):Bool; public var songTitle:String = "Test"; - public var songText:FlxText; + public var songText:CapsuleText; public var favIcon:FlxSprite; + public var ranking:FlxSprite; + + var ranks:Array = ["fail", "average", "great", "excellent", "perfect"]; + + // lol... + var diffRanks:Array = [ + "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "14", "15" + ]; public var targetPos:FlxPoint = new FlxPoint(); public var doLerp:Bool = false; @@ -25,7 +40,12 @@ class SongMenuItem extends FlxSpriteGroup public var doJumpOut:Bool = false; - public function new(x:Float, y:Float, song:String) + public var onConfirm:Void->Void; + public var diffGrayscale:Grayscale; + + public var hsvShader(default, set):HSVShader; + + public function new(x:Float, y:Float, song:String, ?character:String) { super(x, y); @@ -38,19 +58,144 @@ class SongMenuItem extends FlxSpriteGroup // capsule.animation add(capsule); - songText = new FlxText(120, 40, 0, songTitle, 40); - songText.font = "5by7"; - songText.color = 0xFF43C1EA; - add(songText); + // doesn't get added, simply is here to help with visibility of things for the pop in! + grpHide = new FlxGroup(); - favIcon = new FlxSprite(440, 40); + var rank:String = FlxG.random.getObject(ranks); + + ranking = new FlxSprite(capsule.width * 0.84, 30); + ranking.loadGraphic(Paths.image("freeplay/ranks/" + rank)); + ranking.scale.x = ranking.scale.y = realScaled; + ranking.alpha = 0.75; + ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y); + add(ranking); + grpHide.add(ranking); + + diffGrayscale = new Grayscale(1); + + var diffRank = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRankings/diff" + FlxG.random.getObject(diffRanks))); + diffRank.shader = diffGrayscale; + diffRank.visible = false; + add(diffRank); + diffRank.origin.set(capsule.origin.x - diffRank.x, capsule.origin.y - diffRank.y); + grpHide.add(diffRank); + + switch (rank) + { + case "perfect": + ranking.x -= 10; + } + + songText = new CapsuleText(capsule.width * 0.26, 45, songTitle, Std.int(40 * realScaled)); + add(songText); + grpHide.add(songText); + + pixelIcon = new FlxSprite(155, 15); + pixelIcon.makeGraphic(32, 32, 0x00000000); + pixelIcon.antialiasing = false; + pixelIcon.active = false; + add(pixelIcon); + grpHide.add(pixelIcon); + + if (character != null) setCharacter(character); + + favIcon = new FlxSprite(400, 40); favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart'); favIcon.animation.addByPrefix('fav', "favorite heart", 24, false); favIcon.animation.play('fav'); - favIcon.setGraphicSize(60, 60); + favIcon.setGraphicSize(50, 50); + favIcon.visible = false; add(favIcon); + // grpHide.add(favIcon); - selected = selected; // just to kickstart the set_selected + setVisibleGrp(false); + } + + function set_hsvShader(value:HSVShader):HSVShader + { + this.hsvShader = value; + capsule.shader = hsvShader; + songText.shader = hsvShader; + + return value; + } + + function textAppear() + { + songText.scale.x = 1.7; + songText.scale.y = 0.2; + + new FlxTimer().start(1 / 24, function(_) { + songText.scale.x = 0.4; + songText.scale.y = 1.4; + }); + + new FlxTimer().start(2 / 24, function(_) { + songText.scale.x = songText.scale.y = 1; + }); + } + + function setVisibleGrp(value:Bool) + { + for (spr in grpHide.members) + { + spr.visible = value; + } + + if (value) textAppear(); + + selectedAlpha(); + } + + public function init(x:Float, y:Float, song:String, ?character:String) + { + this.x = x; + this.y = y; + this.songTitle = song; + songText.text = this.songTitle; + if (character != null) setCharacter(character); + + selected = selected; + } + + /** + * Set the character displayed next to this song in the freeplay menu. + * @param char The character ID used by this song. + * If the character has no freeplay icon, a warning will be thrown and nothing will display. + */ + public function setCharacter(char:String) + { + var charPath:String = "freeplay/icons/"; + + trace(char); + + switch (char) + { + case "monster-christmas": + charPath += "monsterpixel"; + case "mom-car": + charPath += "mommypixel"; + case "dad": + charPath += "daddypixel"; + case "darnell-blazin": + charPath += "darnellpixel"; + case "senpai-angry": + charPath += "senpaipixel"; + default: + charPath += char + "pixel"; + } + + if (!openfl.utils.Assets.exists(Paths.image(charPath))) + { + trace('[WARN] Character ${char} has no freeplay icon.'); + return; + } + + pixelIcon.loadGraphic(Paths.image(charPath)); + pixelIcon.scale.x = pixelIcon.scale.y = 2; + pixelIcon.origin.x = 100; + // pixelIcon.origin.x = capsule.origin.x; + // pixelIcon.offset.x -= pixelIcon.origin.x; } var frameInTicker:Float = 0; @@ -63,6 +208,63 @@ class SongMenuItem extends FlxSpriteGroup var xPosLerpLol:Array = [0.9, 0.4, 0.16, 0.16, 0.22, 0.22, 0.245]; // NUMBERS ARE JANK CUZ THE SCALING OR WHATEVER var xPosOutLerpLol:Array = [0.245, 0.75, 0.98, 0.98, 1.2]; // NUMBERS ARE JANK CUZ THE SCALING OR WHATEVER + public var realScaled:Float = 0.8; + + public function initJumpIn(maxTimer:Float, ?force:Bool):Void + { + frameInTypeBeat = 0; + + new FlxTimer().start((1 / 24) * maxTimer, function(doShit) { + doJumpIn = true; + }); + + new FlxTimer().start((0.09 * maxTimer) + 0.85, function(lerpTmr) { + doLerp = true; + }); + + if (force) + { + visible = true; + capsule.alpha = 1; + setVisibleGrp(true); + } + else + { + new FlxTimer().start((xFrames.length / 24) * 2.5, function(_) { + visible = true; + capsule.alpha = 1; + setVisibleGrp(true); + }); + } + } + + var grpHide:FlxGroup; + + public function forcePosition() + { + visible = true; + capsule.alpha = 1; + selectedAlpha(); + doLerp = true; + doJumpIn = false; + doJumpOut = false; + + frameInTypeBeat = xFrames.length; + frameOutTypeBeat = 0; + + capsule.scale.x = xFrames[frameInTypeBeat - 1]; + capsule.scale.y = 1 / xFrames[frameInTypeBeat - 1]; + // x = FlxG.width * xPosLerpLol[Std.int(Math.min(frameInTypeBeat - 1, xPosLerpLol.length - 1))]; + + x = targetPos.x; + y = targetPos.y; + + capsule.scale.x *= realScaled; + capsule.scale.y *= realScaled; + + setVisibleGrp(true); + } + override function update(elapsed:Float) { if (doJumpIn) @@ -73,10 +275,13 @@ class SongMenuItem extends FlxSpriteGroup { frameInTicker = 0; - scale.x = xFrames[frameInTypeBeat]; - scale.y = 1 / xFrames[frameInTypeBeat]; + capsule.scale.x = xFrames[frameInTypeBeat]; + capsule.scale.y = 1 / xFrames[frameInTypeBeat]; x = FlxG.width * xPosLerpLol[Std.int(Math.min(frameInTypeBeat, xPosLerpLol.length - 1))]; + capsule.scale.x *= realScaled; + capsule.scale.y *= realScaled; + frameInTypeBeat += 1; } } @@ -89,10 +294,13 @@ class SongMenuItem extends FlxSpriteGroup { frameOutTicker = 0; - scale.x = xFrames[frameOutTypeBeat]; - scale.y = 1 / xFrames[frameOutTypeBeat]; + capsule.scale.x = xFrames[frameOutTypeBeat]; + capsule.scale.y = 1 / xFrames[frameOutTypeBeat]; x = FlxG.width * xPosOutLerpLol[Std.int(Math.min(frameOutTypeBeat, xPosOutLerpLol.length - 1))]; + capsule.scale.x *= realScaled; + capsule.scale.y *= realScaled; + frameOutTypeBeat += 1; } } @@ -106,14 +314,29 @@ class SongMenuItem extends FlxSpriteGroup super.update(elapsed); } + public function intendedY(index:Int):Float + { + return (index * ((height * realScaled) + 10)) + 120; + } + + /** + * Merely a helper function to call set_selected, to make sure that the alpha is correct on the rankings/selections + */ + public function selectedAlpha():Void + { + selected = selected; + } + function set_selected(value:Bool):Bool { - // trace(value); - // cute one liners, lol! + diffGrayscale.setAmount(value ? 0 : 0.8); songText.alpha = value ? 1 : 0.6; + songText.blurredText.visible = value ? true : false; capsule.offset.x = value ? 0 : -5; capsule.animation.play(value ? "selected" : "unselected"); + ranking.alpha = value ? 1 : 0.7; + ranking.color = value ? 0xFFFFFFFF : 0xFFAAAAAA; return value; } } diff --git a/source/funkin/input/PreciseInputManager.hx b/source/funkin/input/PreciseInputManager.hx index 6217b2fe7..4cce0964d 100644 --- a/source/funkin/input/PreciseInputManager.hx +++ b/source/funkin/input/PreciseInputManager.hx @@ -181,7 +181,7 @@ class PreciseInputManager extends FlxKeyManager updateKeyStates(key, true); - if (getInputByKey(key) ?.justPressed ?? false) + if (getInputByKey(key)?.justPressed ?? false) { onInputPressed.dispatch( { @@ -203,7 +203,7 @@ class PreciseInputManager extends FlxKeyManager updateKeyStates(key, false); - if (getInputByKey(key) ?.justReleased ?? false) + if (getInputByKey(key)?.justReleased ?? false) { onInputReleased.dispatch( { @@ -264,7 +264,7 @@ class PreciseInputList extends FlxKeyList { for (key in getKeysForDir(noteDir)) { - if (check(_preciseInputManager.getInputByKey(key) ?.ID)) return true; + if (check(_preciseInputManager.getInputByKey(key)?.ID)) return true; } return false; } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 8eff610c2..d56d2e1a4 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -47,7 +47,7 @@ import funkin.play.song.Song; import funkin.data.song.SongRegistry; import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; -import funkin.data.song.SongData.SongPlayableChar; +import funkin.data.song.SongData.SongCharacterData; import funkin.play.stage.Stage; import funkin.play.stage.StageData.StageDataParser; import funkin.ui.PopUpStuff; @@ -512,41 +512,7 @@ class PlayState extends MusicBeatSubState NoteSplash.buildSplashFrames(); - // Returns null if the song failed to load or doesn't have the selected difficulty. - if (currentSong == null || currentChart == null) - { - // We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic. - criticalFailure = true; - - // Choose an error message. - var message:String = 'There was a critical error. Click OK to return to the main menu.'; - if (currentSong == null) - { - message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.'; - } - else if (currentDifficulty == null) - { - message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.'; - } - else if (currentSong.getDifficulty(currentDifficulty) == null) - { - message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty. Click OK to return to the main menu.'; - } - - // Display a popup. This blocks the application until the user clicks OK. - lime.app.Application.current.window.alert(message, 'Error loading PlayState'); - - // Force the user back to the main menu. - if (isSubState) - { - this.close(); - } - else - { - FlxG.switchState(new MainMenuState()); - } - return; - } + if (!assertChartExists()) return; if (false) { @@ -575,8 +541,8 @@ class PlayState extends MusicBeatSubState // Prepare the current song's instrumental and vocals to be played. if (!overrideMusic && currentChart != null) { - currentChart.cacheInst(currentPlayerId); - currentChart.cacheVocals(currentPlayerId); + currentChart.cacheInst(); + currentChart.cacheVocals(); } // Prepare the Conductor. @@ -661,6 +627,47 @@ class PlayState extends MusicBeatSubState initialized = true; } + function assertChartExists():Bool + { + // Returns null if the song failed to load or doesn't have the selected difficulty. + if (currentSong == null || currentChart == null) + { + // We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic. + criticalFailure = true; + + // Choose an error message. + var message:String = 'There was a critical error. Click OK to return to the main menu.'; + if (currentSong == null) + { + message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.'; + } + else if (currentDifficulty == null) + { + message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.'; + } + else if (currentSong.getDifficulty(currentDifficulty) == null) + { + message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty. Click OK to return to the main menu.'; + } + + // Display a popup. This blocks the application until the user clicks OK. + lime.app.Application.current.window.alert(message, 'Error loading PlayState'); + + // Force the user back to the main menu. + if (isSubState) + { + this.close(); + } + else + { + FlxG.switchState(new MainMenuState()); + } + return false; + } + + return true; + } + public override function update(elapsed:Float):Void { if (criticalFailure) return; @@ -673,6 +680,8 @@ class PlayState extends MusicBeatSubState // Handle restarting the song when needed (player death or pressing Retry) if (needsReset) { + if (!assertChartExists()) return; + dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY)); resetCamera(); @@ -687,8 +696,10 @@ class PlayState extends MusicBeatSubState // Reset music properly. FlxG.sound.music.pause(); - vocals.pause(); FlxG.sound.music.time = (startTimestamp); + + vocals = currentChart.buildVocals(); + vocals.pause(); vocals.time = 0; FlxG.sound.music.volume = 1; @@ -734,7 +745,7 @@ class PlayState extends MusicBeatSubState // DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM! // :nerd: um ackshually it's not 13 it's 11.97278911564 - if (Paths.SOUND_EXT == 'mp3') Conductor.offset = Constants.MP3_DELAY_MS; + if (Constants.EXT_SOUND == 'mp3') Conductor.offset = Constants.MP3_DELAY_MS; Conductor.update(); @@ -1345,34 +1356,20 @@ class PlayState extends MusicBeatSubState trace('Song difficulty could not be loaded.'); } - // Switch the character we are playing as by manipulating currentPlayerId. - // TODO: How to choose which one to use for story mode? - var playableChars:Array = currentChart.getPlayableChars(); - - if (playableChars.length == 0) - { - trace('WARNING: No playable characters found for this song.'); - } - else if (playableChars.indexOf(currentPlayerId) == -1) - { - currentPlayerId = playableChars[0]; - } - - // - var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayerId); + var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the character we are playing as by manipulating currentPlayerId. // // GIRLFRIEND // - var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.girlfriend); + var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.girlfriend); if (girlfriend != null) { girlfriend.characterType = CharacterType.GF; } - else if (currentCharData.girlfriend != '') + else if (currentCharacterData.girlfriend != '') { - trace('WARNING: Could not load girlfriend character with ID ${currentCharData.girlfriend}, skipping...'); + trace('WARNING: Could not load girlfriend character with ID ${currentCharacterData.girlfriend}, skipping...'); } else { @@ -1382,7 +1379,7 @@ class PlayState extends MusicBeatSubState // // DAD // - var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.opponent); + var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.opponent); if (dad != null) { @@ -1401,7 +1398,7 @@ class PlayState extends MusicBeatSubState // // BOYFRIEND // - var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayerId); + var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.player); if (boyfriend != null) { @@ -1550,7 +1547,7 @@ class PlayState extends MusicBeatSubState if (!overrideMusic) { - vocals = currentChart.buildVocals(currentPlayerId); + vocals = currentChart.buildVocals(); if (vocals.members.length == 0) { @@ -1894,6 +1891,7 @@ class PlayState extends MusicBeatSubState { // Grant the player health. health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed; + songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed); } // TODO: Potential penalty for dropping a hold note? @@ -2014,103 +2012,6 @@ class PlayState extends MusicBeatSubState } } - /** - * Handle player inputs. - */ - function keyShit(test:Bool):Void - { - // control arrays, order L D R U - var holdArray:Array = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT]; - var pressArray:Array = [ - controls.NOTE_LEFT_P, - controls.NOTE_DOWN_P, - controls.NOTE_UP_P, - controls.NOTE_RIGHT_P - ]; - var releaseArray:Array = [ - controls.NOTE_LEFT_R, - controls.NOTE_DOWN_R, - controls.NOTE_UP_R, - controls.NOTE_RIGHT_R - ]; - - // if (pressArray.contains(true)) - // { - // var lol:Array = cast pressArray; - // inputSpitter.push(Std.int(Conductor.songPosition) + ' ' + lol.join(' ')); - // } - - // HOLDS, check for sustain notes - if (holdArray.contains(true) && generatedMusic) - { - /* - activeNotes.forEachAlive(function(daNote:Note) { - if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData]) goodNoteHit(daNote); - }); - */ - } - - // PRESSES, check for note hits - if (pressArray.contains(true) && generatedMusic) - { - Haptic.vibrate(100, 100); - - if (currentStage != null && currentStage.getBoyfriend() != null) - { - currentStage.getBoyfriend().holdTimer = 0; - } - - var possibleNotes:Array = []; // notes that can be hit - var directionList:Array = []; // directions that can be hit - var dumbNotes:Array = []; // notes to kill later - - for (note in dumbNotes) - { - FlxG.log.add('killing dumb ass note at ' + note.noteData.time); - note.kill(); - // activeNotes.remove(note, true); - note.destroy(); - } - - possibleNotes.sort((a, b) -> Std.int(a.noteData.time - b.noteData.time)); - - if (perfectMode) - { - goodNoteHit(possibleNotes[0], null); - } - else if (possibleNotes.length > 0) - { - for (shit in 0...pressArray.length) - { // if a direction is hit that shouldn't be - if (pressArray[shit] && !directionList.contains(shit)) ghostNoteMiss(shit); - } - for (coolNote in possibleNotes) - { - if (pressArray[coolNote.noteData.getDirection()]) goodNoteHit(coolNote, null); - } - } - else - { - // HNGGG I really want to add an option for ghost tapping - // L + ratio - for (shit in 0...pressArray.length) - if (pressArray[shit]) ghostNoteMiss(shit, false); - } - } - - if (currentStage == null) return; - - for (keyId => isPressed in pressArray) - { - if (playerStrumline == null) continue; - - var dir:NoteDirection = Strumline.DIRECTIONS[keyId]; - - if (isPressed && !playerStrumline.isConfirm(dir)) playerStrumline.playPress(dir); - if (!holdArray[keyId]) playerStrumline.playStatic(dir); - } - } - function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void { var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true); @@ -2119,19 +2020,16 @@ class PlayState extends MusicBeatSubState // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) return; - if (!note.isHoldNote) - { - Highscore.tallies.combo++; - Highscore.tallies.totalNotesHit++; + Highscore.tallies.combo++; + Highscore.tallies.totalNotesHit++; - if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; + if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; - popUpScore(note, input); - } + popUpScore(note, input); playerStrumline.hitNote(note); - if (note.holdNoteSprite != null) + if (note.isHoldNote && note.holdNoteSprite != null) { playerStrumline.playNoteHoldCover(note.holdNoteSprite); } diff --git a/source/funkin/play/cutscene/dialogue/ConversationData.hx b/source/funkin/play/cutscene/dialogue/ConversationData.hx index 749f1b7a1..8c4aa9684 100644 --- a/source/funkin/play/cutscene/dialogue/ConversationData.hx +++ b/source/funkin/play/cutscene/dialogue/ConversationData.hx @@ -172,9 +172,13 @@ enum abstract BackdropType(String) from String to String class MusicData { public var asset:String; - public var looped:Bool; + public var fadeTime:Float; + @:optional + @:default(false) + public var looped:Bool; + public function new(asset:String, looped:Bool, fadeTime:Float = 0.0) { this.asset = asset; diff --git a/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx b/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx index c25b3e87f..9f80f8f9b 100644 --- a/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx +++ b/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx @@ -6,6 +6,7 @@ import funkin.play.cutscene.dialogue.ScriptedConversation; /** * Contains utilities for loading and parsing conversation data. + * TODO: Refactor to use the json2object + BaseRegistry system that actually validates things for you. */ class ConversationDataParser { diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index e32eb8186..d11c7744b 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -11,7 +11,7 @@ import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongRegistry; import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongPlayableChar; +import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongTimeFormat; import funkin.data.IRegistryEntry; @@ -92,6 +92,15 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry chartData in charts) result.applyChartData(chartData, variation); @@ -144,10 +145,10 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry(); - if (metadata.playData.playableChars == null) continue; - for (charId in metadata.playData.playableChars.keys()) - { - var char:Null = metadata.playData.playableChars.get(charId); - if (char == null) continue; - difficulty.chars.set(charId, char); - } + difficulty.characters = metadata.playData.characters; } } } @@ -321,7 +315,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = SongRegistry.instance.fetchEntryMetadataVersion(id); if (version == null) return null; - return SongRegistry.instance.parseEntryMetadataWithMigration(id, '', version); + return SongRegistry.instance.parseEntryMetadataWithMigration(id, Constants.DEFAULT_VARIATION, version); } function fetchVariationMetadata(id:String):Array @@ -365,19 +359,20 @@ class SongDifficulty */ public var events:Array; - public var songName:String = SongValidator.DEFAULT_SONGNAME; - public var songArtist:String = SongValidator.DEFAULT_ARTIST; - public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT; - public var divisions:Null = SongValidator.DEFAULT_DIVISIONS; - public var looped:Bool = SongValidator.DEFAULT_LOOPED; + public var songName:String = Constants.DEFAULT_SONGNAME; + public var songArtist:String = Constants.DEFAULT_ARTIST; + public var timeFormat:SongTimeFormat = Constants.DEFAULT_TIMEFORMAT; + public var divisions:Null = null; + public var looped:Bool = false; public var generatedBy:String = SongRegistry.DEFAULT_GENERATEDBY; public var timeChanges:Array = []; - public var stage:String = SongValidator.DEFAULT_STAGE; - public var chars:Map = null; + public var stage:String = Constants.DEFAULT_STAGE; + public var noteStyle:String = Constants.DEFAULT_NOTE_STYLE; + public var characters:SongCharacterData = null; - public var scrollSpeed:Float = SongValidator.DEFAULT_SCROLLSPEED; + public var scrollSpeed:Float = Constants.DEFAULT_SCROLLSPEED; public function new(song:Song, diffId:String, variation:String) { @@ -401,28 +396,24 @@ class SongDifficulty return timeChanges[0].bpm; } - public function getPlayableChar(id:String):Null - { - if (id == null || id == '') return null; - return chars.get(id); - } - - public function getPlayableChars():Array - { - return chars.keys().array(); - } - public function getEvents():Array { return cast events; } - public inline function cacheInst(?currentPlayerId:String = null):Void + public function cacheInst(instrumental = ''):Void { - var currentPlayer:Null = getPlayableChar(currentPlayerId); - if (currentPlayer != null) + if (characters != null) { - FlxG.sound.cache(Paths.inst(this.song.id, currentPlayer.inst)); + if (instrumental != '' && characters.altInstrumentals.contains(instrumental)) + { + FlxG.sound.cache(Paths.inst(this.song.id, instrumental)); + } + else + { + // Fallback to default instrumental. + FlxG.sound.cache(Paths.inst(this.song.id, characters.instrumental)); + } } else { @@ -440,9 +431,9 @@ class SongDifficulty * Cache the vocals for a given character. * @param id The character we are about to play. */ - public inline function cacheVocals(?id:String = 'bf'):Void + public inline function cacheVocals():Void { - for (voice in buildVoiceList(id)) + for (voice in buildVoiceList()) { FlxG.sound.cache(voice); } @@ -454,22 +445,15 @@ class SongDifficulty * * @param id The character we are about to play. */ - public function buildVoiceList(?id:String = 'bf'):Array + public function buildVoiceList():Array { - var playableCharData:SongPlayableChar = getPlayableChar(id); - if (playableCharData == null) - { - trace('Could not find playable char $id for song ${this.song.id}'); - return []; - } - var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; // Automatically resolve voices by removing suffixes. // For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`. - var playerId:String = id; - var voicePlayer:String = Paths.voices(this.song.id, '-$id$suffix'); + var playerId:String = characters.player; + var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix'); while (voicePlayer != null && !Assets.exists(voicePlayer)) { // Remove the last suffix. @@ -479,7 +463,7 @@ class SongDifficulty voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix'); } - var opponentId:String = playableCharData.opponent; + var opponentId:String = characters.opponent; var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix'); while (voiceOpponent != null && !Assets.exists(voiceOpponent)) { @@ -505,11 +489,11 @@ class SongDifficulty * @param charId The player ID. * @return The generated vocal group. */ - public function buildVocals(charId:String = 'bf'):VoicesGroup + public function buildVocals():VoicesGroup { var result:VoicesGroup = new VoicesGroup(); - var voiceList:Array = buildVoiceList(charId); + var voiceList:Array = buildVoiceList(); if (voiceList.length == 0) { diff --git a/source/funkin/play/song/SongMigrator.hx b/source/funkin/play/song/SongMigrator.hx deleted file mode 100644 index 43393fa4e..000000000 --- a/source/funkin/play/song/SongMigrator.hx +++ /dev/null @@ -1,256 +0,0 @@ -package funkin.play.song; - -import funkin.play.song.formats.FNFLegacy; -import funkin.data.song.SongData.SongChartData; -import funkin.data.song.SongData.SongEventData; -import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongNoteData; -import funkin.data.song.SongData.SongPlayableChar; -import funkin.util.VersionUtil; - -class SongMigrator -{ - /** - * The current latest version string for the song data format. - * Handle breaking changes by incrementing this value - * and adding migration to the SongMigrator class. - */ - public static final CHART_VERSION:String = '2.0.0'; - - /** - * Version rule for which chart versions are compatible with the current version. - */ - public static final CHART_VERSION_RULE:String = '2.0.x'; - - /** - * Migrate song data from an older chart version to the current version. - * @param jsonData The song metadata to migrate. - * @param songId The ID of the song (only used for error reporting). - * @return The migrated song metadata, or null if the migration failed. - */ - public static function migrateSongMetadata(jsonData:Dynamic, songId:String):SongMetadata - { - if (jsonData.version != null) - { - if (VersionUtil.validateVersionStr(jsonData.version, CHART_VERSION_RULE)) - { - trace('Song (${songId}) metadata version (${jsonData.version}) is valid and up-to-date.'); - - var songMetadata:SongMetadata = cast jsonData; - - return songMetadata; - } - else - { - trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.'); - switch (jsonData.version) - { - case '1.0.0': - return migrateSongMetadataFromLegacy(jsonData); - default: - trace('Song (${songId}) has unknown metadata version (${jsonData.version}), assuming FNF Legacy.'); - return migrateSongMetadataFromLegacy(jsonData); - } - } - } - else - { - trace('Song metadata version is missing.'); - } - return null; - } - - /** - * Migrate song chart data from an older chart version to the current version. - * @param jsonData The song chart data to migrate. - * @param songId The ID of the song (only used for error reporting). - * @return The migrated song chart data, or null if the migration failed. - */ - public static function migrateSongChartData(jsonData:Dynamic, songId:String):SongChartData - { - if (jsonData.version) - { - if (VersionUtil.validateVersionStr(jsonData.version, CHART_VERSION_RULE)) - { - trace('Song (${songId}) chart version (${jsonData.version}) is valid and up-to-date.'); - - var songChartData:SongChartData = cast jsonData; - - return songChartData; - } - else - { - trace('Song (${songId}) chart version (${jsonData.version}) is outdated.'); - switch (jsonData.version) - { - // TODO: Add migration functions as cases here. - default: - // Unknown version. - trace('Song (${songId}) unknown chart version: ${jsonData.version}'); - } - } - } - else - { - trace('Song chart version is missing.'); - } - return null; - } - - /** - * Migrate song metadata from FNF Legacy chart version to the current version. - * @param jsonData The song metadata to migrate. - * @param songId The ID of the song (only used for error reporting). - * @return The migrated song metadata, or null if the migration failed. - */ - public static function migrateSongMetadataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongMetadata - { - trace('Migrating song metadata from FNF Legacy.'); - - var songData:FNFLegacy = cast jsonData; - - var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default'); - - var hadError:Bool = false; - - // Set generatedBy string for debugging. - songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)'; - - try - { - // Set the song's BPM. - songMetadata.timeChanges[0].bpm = songData.song.bpm; - } - catch (e) - { - trace("Couldn't parse BPM!"); - hadError = true; - } - - try - { - // Set the song's stage. - songMetadata.playData.stage = songData.song.stageDefault; - } - catch (e) - { - trace("Couldn't parse stage!"); - hadError = true; - } - - try - { - // Set's the song's name. - songMetadata.songName = songData.song.song; - } - catch (e) - { - trace("Couldn't parse song name!"); - hadError = true; - } - - songMetadata.playData.difficulties = []; - if (songData.song != null && songData.song.notes != null) - { - if (Std.isOfType(songData.song.notes, Array)) - { - // One difficulty of notes. - songMetadata.playData.difficulties.push(difficulty); - } - else - { - // Multiple difficulties of notes. - var songNoteDataDynamic:haxe.DynamicAccess = cast songData.song.notes; - for (difficultyKey in songNoteDataDynamic.keys()) - { - songMetadata.playData.difficulties.push(difficultyKey); - } - } - } - else - { - trace("Couldn't parse difficulties!"); - hadError = true; - } - - songMetadata.playData.songVariations = []; - - // Set the song's song variations. - songMetadata.playData.playableChars = []; - try - { - songMetadata.playData.playableChars.set(songData.song.player1, new SongPlayableChar('', songData.song.player2)); - } - catch (e) - { - trace("Couldn't parse characters!"); - hadError = true; - } - - return songMetadata; - } - - /** - * Migrate song chart data from FNF Legacy chart version to the current version. - * @param jsonData The song data to migrate. - * @param songId The ID of the song (only used for error reporting). - * @param difficulty The difficulty to migrate. - * @return The migrated song chart data, or null if the migration failed. - */ - public static function migrateSongChartDataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongChartData - { - trace('Migrating song chart data from FNF Legacy.'); - - var songData:FNFLegacy = cast jsonData; - - var songChartData:SongChartData = new SongChartData(["normal" => 1.0], [], ["normal" => []]); - - var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; - if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes)); - songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes), difficulty); - songChartData.setScrollSpeed(songData.song.speed, difficulty); - - return songChartData; - } - - static function migrateSongNoteDataFromLegacy(sections:Array):Array - { - var songNotes:Array = []; - - for (section in sections) - { - // Skip empty sections. - if (section.sectionNotes.length == 0) continue; - - for (note in section.sectionNotes) - { - songNotes.push(new SongNoteData(note.time, note.getData(section.mustHitSection), note.length, note.kind)); - } - } - - return songNotes; - } - - static function migrateSongEventDataFromLegacy(sections:Array):Array - { - var songEvents:Array = []; - - var lastSectionWasMustHit:Null = null; - for (section in sections) - { - // Skip empty sections. - if (section.sectionNotes.length == 0) continue; - - if (section.mustHitSection != lastSectionWasMustHit) - { - lastSectionWasMustHit = section.mustHitSection; - - var firstNote:LegacyNote = section.sectionNotes[0]; - - songEvents.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1})); - } - } - - return songEvents; - } -} diff --git a/source/funkin/play/song/SongSerializer.hx b/source/funkin/play/song/SongSerializer.hx index a0a468c5b..10296e5b4 100644 --- a/source/funkin/play/song/SongSerializer.hx +++ b/source/funkin/play/song/SongSerializer.hx @@ -3,14 +3,14 @@ package funkin.play.song; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongMetadata; import funkin.util.SerializerUtil; +import funkin.util.FileUtil; import lime.utils.Bytes; import openfl.events.Event; import openfl.events.IOErrorEvent; import openfl.net.FileReference; /** - * Utilities for exporting a chart to a JSON file. - * Primarily used for the chart editor. + * TODO: Refactor and remove this. */ class SongSerializer { @@ -20,7 +20,7 @@ class SongSerializer */ public static function importSongChartDataSync(path:String):SongChartData { - var fileData = readFile(path); + var fileData = FileUtil.readStringFromPath(path); if (fileData == null) return null; @@ -35,7 +35,7 @@ class SongSerializer */ public static function importSongMetadataSync(path:String):SongMetadata { - var fileData = readFile(path); + var fileData = FileUtil.readStringFromPath(path); if (fileData == null) return null; @@ -50,7 +50,7 @@ class SongSerializer */ public static function importSongChartDataAsync(callback:SongChartData->Void):Void { - browseFileReference(function(fileReference:FileReference) { + FileUtil.browseFileReference(function(fileReference:FileReference) { var data = fileReference.data.toString(); if (data == null) return; @@ -67,7 +67,7 @@ class SongSerializer */ public static function importSongMetadataAsync(callback:SongMetadata->Void):Void { - browseFileReference(function(fileReference:FileReference) { + FileUtil.browseFileReference(function(fileReference:FileReference) { var data = fileReference.data.toString(); if (data == null) return; @@ -77,126 +77,4 @@ class SongSerializer if (songMetadata != null) callback(songMetadata); }); } - - /** - * Save a SongChartData object as a JSON file to an automatically generated path. - * Works great on HTML5 and desktop. - */ - public static function exportSongChartData(data:SongChartData, songId:String) - { - var path = '${songId}-chart.json'; - exportSongChartDataAs(path, data); - } - - /** - * Save a SongMetadata object as a JSON file to an automatically generated path. - * Works great on HTML5 and desktop. - */ - public static function exportSongMetadata(data:SongMetadata, songId:String) - { - var path = '${songId}-metadata.json'; - exportSongMetadataAs(path, data); - } - - /** - * Save a SongChartData object as a JSON file to a specified path. - * Works great on HTML5 and desktop. - * - * @param path The file path to save to. - */ - public static function exportSongChartDataAs(path:String, data:SongChartData) - { - var dataString = SerializerUtil.toJSON(data); - - writeFileReference(path, dataString); - } - - /** - * Save a SongMetadata object as a JSON file to a specified path. - * Works great on HTML5 and desktop. - * - * @param path The file path to save to. - */ - public static function exportSongMetadataAs(path:String, data:SongMetadata) - { - var dataString = SerializerUtil.toJSON(data); - - writeFileReference(path, dataString); - } - - /** - * Read the string contents of a file. - * Only works on desktop platforms. - * @param path The file path to read from. - */ - static function readFile(path:String):String - { - #if sys - var fileBytes:Bytes = sys.io.File.getBytes(path); - - if (fileBytes == null) return null; - - return fileBytes.toString(); - #end - - trace('ERROR: readFile not implemented for this platform'); - return null; - } - - /** - * Write string contents to a file. - * Only works on desktop platforms. - * @param path The file path to read from. - */ - static function writeFile(path:String, data:String):Void - { - #if sys - sys.io.File.saveContent(path, data); - return; - #end - trace('ERROR: writeFile not implemented for this platform'); - return; - } - - /** - * Browse for a file to read and execute a callback once we have a file reference. - * Works great on HTML5 or desktop. - * - * @param callback The function to call when the file is loaded. - */ - static function browseFileReference(callback:FileReference->Void) - { - var file = new FileReference(); - - file.addEventListener(Event.SELECT, function(e) { - var selectedFileRef:FileReference = e.target; - trace('Selected file: ' + selectedFileRef.name); - selectedFileRef.addEventListener(Event.COMPLETE, function(e) { - var loadedFileRef:FileReference = e.target; - trace('Loaded file: ' + loadedFileRef.name); - callback(loadedFileRef); - }); - selectedFileRef.load(); - }); - - file.browse(); - } - - /** - * Prompts the user to save a file to their computer. - */ - static function writeFileReference(path:String, data:String) - { - var file = new FileReference(); - file.addEventListener(Event.COMPLETE, function(e:Event) { - trace('Successfully wrote file.'); - }); - file.addEventListener(Event.CANCEL, function(e:Event) { - trace('Cancelled writing file.'); - }); - file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) { - trace('IO error writing file.'); - }); - file.save(data, path); - } } diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx deleted file mode 100644 index e33ddd87c..000000000 --- a/source/funkin/play/song/SongValidator.hx +++ /dev/null @@ -1,149 +0,0 @@ -package funkin.play.song; - -import funkin.data.song.SongRegistry; -import funkin.data.song.SongData.SongChartData; -import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongPlayData; -import funkin.data.song.SongData.SongTimeChange; -import funkin.data.song.SongData.SongTimeFormat; - -/** - * For SongMetadata and SongChartData objects, - * ensures mandatory fields are present and populates optional fields with default values. - */ -class SongValidator -{ - public static final DEFAULT_SONGNAME:String = "Unknown"; - public static final DEFAULT_ARTIST:String = "Unknown"; - public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS; - public static final DEFAULT_DIVISIONS:Null = null; - public static final DEFAULT_LOOPED:Bool = false; - public static final DEFAULT_STAGE:String = "mainStage"; - public static final DEFAULT_SCROLLSPEED:Float = 1.0; - - public static var DEFAULT_GENERATEDBY(get, never):String; - - static function get_DEFAULT_GENERATEDBY():String - { - return '${Constants.TITLE} - ${Constants.VERSION}'; - } - - /** - * Validates the fields of a SongMetadata object (excluding the version field). - * - * @param input The SongMetadata object to validate. - * @param songId The ID of the song being validated. Only used for error messages. - * @return The validated SongMetadata object. - */ - public static function validateSongMetadata(input:SongMetadata, songId:String = 'unknown'):SongMetadata - { - if (input == null) - { - trace('[SONGDATA] Could not parse metadata for song ${songId}'); - return null; - } - - if (input.songName == null) - { - trace('[SONGDATA] Song ${songId} is missing a songName field. '); - input.songName = DEFAULT_SONGNAME; - } - if (input.artist == null) - { - trace('[SONGDATA] Song ${songId} is missing an artist field. '); - input.artist = DEFAULT_ARTIST; - } - if (input.timeFormat == null) - { - trace('[SONGDATA] Song ${songId} is missing a timeFormat field. '); - input.timeFormat = DEFAULT_TIMEFORMAT; - } - if (input.generatedBy == null) - { - input.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; - } - - input.timeChanges = validateTimeChanges(input.timeChanges, songId); - if (input.timeChanges == null) - { - trace('[SONGDATA] Song ${songId} is missing a timeChanges field. '); - return null; - } - - input.playData = validatePlayData(input.playData, songId); - - if (input.variation == null) input.variation = ''; - - return input; - } - - /** - * Validates the fields of a SongPlayData object. - * - * @param input The SongPlayData object to validate. - * @param songId The ID of the song being validated. Only used for error messages. - * @return The validated SongPlayData object. - */ - public static function validatePlayData(input:SongPlayData, songId:String = 'unknown'):SongPlayData - { - if (input == null) - { - trace('[SONGDATA] Could not parse metadata.playData for song ${songId}'); - return null; - } - - return input; - } - - /** - * Validates the fields of a TimeChange object. - * - * @param input The TimeChange object to validate. - * @param songId The ID of the song being validated. Only used for error messages. - * @return The validated TimeChange object. - */ - public static function validateTimeChange(input:SongTimeChange, songId:String = 'unknown'):SongTimeChange - { - if (input == null) - { - trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}'); - return null; - } - - return input; - } - - /** - * Validates multiple TimeChange objects in an array. - */ - public static function validateTimeChanges(input:Array, songId:String = 'unknown'):Array - { - if (input == null) - { - trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}'); - return null; - } - - input = input.map((timeChange) -> validateTimeChange(timeChange, songId)); - - return input; - } - - /** - * Validates the fields of a SongChartData object (excluding the version field). - * - * @param input The SongChartData object to validate. - * @param songId The ID of the song being validated. Only used for error messages. - * @return The validated SongChartData object. - */ - public static function validateSongChartData(input:SongChartData, songId:String = 'unknown'):SongChartData - { - if (input == null) - { - trace('[SONGDATA] Could not parse chart data for song ${songId}'); - return null; - } - - return input; - } -} diff --git a/source/funkin/play/song/formats/FNFLegacy.hx b/source/funkin/play/song/formats/FNFLegacy.hx deleted file mode 100644 index a64e461bd..000000000 --- a/source/funkin/play/song/formats/FNFLegacy.hx +++ /dev/null @@ -1,131 +0,0 @@ -package funkin.play.song.formats; - -typedef FNFLegacy = -{ - var song:LegacySongData; -} - -typedef LegacySongData = -{ - var player1:String; // Boyfriend - var player2:String; // Opponent - - var speed:Float; - var stageDefault:String; - var bpm:Float; - var notes:Array; - var song:String; // Song name -}; - -typedef LegacyScrollSpeeds = -{ - var easy:Float; - var normal:Float; - var hard:Float; -}; - -typedef LegacyNoteData = -{ - /** - * The easy difficulty. - */ - var ?easy:Array; - - /** - * The normal difficulty. - */ - var ?normal:Array; - - /** - * The hard difficulty. - */ - var ?hard:Array; -}; - -typedef LegacyNoteSection = -{ - /** - * Whether the section is a must-hit section. - * If true, 0-3 are boyfriends notes, 4-7 are opponents notes. - * If false, 0-3 are opponents notes, 4-7 are boyfriends notes. - */ - var mustHitSection:Bool; - - /** - * Array of note data: - * - Direction - * - Time (ms) - * - Sustain Duration (ms) - * - Note kind (true = "alt", or string) - */ - var sectionNotes:Array; - - var typeOfSection:Int; - var lengthInSteps:Int; -} - -/** - * Notes in the old format are stored as an Array - */ -abstract LegacyNote(Array) -{ - public var time(get, set):Float; - - function get_time():Float - { - return this[0]; - } - - function set_time(value:Float):Float - { - return this[0] = value; - } - - public var data(get, set):Int; - - function get_data():Int - { - return this[1]; - } - - function set_data(value:Int):Int - { - return this[1] = value; - } - - public function getData(mustHitSection:Bool):Int - { - if (mustHitSection) return this[1]; - - return (this[1] + 4) % 8; - } - - public var length(get, set):Float; - - function get_length():Float - { - if (this.length < 3) return 0.0; - return this[2]; - } - - function set_length(value:Float):Float - { - return this[2] = value; - } - - public var kind(get, set):String; - - function get_kind():String - { - if (this.length < 4) return 'normal'; - - if (Std.isOfType(this[3], Bool)) return this[3] ? 'alt' : 'normal'; - - return this[3]; - } - - function set_kind(value:String):String - { - return this[3] = value; - } -} diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 1ac9b0b67..d9875e456 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -649,16 +649,20 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass } boppers = []; - for (sprite in this.group) + if (group != null) { - if (sprite != null) + for (sprite in this.group) { - sprite.kill(); - sprite.destroy(); - remove(sprite); + if (sprite != null) + { + sprite.kill(); + sprite.destroy(); + remove(sprite); + } } + group.clear(); } - group.clear(); + if (debugIconGroup != null && debugIconGroup.group != null) { debugIconGroup.kill(); diff --git a/source/funkin/shaderslmfao/BlendModesShader.hx b/source/funkin/shaderslmfao/BlendModesShader.hx new file mode 100644 index 000000000..6807a65c0 --- /dev/null +++ b/source/funkin/shaderslmfao/BlendModesShader.hx @@ -0,0 +1,23 @@ +package funkin.shaderslmfao; + +import flixel.addons.display.FlxRuntimeShader; +import funkin.Paths; +import openfl.utils.Assets; +import openfl.display.BitmapData; + +class BlendModesShader extends FlxRuntimeShader +{ + public var camera:BitmapData; + + public function new() + { + super(Assets.getText(Paths.frag('blendModes'))); + } + + public function setCamera(camera:BitmapData):Void + { + this.camera = camera; + + this.setBitmapData('camera', camera); + } +} diff --git a/source/funkin/shaderslmfao/GaussianBlurShader.hx b/source/funkin/shaderslmfao/GaussianBlurShader.hx new file mode 100644 index 000000000..ad472ac31 --- /dev/null +++ b/source/funkin/shaderslmfao/GaussianBlurShader.hx @@ -0,0 +1,25 @@ +package funkin.shaderslmfao; + +import flixel.addons.display.FlxRuntimeShader; +import funkin.Paths; +import openfl.utils.Assets; + +/** + * Note... not actually gaussian! + */ +class GaussianBlurShader extends FlxRuntimeShader +{ + public var amount:Float; + + public function new(amount:Float = 1.0) + { + super(Assets.getText(Paths.frag("gaussianBlur"))); + setAmount(amount); + } + + public function setAmount(value:Float):Void + { + this.amount = value; + this.setFloat("amount", amount); + } +} diff --git a/source/funkin/shaderslmfao/Grayscale.hx b/source/funkin/shaderslmfao/Grayscale.hx new file mode 100644 index 000000000..016d64b46 --- /dev/null +++ b/source/funkin/shaderslmfao/Grayscale.hx @@ -0,0 +1,22 @@ +package funkin.shaderslmfao; + +import flixel.addons.display.FlxRuntimeShader; +import funkin.Paths; +import openfl.utils.Assets; + +class Grayscale extends FlxRuntimeShader +{ + public var amount:Float = 1; + + public function new(amount:Float = 1) + { + super(Assets.getText(Paths.frag("grayscale"))); + setAmount(amount); + } + + public function setAmount(value:Float):Void + { + amount = value; + this.setFloat("amount", amount); + } +} diff --git a/source/funkin/shaderslmfao/HSVShader.hx b/source/funkin/shaderslmfao/HSVShader.hx new file mode 100644 index 000000000..066a49c96 --- /dev/null +++ b/source/funkin/shaderslmfao/HSVShader.hx @@ -0,0 +1,44 @@ +package funkin.shaderslmfao; + +import flixel.addons.display.FlxRuntimeShader; +import funkin.Paths; +import openfl.utils.Assets; + +class HSVShader extends FlxRuntimeShader +{ + public var hue(default, set):Float; + public var saturation(default, set):Float; + public var value(default, set):Float; + + public function new() + { + super(Assets.getText(Paths.frag('hsv'))); + hue = 1; + saturation = 1; + value = 1; + } + + function set_hue(value:Float):Float + { + this.setFloat('hue', value); + this.hue = value; + + return this.hue; + } + + function set_saturation(value:Float):Float + { + this.setFloat('sat', value); + this.saturation = value; + + return this.saturation; + } + + function set_value(value:Float):Float + { + this.setFloat('val', value); + this.value = value; + + return this.value; + } +} diff --git a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx new file mode 100644 index 000000000..e852dff0a --- /dev/null +++ b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx @@ -0,0 +1,170 @@ +package funkin.ui.debug.charting; + +import openfl.utils.Assets; +import flixel.system.FlxAssets.FlxSoundAsset; +import flixel.system.FlxSound; +import funkin.play.character.BaseCharacter.CharacterType; +import flixel.system.FlxSound; +import haxe.io.Path; + +/** + * Functions for loading audio for the chart editor. + */ +@:nullSafety +@:allow(funkin.ui.debug.charting.ChartEditorState) +@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler) +@:allow(funkin.ui.debug.charting.ChartEditorImportExportHandler) +class ChartEditorAudioHandler +{ + /** + * Loads a vocal track from an absolute file path. + * @param path The absolute path to the audio file. + * @param charKey The character to load the vocal track for. + * @return Success or failure. + */ + static function loadVocalsFromPath(state:ChartEditorState, path:Path, charKey:String = 'default'):Bool + { + #if sys + var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString()); + return loadVocalsFromBytes(state, fileBytes, charKey); + #else + trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way."); + return false; + #end + } + + /** + * Load a vocal track for a given song and character and add it to the voices group. + * + * @param path ID of the asset. + * @param charKey Character to load the vocal track for. + * @return Success or failure. + */ + static function loadVocalsFromAsset(state:ChartEditorState, path:String, charType:CharacterType = OTHER):Bool + { + var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false); + if (vocalTrack != null) + { + switch (charType) + { + case CharacterType.BF: + if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); + state.audioVocalTrackData.set(state.currentSongCharacterPlayer, Assets.getBytes(path)); + case CharacterType.DAD: + if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); + state.audioVocalTrackData.set(state.currentSongCharacterOpponent, Assets.getBytes(path)); + default: + if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack); + state.audioVocalTrackData.set('default', Assets.getBytes(path)); + } + + return true; + } + return false; + } + + /** + * Loads a vocal track from audio byte data. + */ + static function loadVocalsFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, charKey:String = ''):Bool + { + var openflSound:openfl.media.Sound = new openfl.media.Sound(); + openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); + var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false); + if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack); + state.audioVocalTrackData.set(charKey, bytes); + return true; + } + + /** + * Loads an instrumental from an absolute file path, replacing the current instrumental. + * + * @param path The absolute path to the audio file. + * + * @return Success or failure. + */ + static function loadInstrumentalFromPath(state:ChartEditorState, path:Path):Bool + { + #if sys + // Validate file extension. + if (path.ext != null && !ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + { + return false; + } + + var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString()); + return loadInstrumentalFromBytes(state, fileBytes, '${path.file}.${path.ext}'); + #else + trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way."); + return false; + #end + } + + /** + * Loads an instrumental from audio byte data, replacing the current instrumental. + * @param bytes The audio byte data. + * @param fileName The name of the file, if available. Used for notifications. + * @return Success or failure. + */ + static function loadInstrumentalFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, fileName:String = null):Bool + { + if (bytes == null) + { + return false; + } + + var openflSound:openfl.media.Sound = new openfl.media.Sound(); + openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); + state.audioInstTrack = FlxG.sound.load(openflSound, 1.0, false); + state.audioInstTrack.autoDestroy = false; + state.audioInstTrack.pause(); + + state.audioInstTrackData = bytes; + + state.postLoadInstrumental(); + + return true; + } + + /** + * Loads an instrumental from an OpenFL asset, replacing the current instrumental. + * @param path The path to the asset. Use `Paths` to build this. + * @return Success or failure. + */ + static function loadInstrumentalFromAsset(state:ChartEditorState, path:String):Bool + { + var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false); + if (instTrack != null) + { + state.audioInstTrack = instTrack; + + state.audioInstTrackData = Assets.getBytes(path); + + state.postLoadInstrumental(); + return true; + } + + return false; + } + + /** + * Play a sound effect. + * Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance. + */ + public static function playSound(path:String):Void + { + var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound(); + + var asset:Null = FlxG.sound.cache(path); + if (asset == null) + { + trace('WARN: Failed to play sound $path, asset not found.'); + return; + } + + snd.loadEmbedded(asset); + snd.autoDestroy = true; + FlxG.sound.list.add(snd); + snd.play(); + } +} diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index 79f58a098..c358c1d3d 100644 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx @@ -64,7 +64,7 @@ class AddNotesCommand implements ChartEditorCommand state.currentEventSelection = []; } - state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -78,7 +78,7 @@ class AddNotesCommand implements ChartEditorCommand state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentNoteSelection = []; state.currentEventSelection = []; - state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01')); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -114,7 +114,7 @@ class RemoveNotesCommand implements ChartEditorCommand state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentNoteSelection = []; state.currentEventSelection = []; - state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01')); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -131,7 +131,7 @@ class RemoveNotesCommand implements ChartEditorCommand } state.currentNoteSelection = notes; state.currentEventSelection = []; - state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -252,7 +252,7 @@ class AddEventsCommand implements ChartEditorCommand state.currentEventSelection = events; } - state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -296,7 +296,7 @@ class RemoveEventsCommand implements ChartEditorCommand { state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); state.currentEventSelection = []; - state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01')); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -312,7 +312,7 @@ class RemoveEventsCommand implements ChartEditorCommand state.currentSongChartEventData.push(event); } state.currentEventSelection = events; - state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -352,7 +352,7 @@ class RemoveItemsCommand implements ChartEditorCommand state.currentNoteSelection = []; state.currentEventSelection = []; - state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01')); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -376,7 +376,7 @@ class RemoveItemsCommand implements ChartEditorCommand state.currentNoteSelection = notes; state.currentEventSelection = events; - state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); + ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); state.saveDataDirty = true; state.noteDisplayDirty = true; diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index 6f44f89a2..736851d16 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -1,40 +1,45 @@ package funkin.ui.debug.charting; -import funkin.play.character.CharacterData; -import funkin.util.Constants; -import funkin.util.SerializerUtil; +import funkin.ui.haxeui.components.FunkinDropDown; +import flixel.util.FlxTimer; +import funkin.data.song.importer.FNFLegacyData; +import funkin.data.song.importer.FNFLegacyImporter; +import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongMetadata; -import flixel.util.FlxTimer; -import funkin.ui.haxeui.components.FunkinLink; -import funkin.util.SortUtil; +import funkin.data.song.SongData.SongTimeChange; +import funkin.data.song.SongRegistry; import funkin.input.Cursor; import funkin.play.character.BaseCharacter; +import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.song.Song; -import funkin.play.song.SongMigrator; -import funkin.play.song.SongValidator; -import funkin.data.song.SongRegistry; -import funkin.data.song.SongData.SongPlayableChar; -import funkin.data.song.SongData.SongTimeChange; +import funkin.play.stage.StageData; +import funkin.ui.haxeui.components.FunkinLink; +import funkin.util.Constants; import funkin.util.FileUtil; +import funkin.util.SerializerUtil; +import funkin.util.SortUtil; +import funkin.util.VersionUtil; import haxe.io.Path; import haxe.ui.components.Button; import haxe.ui.components.DropDown; import haxe.ui.components.Label; import haxe.ui.components.Link; import haxe.ui.components.NumberStepper; +import haxe.ui.components.Slider; import haxe.ui.components.TextField; import haxe.ui.containers.Box; import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.Dialog.DialogButton; import haxe.ui.containers.dialogs.Dialogs; -import haxe.ui.containers.properties.PropertyGrid; -import haxe.ui.containers.properties.PropertyGroup; +import haxe.ui.containers.Form; import haxe.ui.containers.VBox; import haxe.ui.core.Component; import haxe.ui.events.UIEvent; import haxe.ui.notifications.NotificationManager; import haxe.ui.notifications.NotificationType; +import thx.semver.Version; using Lambda; @@ -48,13 +53,14 @@ class ChartEditorDialogHandler static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome'); static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata'); - static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata-chargroup'); static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals'); static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry'); static final CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart'); static final CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-entry'); static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart'); static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide'); + static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation'); + static final CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-difficulty'); /** * Builds and opens a dialog giving brief credits for the chart editor. @@ -83,6 +89,7 @@ class ChartEditorDialogHandler linkCreateBasic.onClick = function(_event) { // Hide the welcome dialog dialog.hideDialog(DialogButton.CANCEL); + state.stopWelcomeMusic(); // // Create Song Wizard @@ -95,6 +102,7 @@ class ChartEditorDialogHandler linkImportChartLegacy.onClick = function(_event) { // Hide the welcome dialog dialog.hideDialog(DialogButton.CANCEL); + state.stopWelcomeMusic(); // Open the "Import Chart" dialog openImportChartWizard(state, 'legacy', false); @@ -105,6 +113,7 @@ class ChartEditorDialogHandler buttonBrowse.onClick = function(_event) { // Hide the welcome dialog dialog.hideDialog(DialogButton.CANCEL); + state.stopWelcomeMusic(); // Open the "Open Chart" dialog openBrowseWizard(state, false); @@ -133,14 +142,16 @@ class ChartEditorDialogHandler linkTemplateSong.text = songName; linkTemplateSong.onClick = function(_event) { dialog.hideDialog(DialogButton.CANCEL); + state.stopWelcomeMusic(); // Load song from template - state.loadSongAsTemplate(targetSongId); + ChartEditorImportExportHandler.loadSongAsTemplate(state, targetSongId); } splashTemplateContainer.addComponent(linkTemplateSong); } + state.fadeInWelcomeMusic(); return dialog; } @@ -298,7 +309,7 @@ class ChartEditorDialogHandler {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) { if (selectedFile != null && selectedFile.bytes != null) { - if (state.loadInstrumentalFromBytes(selectedFile.bytes)) + if (ChartEditorAudioHandler.loadInstrumentalFromBytes(state, selectedFile.bytes)) { trace('Selected file: ' + selectedFile.fullPath); #if !mac @@ -335,7 +346,7 @@ class ChartEditorDialogHandler onDropFile = function(pathStr:String) { var path:Path = new Path(pathStr); trace('Dropped file (${path})'); - if (state.loadInstrumentalFromPath(path)) + if (ChartEditorAudioHandler.loadInstrumentalFromPath(state, path)) { // Tell the user the load was successful. #if !mac @@ -457,62 +468,96 @@ class ChartEditorDialogHandler dialog.hideDialog(DialogButton.CANCEL); } - var dialogSongName:Null = dialog.findComponent('dialogSongName', TextField); - if (dialogSongName == null) throw 'Could not locate dialogSongName TextField in Song Metadata dialog'; - dialogSongName.onChange = function(event:UIEvent) { + var newSongMetadata:SongMetadata = new SongMetadata('', '', 'default'); + + var inputSongName:Null = dialog.findComponent('inputSongName', TextField); + if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog'; + inputSongName.onChange = function(event:UIEvent) { var valid:Bool = event.target.text != null && event.target.text != ''; if (valid) { - dialogSongName.removeClass('invalid-value'); - state.currentSongMetadata.songName = event.target.text; + inputSongName.removeClass('invalid-value'); + newSongMetadata.songName = event.target.text; } else { - state.currentSongMetadata.songName = ""; + newSongMetadata.songName = ""; } }; - state.currentSongMetadata.songName = ""; + inputSongName.text = ""; - var dialogSongArtist:Null = dialog.findComponent('dialogSongArtist', TextField); - if (dialogSongArtist == null) throw 'Could not locate dialogSongArtist TextField in Song Metadata dialog'; - dialogSongArtist.onChange = function(event:UIEvent) { + var inputSongArtist:Null = dialog.findComponent('inputSongArtist', TextField); + if (inputSongArtist == null) throw 'Could not locate inputSongArtist TextField in Song Metadata dialog'; + inputSongArtist.onChange = function(event:UIEvent) { var valid:Bool = event.target.text != null && event.target.text != ''; if (valid) { - dialogSongArtist.removeClass('invalid-value'); - state.currentSongMetadata.artist = event.target.text; + inputSongArtist.removeClass('invalid-value'); + newSongMetadata.artist = event.target.text; } else { - state.currentSongMetadata.artist = ""; + newSongMetadata.artist = ""; } }; - state.currentSongMetadata.artist = ""; + inputSongArtist.text = ""; - var dialogStage:Null = dialog.findComponent('dialogStage', DropDown); - if (dialogStage == null) throw 'Could not locate dialogStage DropDown in Song Metadata dialog'; - dialogStage.onChange = function(event:UIEvent) { + var inputStage:Null = dialog.findComponent('inputStage', DropDown); + if (inputStage == null) throw 'Could not locate inputStage DropDown in Song Metadata dialog'; + inputStage.onChange = function(event:UIEvent) { if (event.data == null && event.data.id == null) return; - state.currentSongMetadata.playData.stage = event.data.id; + newSongMetadata.playData.stage = event.data.id; }; - state.currentSongMetadata.playData.stage = 'mainStage'; + var startingValueStage = ChartEditorDropdowns.populateDropdownWithStages(inputStage, newSongMetadata.playData.stage); + inputStage.value = startingValueStage; - var dialogNoteSkin:Null = dialog.findComponent('dialogNoteSkin', DropDown); - if (dialogNoteSkin == null) throw 'Could not locate dialogNoteSkin DropDown in Song Metadata dialog'; - dialogNoteSkin.onChange = function(event:UIEvent) { + var inputNoteStyle:Null = dialog.findComponent('inputNoteStyle', FunkinDropDown); + if (inputNoteStyle == null) throw 'Could not locate inputNoteStyle DropDown in Song Metadata dialog'; + inputNoteStyle.onChange = function(event:UIEvent) { if (event.data.id == null) return; - state.currentSongNoteSkin = event.data.id; + newSongMetadata.playData.noteSkin = event.data.id; }; - state.currentSongNoteSkin = 'funkin'; + var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, newSongMetadata.playData.noteSkin); + inputNoteStyle.value = startingValueNoteStyle; + + var inputCharacterPlayer:Null = dialog.findComponent('inputCharacterPlayer', FunkinDropDown); + if (inputCharacterPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterPlayer component.'; + inputCharacterPlayer.onChange = function(event:UIEvent) { + if (event.data?.id == null) return; + newSongMetadata.playData.characters.player = event.data.id; + }; + var startingValuePlayer = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterPlayer, CharacterType.BF, + newSongMetadata.playData.characters.player); + inputCharacterPlayer.value = startingValuePlayer; + + var inputCharacterOpponent:Null = dialog.findComponent('inputCharacterOpponent', FunkinDropDown); + if (inputCharacterOpponent == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterOpponent component.'; + inputCharacterOpponent.onChange = function(event:UIEvent) { + if (event.data?.id == null) return; + newSongMetadata.playData.characters.opponent = event.data.id; + }; + var startingValueOpponent = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterOpponent, CharacterType.DAD, + newSongMetadata.playData.characters.opponent); + inputCharacterOpponent.value = startingValueOpponent; + + var inputCharacterGirlfriend:Null = dialog.findComponent('inputCharacterGirlfriend', FunkinDropDown); + if (inputCharacterGirlfriend == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterGirlfriend component.'; + inputCharacterGirlfriend.onChange = function(event:UIEvent) { + if (event.data?.id == null) return; + newSongMetadata.playData.characters.girlfriend = event.data.id == "none" ? "" : event.data.id; + }; + var startingValueGirlfriend = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterGirlfriend, CharacterType.GF, + newSongMetadata.playData.characters.girlfriend); + inputCharacterGirlfriend.value = startingValueGirlfriend; var dialogBPM:Null = dialog.findComponent('dialogBPM', NumberStepper); if (dialogBPM == null) throw 'Could not locate dialogBPM NumberStepper in Song Metadata dialog'; dialogBPM.onChange = function(event:UIEvent) { if (event.value == null || event.value <= 0) return; - var timeChanges:Array = state.currentSongMetadata.timeChanges; + var timeChanges:Array = newSongMetadata.timeChanges; if (timeChanges == null || timeChanges.length == 0) { timeChanges = [new SongTimeChange(0, event.value)]; @@ -524,24 +569,9 @@ class ChartEditorDialogHandler Conductor.forceBPM(event.value); - state.currentSongMetadata.timeChanges = timeChanges; + newSongMetadata.timeChanges = timeChanges; }; - var dialogCharGrid:Null = dialog.findComponent('dialogCharGrid', PropertyGrid); - if (dialogCharGrid == null) throw 'Could not locate dialogCharGrid PropertyGrid in Song Metadata dialog'; - var dialogCharAdd:Null