diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index e217d1f18..4d674f025 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -3,6 +3,10 @@ on: workflow_dispatch: push: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: create-nightly-html5: runs-on: [self-hosted, linux] diff --git a/.github/workflows/cancel-merged-branches.yml b/.github/workflows/cancel-merged-branches.yml new file mode 100644 index 000000000..84e3bedc9 --- /dev/null +++ b/.github/workflows/cancel-merged-branches.yml @@ -0,0 +1,35 @@ +name: cancel-merged-branches +on: + pull_request: + types: + - closed + +jobs: + cancel_stuff: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - uses: actions/github-script@v7 + id: cancel-runs + with: + result-encoding: string + retries: 3 + script: | + let branch_workflows = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: "build-shit.yml", + status: "queued", + branch: "${{ github.event.pull_request.head.ref }}" + }); + let runs = branch_workflows.data.workflow_runs; + runs.forEach((run) => { + github.rest.actions.cancelWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id + }); + }); + console.log(runs); diff --git a/Project.xml b/Project.xml index c0da3c89a..4b81fd07b 100644 --- a/Project.xml +++ b/Project.xml @@ -183,6 +183,7 @@ +
diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx index 996e2367e..94f41cea4 100644 --- a/source/funkin/Highscore.hx +++ b/source/funkin/Highscore.hx @@ -59,6 +59,7 @@ abstract Tallies(RawTallies) totalNotes: 0, totalNotesHit: 0, maxCombo: 0, + score: 0, isNewHighscore: false } } @@ -81,6 +82,9 @@ typedef RawTallies = var good:Int; var sick:Int; var maxCombo:Int; + + var score:Int; + var isNewHighscore:Bool; /** diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 0df290feb..3bea9cca2 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -402,10 +402,16 @@ class FunkinSound extends FlxSound implements ICloneable return sound; } + @:nullSafety(Off) public override function destroy():Void { // trace('[FunkinSound] Destroying sound "${this._label}"'); super.destroy(); + if (fadeTween != null) + { + fadeTween.cancel(); + fadeTween = null; + } FlxTween.cancelTweensOf(this); this._label = 'unknown'; } diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 91054cfb0..5037ee1d0 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -159,10 +159,18 @@ class VoicesGroup extends SoundGroup public override function destroy():Void { - playerVoices.destroy(); - playerVoices = null; - opponentVoices.destroy(); - opponentVoices = null; + if (playerVoices != null) + { + playerVoices.destroy(); + playerVoices = null; + } + + if (opponentVoices != null) + { + opponentVoices.destroy(); + opponentVoices = null; + } + super.destroy(); } } diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 7419d9425..118516bec 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -325,12 +325,3 @@ abstract class BaseRegistry & Constructible; } diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 4fdf5d0df..277dcd9e1 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -427,7 +427,7 @@ class SongRegistry extends BaseRegistry return ScriptedSong.listScriptClasses(); } - function loadEntryMetadataFile(id:String, ?variation:String):Null + function loadEntryMetadataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); @@ -442,7 +442,7 @@ class SongRegistry extends BaseRegistry return {fileName: entryFilePath, contents: rawJson}; } - function loadMusicDataFile(id:String, ?variation:String):Null + function loadMusicDataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json'); @@ -460,7 +460,7 @@ class SongRegistry extends BaseRegistry return openfl.Assets.exists(entryFilePath); } - function loadEntryChartFile(id:String, ?variation:String):Null + function loadEntryChartFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index a1796e912..652ba1484 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -238,11 +238,11 @@ class GameOverSubState extends MusicBeatSubState } else if (PlayStatePlaylist.isStoryMode) { - FlxG.switchState(() -> new StoryMenuState()); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker))); } else { - FlxG.switchState(() -> new FreeplayState()); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(sticker))); } } diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index f16aa00d8..9c2356bba 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -12,6 +12,7 @@ import flixel.tweens.FlxTween; import flixel.util.FlxColor; import funkin.audio.FunkinSound; import funkin.data.song.SongRegistry; +import funkin.ui.freeplay.FreeplayState; import funkin.graphics.FunkinSprite; import funkin.play.cutscene.VideoCutscene; import funkin.play.PlayState; @@ -230,7 +231,7 @@ class PauseSubState extends MusicBeatSubState */ function startPauseMusic():Void { - var pauseMusicPath:String = Paths.music('breakfast$musicSuffix'); + var pauseMusicPath:String = Paths.music('breakfast$musicSuffix/breakfast$musicSuffix'); pauseMusic = FunkinSound.load(pauseMusicPath, true, true); if (pauseMusic == null) @@ -567,6 +568,8 @@ class PauseSubState extends MusicBeatSubState PlayStatePlaylist.campaignDifficulty = difficulty; PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty; + FreeplayState.rememberedDifficulty = difficulty; + PlayState.instance.needsReset = true; state.close(); @@ -658,7 +661,7 @@ class PauseSubState extends MusicBeatSubState } else { - state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker))); + state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker))); } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index fd6463bb1..ebc25bf7d 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -728,6 +728,10 @@ class PlayState extends MusicBeatSubState #end initialized = true; + + // This step ensures z-indexes are applied properly, + // and it's important to call it last so all elements get affected. + refresh(); } public override function draw():Void @@ -1720,8 +1724,6 @@ class PlayState extends MusicBeatSubState playerStrumline.fadeInArrows(); opponentStrumline.fadeInArrows(); } - - this.refresh(); } /** @@ -2441,7 +2443,8 @@ class PlayState extends MusicBeatSubState if (Highscore.tallies.combo != 0) { // Break the combo. - Highscore.tallies.combo = comboPopUps.displayCombo(0); + if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); + Highscore.tallies.combo = 0; } if (playSound) @@ -2568,32 +2571,38 @@ class PlayState extends MusicBeatSubState */ function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void { - vocals.playerVolume = 1; - if (daRating == 'miss') { // If daRating is 'miss', that means we made a mistake and should not continue. - trace('[WARNING] popUpScore judged a note as a miss!'); + FlxG.log.warn('popUpScore judged a note as a miss!'); // TODO: Remove this. comboPopUps.displayRating('miss'); return; } + vocals.playerVolume = 1; + var isComboBreak = false; switch (daRating) { case 'sick': Highscore.tallies.sick += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; case 'good': Highscore.tallies.good += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; case 'bad': Highscore.tallies.bad += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; case 'shit': Highscore.tallies.shit += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; + default: + FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!'); } health += healthChange; @@ -2601,18 +2610,18 @@ class PlayState extends MusicBeatSubState if (isComboBreak) { // Break the combo, but don't increment tallies.misses. - Highscore.tallies.combo = comboPopUps.displayCombo(0); + if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); + Highscore.tallies.combo = 0; } else { Highscore.tallies.combo++; - Highscore.tallies.totalNotesHit++; if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; } playerStrumline.hitNote(daNote, !isComboBreak); - if (daRating == "sick") + if (daRating == 'sick') { playerStrumline.playNoteSplash(daNote.noteData.getDirection()); } @@ -2724,7 +2733,7 @@ class PlayState extends MusicBeatSubState */ public function endSong(rightGoddamnNow:Bool = false):Void { - FlxG.sound.music.volume = 0; + if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; vocals.volume = 0; mayPauseGame = false; @@ -2742,6 +2751,8 @@ class PlayState extends MusicBeatSubState deathCounter = 0; + var isNewHighscore = false; + if (currentSong != null && currentSong.validScore) { // crackhead double thingie, sets whether was new highscore, AND saves the song! @@ -2772,11 +2783,14 @@ class PlayState extends MusicBeatSubState #if newgrounds NGio.postScore(score, currentSong.id); #end + isNewHighscore = true; } } if (PlayStatePlaylist.isStoryMode) { + isNewHighscore = false; + PlayStatePlaylist.campaignScore += songScore; // Pop the next song ID from the list. @@ -2785,18 +2799,6 @@ class PlayState extends MusicBeatSubState if (targetSongId == null) { - FunkinSound.playMusic('freakyMenu', - { - overrideExisting: true, - restartTrack: false - }); - - // transIn = FlxTransitionableState.defaultTransIn; - // transOut = FlxTransitionableState.defaultTransOut; - - // TODO: Rework week unlock logic. - // StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true; - if (currentSong.validScore) { NGio.unlockMedal(60961); @@ -2826,6 +2828,7 @@ class PlayState extends MusicBeatSubState #if newgrounds NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}'); #end + isNewHighscore = true; } } @@ -2837,11 +2840,11 @@ class PlayState extends MusicBeatSubState { if (rightGoddamnNow) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } else { - zoomIntoResultsScreen(); + zoomIntoResultsScreen(isNewHighscore); } } } @@ -2902,11 +2905,11 @@ class PlayState extends MusicBeatSubState { if (rightGoddamnNow) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } else { - zoomIntoResultsScreen(); + zoomIntoResultsScreen(isNewHighscore); } } } @@ -2980,7 +2983,7 @@ class PlayState extends MusicBeatSubState /** * Play the camera zoom animation and then move to the results screen once it's done. */ - function zoomIntoResultsScreen():Void + function zoomIntoResultsScreen(isNewHighscore:Bool):Void { trace('WENT TO RESULTS SCREEN!'); @@ -3037,7 +3040,7 @@ class PlayState extends MusicBeatSubState { ease: FlxEase.expoIn, onComplete: function(_) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } }); }); @@ -3046,7 +3049,7 @@ class PlayState extends MusicBeatSubState /** * Move to the results screen right goddamn now. */ - function moveToResultsScreen():Void + function moveToResultsScreen(isNewHighscore:Bool):Void { persistentUpdate = false; vocals.stop(); @@ -3058,7 +3061,24 @@ class PlayState extends MusicBeatSubState { storyMode: PlayStatePlaylist.isStoryMode, title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), - tallies: talliesToUse, + scoreData: + { + score: songScore, + tallies: + { + sick: Highscore.tallies.sick, + good: Highscore.tallies.good, + bad: Highscore.tallies.bad, + shit: Highscore.tallies.shit, + missed: Highscore.tallies.missed, + combo: Highscore.tallies.combo, + maxCombo: Highscore.tallies.maxCombo, + totalNotesHit: Highscore.tallies.totalNotesHit, + totalNotes: Highscore.tallies.totalNotes, + }, + accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, + }, + isNewHighscore: isNewHighscore }); res.camera = camHUD; openSubState(res); @@ -3205,7 +3225,10 @@ class PlayState extends MusicBeatSubState // Don't go back in time to before the song started. targetTimeMs = Math.max(0, targetTimeMs); - FlxG.sound.music.time = targetTimeMs; + if (FlxG.sound.music != null) + { + FlxG.sound.music.time = targetTimeMs; + } handleSkippedNotes(); SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition); diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 821f4ba3c..3ae8ad138 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -1,5 +1,6 @@ package funkin.play; +import funkin.util.MathUtil; import funkin.ui.story.StoryMenuState; import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.FlxSprite; @@ -16,6 +17,8 @@ import flixel.tweens.FlxTween; import funkin.audio.FunkinSound; import flixel.util.FlxGradient; import flixel.util.FlxTimer; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; import funkin.graphics.shaders.LeftMaskShader; import funkin.play.components.TallyCounter; @@ -42,12 +45,15 @@ class ResultState extends MusicBeatSubState override function create():Void { - if (params.tallies.sick == params.tallies.totalNotesHit - && params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT; - else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50) - resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! - else - resultsVariation = NORMAL; + /* + if (params.scoreData.sick == params.scoreData.totalNotesHit + && params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT; + else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50) + resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! + else + resultsVariation = NORMAL; + */ + resultsVariation = NORMAL; FunkinSound.playMusic('results$resultsVariation', { @@ -130,12 +136,16 @@ class ResultState extends MusicBeatSubState var diffSpr:String = switch (PlayState.instance.currentDifficulty) { - case 'EASY': + case 'easy': 'difEasy'; - case 'NORMAL': + case 'normal': 'difNormal'; - case 'HARD': + case 'hard': 'difHard'; + case 'erect': + 'difErect'; + case 'nightmare': + 'difNightmare'; case _: 'difNormal'; } @@ -195,29 +205,33 @@ class ResultState extends MusicBeatSubState * NOTE: We display how many notes were HIT, not how many notes there were in total. * */ - var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit); + var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.scoreData.tallies.totalNotesHit); ratingGrp.add(totalHit); - var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.tallies.maxCombo); + var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.scoreData.tallies.maxCombo); ratingGrp.add(maxCombo); hStuf += 2; var extraYOffset:Float = 5; - var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.tallies.sick, 0xFF89E59E); + var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E); ratingGrp.add(tallySick); - var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5); + var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.scoreData.tallies.good, 0xFF89C9E5); ratingGrp.add(tallyGood); - var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xFFE6CF8A); + var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.scoreData.tallies.bad, 0xFFE6CF8A); ratingGrp.add(tallyBad); - var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A); + var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.scoreData.tallies.shit, 0xFFE68C8A); ratingGrp.add(tallyShit); - var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.tallies.missed, 0xFFC68AE6); + var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6); ratingGrp.add(tallyMissed); + var score:TallyCounter = new TallyCounter(825, 630, params.scoreData.score, RIGHT); + score.scale.set(2, 2); + ratingGrp.add(score); + for (ind => rating in ratingGrp.members) { rating.visible = false; @@ -235,9 +249,16 @@ class ResultState extends MusicBeatSubState scorePopin.animation.play("score"); scorePopin.visible = true; - highscoreNew.visible = true; - highscoreNew.animation.play("new"); - FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); + if (params.isNewHighscore) + { + highscoreNew.visible = true; + highscoreNew.animation.play("new"); + FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); + } + else + { + highscoreNew.visible = false; + } }; switch (resultsVariation) @@ -276,8 +297,6 @@ class ResultState extends MusicBeatSubState } }); - if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!"); - super.create(); } @@ -365,7 +384,7 @@ class ResultState extends MusicBeatSubState } else { - openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new FreeplayState(null, sticker))); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker))); } } @@ -393,8 +412,13 @@ typedef ResultsStateParams = */ var title:String; + /** + * Whether the displayed score is a new highscore + */ + var isNewHighscore:Bool; + /** * The score, accuracy, and judgements. */ - var tallies:Highscore.Tallies; + var scoreData:SaveScoreData; }; diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx index 39fc192a0..724bf0cb9 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -85,7 +85,7 @@ class PopUpStuff extends FlxTypedGroup comboSpr.velocity.y -= 150; comboSpr.velocity.x += FlxG.random.int(1, 10); - add(comboSpr); + // add(comboSpr); if (PlayState.instance.currentStageId.startsWith('school')) { diff --git a/source/funkin/play/components/TallyCounter.hx b/source/funkin/play/components/TallyCounter.hx index 77e6ef4ec..35a8f3f51 100644 --- a/source/funkin/play/components/TallyCounter.hx +++ b/source/funkin/play/components/TallyCounter.hx @@ -6,6 +6,8 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.math.FlxMath; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; +import flixel.text.FlxText.FlxTextAlign; +import funkin.util.MathUtil; /** * Numerical counters used next to each judgement in the Results screen. @@ -13,18 +15,23 @@ import flixel.tweens.FlxTween; class TallyCounter extends FlxTypedSpriteGroup { public var curNumber:Float = 0; - public var neededNumber:Int = 0; + public var flavour:Int = 0xFFFFFFFF; - public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF) + public var align:FlxTextAlign = FlxTextAlign.LEFT; + + public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF, align:FlxTextAlign = FlxTextAlign.LEFT) { super(x, y); + this.align = align; + this.flavour = flavour; this.neededNumber = neededNumber; - drawNumbers(); + + if (curNumber == neededNumber) drawNumbers(); } var tmr:Float = 0; @@ -41,6 +48,8 @@ class TallyCounter extends FlxTypedSpriteGroup var seperatedScore:Array = []; var tempCombo:Int = Math.round(curNumber); + var fullNumberDigits:Int = Std.int(Math.max(1, Math.ceil(MathUtil.logBase(10, neededNumber)))); + while (tempCombo != 0) { seperatedScore.push(tempCombo % 10); @@ -55,7 +64,13 @@ class TallyCounter extends FlxTypedSpriteGroup { if (ind >= members.length) { - var numb:TallyNumber = new TallyNumber(ind * 43, 0, num); + var xPos = ind * (43 * this.scale.x); + if (this.align == FlxTextAlign.RIGHT) + { + xPos -= (fullNumberDigits * (43 * this.scale.x)); + } + var numb:TallyNumber = new TallyNumber(xPos, 0, num); + numb.scale.set(this.scale.x, this.scale.y); add(numb); numb.color = flavour; } diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 9a6699c43..2b10c05ee 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -295,6 +295,11 @@ class Strumline extends FlxSpriteGroup { if (noteData.length == 0) return; + // Ensure note data gets reset if the song happens to loop. + // NOTE: I had to remove this line because it was causing notes visible during the countdown to be placed multiple times. + // I don't remember what bug I was trying to fix by adding this. + // if (conductorInUse.currentStep == 0) nextNoteIndex = 0; + var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0; var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS; var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS; @@ -822,7 +827,7 @@ class Strumline extends FlxSpriteGroup { // The note sprite pool is full and all note splashes are active. // We have to create a new note. - result = new SustainTrail(0, 100, noteStyle); + result = new SustainTrail(0, 0, noteStyle); this.holdNotes.add(result); } diff --git a/source/funkin/ui/credits/CreditsData.hx b/source/funkin/ui/credits/CreditsData.hx new file mode 100644 index 000000000..bf7f13ad5 --- /dev/null +++ b/source/funkin/ui/credits/CreditsData.hx @@ -0,0 +1,34 @@ +package funkin.ui.credits; + +/** + * The members of the Funkin' Crew, organized by their roles. + */ +typedef CreditsData = +{ + var entries:Array; +} + +/** + * The members of a specific role on the Funkin' Crew. + */ +typedef CreditsDataRole = +{ + @:optional + var header:String; + + @:optional + @:default([]) + var body:Array; + + @:optional + @:default(false) + var appendBackers:Bool; +} + +/** + * A member of a specific person on the Funkin' Crew. + */ +typedef CreditsDataMember = +{ + var line:String; +} diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx new file mode 100644 index 000000000..f2722ffbf --- /dev/null +++ b/source/funkin/ui/credits/CreditsDataHandler.hx @@ -0,0 +1,134 @@ +package funkin.ui.credits; + +import funkin.data.JsonFile; + +using StringTools; + +@:nullSafety +class CreditsDataHandler +{ + public static final BACKER_PUBLIC_URL:String = 'https://funkin.me/backers'; + + #if HARDCODED_CREDITS + static final CREDITS_DATA_PATH:String = "assets/exclude/data/credits.json"; + #else + static final CREDITS_DATA_PATH:String = "assets/data/credits.json"; + #end + + public static function debugPrint(data:Null):Void + { + if (data == null) + { + trace('CreditsData(NULL)'); + + return; + } + + var entryCount = data.entries.length; + var lineCount = 0; + for (entry in data.entries) + { + lineCount += entry?.body?.length ?? 0; + } + + trace('CreditsData($entryCount entries containing $lineCount lines)'); + } + + /** + * If for some reason the full credits won't load, + * use this hardcoded data for the original Funkin' Crew. + * + * @return `CreditsData` + */ + public static inline function getFallback():CreditsData + { + return { + entries: [ + { + header: 'Founders', + body: [ + {line: 'ninjamuffin99'}, + {line: 'PhantomArcade'}, + {line: 'KawaiSprite'}, + {line: 'evilsk8r'}, + ] + }, + { + header: 'Kickstarter Backers', + appendBackers: true + } + ] + }; + } + + public static function fetchBackerEntries():Array + { + // TODO: Replace this with a web request. + // We can't just grab the current Kickstarter data and include it in builds, + // because we don't want to deadname people who haven't logged into the portal yet. + // It can be async and paginated for performance! + return ['See the list of backers at $BACKER_PUBLIC_URL.']; + } + + #if HARDCODED_CREDITS + /** + * The data for the credits. + * Hardcoded into game via a macro at compile time. + */ + public static final CREDITS_DATA:Null = #if macro null #else CreditsDataMacro.loadCreditsData() #end; + #else + + /** + * The data for the credits. + * Loaded dynamically from the game folder when needed. + * Nullable because data may fail to parse. + */ + public static var CREDITS_DATA(get, default):Null = null; + + static function get_CREDITS_DATA():Null + { + if (CREDITS_DATA == null) CREDITS_DATA = parseCreditsData(fetchCreditsData()); + + return CREDITS_DATA; + } + + static function fetchCreditsData():funkin.data.JsonFile + { + var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim(); + + return { + fileName: CREDITS_DATA_PATH, + contents: rawJson + }; + } + + static function parseCreditsData(file:JsonFile):Null + { + #if !macro + if (file.contents == null) return null; + + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + trace('[CREDITS] Parsing credits data from ${CREDITS_DATA_PATH}'); + parser.fromJson(file.contents, file.fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, file.fileName); + return null; + } + return parser.value; + #else + return null; + #end + } + + static function printErrors(errors:Array, id:String = ''):Void + { + trace('[CREDITS] Failed to parse credits data: ${id}'); + + for (error in errors) + funkin.data.DataError.printError(error); + } + #end +} diff --git a/source/funkin/ui/credits/CreditsDataMacro.hx b/source/funkin/ui/credits/CreditsDataMacro.hx new file mode 100644 index 000000000..c97770eef --- /dev/null +++ b/source/funkin/ui/credits/CreditsDataMacro.hx @@ -0,0 +1,67 @@ +package funkin.ui.credits; + +#if macro +import haxe.macro.Context; +#end + +@:access(funkin.ui.credits.CreditsDataHandler) +class CreditsDataMacro +{ + public static macro function loadCreditsData():haxe.macro.Expr.ExprOf + { + #if !display + trace('Hardcoding credits data...'); + var json = CreditsDataMacro.fetchJSON(); + + if (json == null) + { + Context.info('[WARN] Could not fetch JSON data for credits.', Context.currentPos()); + return macro $v{CreditsDataHandler.getFallback()}; + } + + var creditsData = CreditsDataMacro.parseJSON(json); + + if (creditsData == null) + { + Context.info('[WARN] Could not parse JSON data for credits.', Context.currentPos()); + return macro $v{CreditsDataHandler.getFallback()}; + } + + CreditsDataHandler.debugPrint(creditsData); + return macro $v{creditsData}; + // return macro $v{null}; + #else + // `#if display` is used for code completion. In this case we return + // a minimal value to keep code completion fast. + return macro $v{CreditsDataHandler.getFallback()}; + #end + } + + #if macro + static function fetchJSON():Null + { + return sys.io.File.getContent(CreditsDataHandler.CREDITS_DATA_PATH); + } + + /** + * Parse the JSON data for the credits. + * + * @param json The string data to parse. + * @return The parsed data. + */ + static function parseJSON(json:String):Null + { + try + { + // TODO: Use something with better validation but that still works at macro time. + return haxe.Json.parse(json); + } + catch (e) + { + trace('[ERROR] Failed to parse JSON data for credits.'); + trace(e); + return null; + } + } + #end +} diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx new file mode 100644 index 000000000..d43e25114 --- /dev/null +++ b/source/funkin/ui/credits/CreditsState.hx @@ -0,0 +1,213 @@ +package funkin.ui.credits; + +import flixel.text.FlxText; +import flixel.util.FlxColor; +import funkin.audio.FunkinSound; +import flixel.FlxSprite; +import flixel.group.FlxSpriteGroup; + +/** + * The state used to display the credits scroll. + * AAA studios often fail to credit properly, and we're better than them! + */ +class CreditsState extends MusicBeatState +{ + /** + * The height the credits should start at. + * Make this an instanced variable so it gets set by the constructor. + */ + final STARTING_HEIGHT = FlxG.height; + + /** + * The padding on each side of the screen. + */ + static final SCREEN_PAD = 24; + + /** + * The width of the screen the credits should maximally fill up. + * Make this an instanced variable so it gets set by the constructor. + */ + final FULL_WIDTH = FlxG.width - (SCREEN_PAD * 2); + + /** + * The font to use to display the text. + * To use a font from the `assets` folder, use `Paths.font(...)`. + * Choose something that will render Unicode properly. + */ + static final CREDITS_FONT = 'Arial'; + + /** + * The size of the font. + */ + static final CREDITS_FONT_SIZE = 48; + + static final CREDITS_HEADER_FONT_SIZE = 72; + + /** + * The color of the text itself. + */ + static final CREDITS_FONT_COLOR = FlxColor.WHITE; + + /** + * The color of the text's outline. + */ + static final CREDITS_FONT_STROKE_COLOR = FlxColor.BLACK; + + /** + * The speed the credits scroll at, in pixels per second. + */ + static final CREDITS_SCROLL_BASE_SPEED = 25.0; + + /** + * The speed the credits scroll at while the button is held, in pixels per second. + */ + static final CREDITS_SCROLL_FAST_SPEED = CREDITS_SCROLL_BASE_SPEED * 4.0; + + /** + * The actual sprites and text used to display the credits. + */ + var creditsGroup:FlxSpriteGroup; + + var scrollPaused:Bool = false; + + public function new() + { + super(); + } + + public override function create():Void + { + super.create(); + + // Background + var bg = new FlxSprite(Paths.image('menuDesat')); + bg.scrollFactor.x = 0; + bg.scrollFactor.y = 0; + bg.setGraphicSize(Std.int(FlxG.width)); + bg.updateHitbox(); + bg.x = 0; + bg.y = 0; + bg.visible = true; + bg.color = 0xFFB57EDC; // Lavender + add(bg); + + // TODO: Once we need to display Kickstarter backers, + // make this use a recycled pool so we don't kill peformance. + creditsGroup = new FlxSpriteGroup(); + creditsGroup.x = SCREEN_PAD; + creditsGroup.y = STARTING_HEIGHT; + + buildCreditsGroup(); + + add(creditsGroup); + + // Music + FunkinSound.playMusic('freeplayRandom', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: true, + loop: true + }); + FlxG.sound.music.fadeIn(2, 0, 0.8); + } + + function buildCreditsGroup():Void + { + var y = 0; + + for (entry in CreditsDataHandler.CREDITS_DATA.entries) + { + if (entry.header != null) + { + creditsGroup.add(buildCreditsLine(entry.header, y, true, CreditsSide.Center)); + y += CREDITS_HEADER_FONT_SIZE; + } + + for (line in entry?.body ?? []) + { + creditsGroup.add(buildCreditsLine(line.line, y, false, CreditsSide.Center)); + y += CREDITS_FONT_SIZE; + } + + if (entry.appendBackers) + { + var backers = CreditsDataHandler.fetchBackerEntries(); + for (backer in backers) + { + creditsGroup.add(buildCreditsLine(backer, y, false, CreditsSide.Center)); + y += CREDITS_FONT_SIZE; + } + } + + // Padding between each role. + y += CREDITS_FONT_SIZE * 2; + } + } + + function buildCreditsLine(text:String, yPos:Float, header:Bool, side:CreditsSide = CreditsSide.Center):FlxText + { + // CreditsSide.Center: Full screen width + // CreditsSide.Left: Left half of screen + // CreditsSide.Right: Right half of screen + var xPos = (side == CreditsSide.Right) ? (FULL_WIDTH / 2) : 0; + var width = (side == CreditsSide.Center) ? FULL_WIDTH : (FULL_WIDTH / 2); + var size = header ? CREDITS_HEADER_FONT_SIZE : CREDITS_FONT_SIZE; + + var creditsLine:FlxText = new FlxText(xPos, yPos, width, text); + creditsLine.setFormat(CREDITS_FONT, size, CREDITS_FONT_COLOR, FlxTextAlign.CENTER, FlxTextBorderStyle.OUTLINE, CREDITS_FONT_STROKE_COLOR, true); + + return creditsLine; + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (!scrollPaused) + { + // TODO: Replace with whatever the special note button is. + if (controls.ACCEPT || FlxG.keys.pressed.SPACE) + { + // Move the whole group. + creditsGroup.y -= CREDITS_SCROLL_FAST_SPEED * elapsed; + } + else + { + // Move the whole group. + creditsGroup.y -= CREDITS_SCROLL_BASE_SPEED * elapsed; + } + } + + if (controls.BACK || hasEnded()) + { + exit(); + } + else if (controls.PAUSE) + { + scrollPaused = !scrollPaused; + } + } + + function hasEnded():Bool + { + return creditsGroup.y < -creditsGroup.height; + } + + function exit():Void + { + FlxG.switchState(new funkin.ui.mainmenu.MainMenuState()); + } + + public override function destroy():Void + { + super.destroy(); + } +} + +enum CreditsSide +{ + Left; + Center; + Right; +} diff --git a/source/funkin/ui/freeplay/Album.hx b/source/funkin/ui/freeplay/Album.hx index 7291c7357..3060d3eb8 100644 --- a/source/funkin/ui/freeplay/Album.hx +++ b/source/funkin/ui/freeplay/Album.hx @@ -1,6 +1,7 @@ package funkin.ui.freeplay; import funkin.data.freeplay.AlbumData; +import funkin.data.animation.AnimationData; import funkin.data.freeplay.AlbumRegistry; import funkin.data.IRegistryEntry; import flixel.graphics.FlxGraphic; @@ -75,6 +76,16 @@ class Album implements IRegistryEntry return _data.albumTitleAsset; } + public function hasAlbumTitleAnimations() + { + return _data.albumTitleAnimations.length > 0; + } + + public function getAlbumTitleAnimations():Array + { + return _data.albumTitleAnimations; + } + public function toString():String { return 'Album($id)'; diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index a1e63c9a1..bde946e79 100644 --- a/source/funkin/ui/freeplay/AlbumRoll.hx +++ b/source/funkin/ui/freeplay/AlbumRoll.hx @@ -7,6 +7,7 @@ import flixel.tweens.FlxTween; import flixel.util.FlxTimer; import flixel.tweens.FlxEase; import funkin.data.freeplay.AlbumRegistry; +import funkin.util.assets.FlxAnimationUtil; import funkin.graphics.FunkinSprite; import funkin.util.SortUtil; import openfl.utils.Assets; @@ -21,9 +22,9 @@ class AlbumRoll extends FlxSpriteGroup * The ID of the album to display. * Modify this value to automatically update the album art and title. */ - public var albumId(default, set):String; + public var albumId(default, set):Null; - function set_albumId(value:String):String + function set_albumId(value:Null):Null { if (this.albumId != value) { @@ -65,6 +66,17 @@ class AlbumRoll extends FlxSpriteGroup */ function updateAlbum():Void { + if (albumId == null) + { + albumArt.visible = false; + albumTitle.visible = false; + if (titleTimer != null) + { + titleTimer.cancel(); + titleTimer = null; + } + } + albumData = AlbumRegistry.instance.fetchEntry(albumId); if (albumData == null) @@ -94,7 +106,15 @@ class AlbumRoll extends FlxSpriteGroup if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey()))) { - albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey())); + if (albumData.hasAlbumTitleAnimations()) + { + albumTitle.loadSparrow(albumData.getAlbumTitleAssetKey()); + FlxAnimationUtil.addAtlasAnimations(albumTitle, albumData.getAlbumTitleAnimations()); + } + else + { + albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey())); + } } else { @@ -155,6 +175,8 @@ class AlbumRoll extends FlxSpriteGroup }); } + var titleTimer:Null = null; + /** * Play the intro animation on the album art. */ @@ -164,7 +186,14 @@ class AlbumRoll extends FlxSpriteGroup FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut}); albumTitle.visible = false; - new FlxTimer().start(0.75, function(_) { + + if (titleTimer != null) + { + titleTimer.cancel(); + titleTimer = null; + } + + titleTimer = new FlxTimer().start(0.75, function(_) { showTitle(); }); } @@ -179,6 +208,8 @@ class AlbumRoll extends FlxSpriteGroup public function showTitle():Void { albumTitle.visible = true; + albumTitle.animation.play('active'); + albumTitle.animation.finishCallback = (_) -> albumTitle.animation.play('idle'); } /** diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx index 33f264301..5f1144fab 100644 --- a/source/funkin/ui/freeplay/DJBoyfriend.hx +++ b/source/funkin/ui/freeplay/DJBoyfriend.hx @@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite var gotSpooked:Bool = false; - static final SPOOK_PERIOD:Float = 10.0; - static final TV_PERIOD:Float = 10.0; + static final SPOOK_PERIOD:Float = 120.0; + static final TV_PERIOD:Float = 180.0; // Time since dad last SPOOKED you. var timeSinceSpook:Float = 0; @@ -43,7 +43,14 @@ class DJBoyfriend extends FlxAtlasSprite switch (name) { case "Boyfriend DJ watchin tv OG": - if (number == 85) runTvLogic(); + if (number == 80) + { + FunkinSound.playOnce(Paths.sound('remote_click')); + } + if (number == 85) + { + runTvLogic(); + } default: } }; @@ -219,19 +226,17 @@ class DJBoyfriend extends FlxAtlasSprite if (cartoonSnd == null) { // tv is OFF, but getting turned on - // Eric got FUCKING TROLLED there is no `tv_on` or `channel_switch` sound! - // FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { - // }); - loadCartoon(); + FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { + loadCartoon(); + }); } else { // plays it smidge after the click - // new FlxTimer().start(0.1, function(_) { - // // FunkinSound.playOnce(Paths.sound('channel_switch')); - // }); - cartoonSnd.destroy(); - loadCartoon(); + FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() { + cartoonSnd.destroy(); + loadCartoon(); + }); } // loadCartoon(); diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 6cb0d1d9a..10650a46c 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -133,8 +133,8 @@ class FreeplayState extends MusicBeatSubState var stickerSubState:StickerSubState; - static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; - static var rememberedSongId:Null = null; + public static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; + public static var rememberedSongId:Null = null; public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { @@ -145,7 +145,7 @@ class FreeplayState extends MusicBeatSubState stickerSubState = stickers; } - super(); + super(FlxColor.TRANSPARENT); } override function create():Void @@ -380,7 +380,7 @@ class FreeplayState extends MusicBeatSubState } albumRoll = new AlbumRoll(); - albumRoll.albumId = 'volume1'; + albumRoll.albumId = null; add(albumRoll); albumRoll.applyExitMovers(exitMovers); @@ -536,21 +536,18 @@ class FreeplayState extends MusicBeatSubState }); } + var currentFilter:SongFilter = null; + var currentFilteredSongs:Array = []; + /** * Given the current filter, rebuild the current song list. * * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite) * @param force + * @param onlyIfChanged Only apply the filter if the song list has changed */ - public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void + public function generateSongList(filterStuff:Null, force:Bool = false, onlyIfChanged:Bool = true):Void { - curSelected = 1; - - for (cap in grpCapsules.members) - { - cap.kill(); - } - var tempSongs:Array = songs; if (filterStuff != null) @@ -582,6 +579,35 @@ class FreeplayState extends MusicBeatSubState } } + // Filter further by current selected difficulty. + if (currentDifficulty != null) + { + tempSongs = tempSongs.filter(song -> { + if (song == null) return true; // Random + return song.songDifficulties.contains(currentDifficulty); + }); + } + + if (onlyIfChanged) + { + // == performs equality by reference + if (tempSongs.isEqualUnordered(currentFilteredSongs)) return; + } + + // Only now do we know that the filter is actually changing. + + rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId; + + for (cap in grpCapsules.members) + { + cap.kill(); + } + + currentFilter = filterStuff; + + currentFilteredSongs = tempSongs; + curSelected = 0; + var hsvShader:HSVShader = new HSVShader(); var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); @@ -658,11 +684,12 @@ class FreeplayState extends MusicBeatSubState if (FlxG.keys.justPressed.F) { - if (songs[curSelected] != null) + var targetSong = grpCapsules.members[curSelected]?.songData; + if (targetSong != null) { var realShit:Int = curSelected; - songs[curSelected].isFav = !songs[curSelected].isFav; - if (songs[curSelected].isFav) + targetSong.isFav = !targetSong.isFav; + if (targetSong.isFav) { FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, { @@ -854,11 +881,13 @@ class FreeplayState extends MusicBeatSubState { dj.resetAFKTimer(); changeDiff(-1); + generateSongList(currentFilter, true); } if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) { dj.resetAFKTimer(); changeDiff(1); + generateSongList(currentFilter, true); } if (controls.BACK && !typing.hasFocus) @@ -877,6 +906,8 @@ class FreeplayState extends MusicBeatSubState for (spr in grpSpr) { + if (spr == null) continue; + var funnyMoveShit:MoveData = moveData; if (moveData.x == null) funnyMoveShit.x = spr.x; @@ -899,7 +930,7 @@ class FreeplayState extends MusicBeatSubState if (Type.getClass(FlxG.state) == MainMenuState) { - FlxG.state.persistentUpdate = true; + FlxG.state.persistentUpdate = false; FlxG.state.persistentDraw = true; } @@ -926,7 +957,7 @@ class FreeplayState extends MusicBeatSubState public override function destroy():Void { super.destroy(); - var daSong:Null = songs[curSelected]; + var daSong:Null = grpCapsules.members[curSelected]?.songData; if (daSong != null) { clearDaCache(daSong.songName); @@ -948,10 +979,10 @@ class FreeplayState extends MusicBeatSubState currentDifficulty = diffIdsCurrent[currentDifficultyIndex]; - var daSong:Null = songs[curSelected]; + var daSong:Null = grpCapsules.members[curSelected].songData; if (daSong != null) { - var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty); + var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore?.accuracy ?? 0.0; rememberedDifficulty = currentDifficulty; @@ -1015,7 +1046,7 @@ class FreeplayState extends MusicBeatSubState albumRoll.setDifficultyStars(daSong?.songRating); // Set the album graphic and play the animation if relevant. - var newAlbumId:String = daSong?.albumId ?? Constants.DEFAULT_ALBUM_ID; + var newAlbumId:String = daSong?.albumId; if (albumRoll.albumId != newAlbumId) { albumRoll.albumId = newAlbumId; @@ -1103,6 +1134,12 @@ class FreeplayState extends MusicBeatSubState targetVariation: targetVariation, practiceMode: false, minimalMode: false, + + #if (debug || FORCE_DEBUG_VERSION) + botPlayMode: FlxG.keys.pressed.SHIFT, + #else + botPlayMode: false, + #end // TODO: Make these an option! It's currently only accessible via chart editor. // startTimestamp: 0.0, // playbackRate: 0.5, @@ -1115,10 +1152,12 @@ class FreeplayState extends MusicBeatSubState { if (rememberedSongId != null) { - curSelected = songs.findIndex(function(song) { + curSelected = currentFilteredSongs.findIndex(function(song) { if (song == null) return false; return song.songId == rememberedSongId; }); + + if (curSelected == -1) curSelected = 0; } if (rememberedDifficulty != null) @@ -1127,7 +1166,7 @@ class FreeplayState extends MusicBeatSubState } // Set the difficulty star count on the right. - var daSong:Null = songs[curSelected]; + var daSong:Null = grpCapsules.members[curSelected]?.songData; albumRoll.setDifficultyStars(daSong?.songRating ?? 0); } @@ -1156,8 +1195,10 @@ class FreeplayState extends MusicBeatSubState { intendedScore = 0; intendedCompletion = 0.0; + diffIdsCurrent = diffIdsTotal; rememberedSongId = null; rememberedDifficulty = null; + albumRoll.albumId = null; } for (index => capsule in grpCapsules.members) @@ -1201,6 +1242,21 @@ class FreeplayState extends MusicBeatSubState grpCapsules.members[curSelected].selected = true; } } + + /** + * Build an instance of `FreeplayState` that is above the `MainMenuState`. + * @return The MainMenuState with the FreeplayState as a substate. + */ + public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState + { + var result = new MainMenuState(); + result.persistentUpdate = false; + result.persistentDraw = true; + + result.openSubState(new FreeplayState(params, stickers)); + + return result; + } } /** @@ -1307,7 +1363,7 @@ class FreeplaySongData public var songName(default, null):String = ''; public var songCharacter(default, null):String = ''; public var songRating(default, null):Int = 0; - public var albumId(default, null):String = ''; + public var albumId(default, null):Null = null; public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; public var displayedVariations(default, null):Array = [Constants.DEFAULT_VARIATION]; @@ -1341,7 +1397,15 @@ class FreeplaySongData this.songName = songDifficulty.songName; this.songCharacter = songDifficulty.characters.opponent; this.songRating = songDifficulty.difficultyRating; - this.albumId = songDifficulty.album; + if (songDifficulty.album == null) + { + FlxG.log.warn('No album for: ${songDifficulty.songName}'); + this.albumId = Constants.DEFAULT_ALBUM_ID; + } + else + { + this.albumId = songDifficulty.album; + } } } diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index a8c2039ab..f38db1ccd 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -51,12 +51,10 @@ class MainMenuState extends MusicBeatState transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; - if (!(FlxG?.sound?.music?.playing ?? false)) - { - playMenuMusic(); - } + playMenuMusic(); - persistentUpdate = persistentDraw = true; + persistentUpdate = false; + persistentDraw = true; var bg:FlxSprite = new FlxSprite(Paths.image('menuBG')); bg.scrollFactor.x = 0; @@ -109,14 +107,21 @@ class MainMenuState extends MusicBeatState }); #if CAN_OPEN_LINKS + // In order to prevent popup blockers from triggering, + // we need to open the link as an immediate result of a keypress event, + // so we can't wait for the flicker animation to complete. var hasPopupBlocker = #if web true #else false #end; - createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker); + createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker); #end createMenuItem('options', 'mainmenu/options', function() { startExitState(() -> new funkin.ui.options.OptionsState()); }); + createMenuItem('credits', 'mainmenu/credits', function() { + startExitState(() -> new funkin.ui.credits.CreditsState()); + }); + // Reset position of menu items. var spacing = 160; var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2; @@ -125,6 +130,9 @@ class MainMenuState extends MusicBeatState var menuItem = menuItems.members[i]; menuItem.x = FlxG.width / 2; menuItem.y = top + spacing * i; + menuItem.scrollFactor.x = 0.0; + // This one affects how much the menu items move when you scroll between them. + menuItem.scrollFactor.y = 0.4; } resetCamStuff(); @@ -212,6 +220,11 @@ class MainMenuState extends MusicBeatState { WindowUtil.openURL(Constants.URL_ITCH); } + + function selectMerch() + { + WindowUtil.openURL(Constants.URL_MERCH); + } #end #if newgrounds @@ -311,8 +324,6 @@ class MainMenuState extends MusicBeatState // Open the debug menu, defaults to ` / ~ if (controls.DEBUG_MENU) { - this.persistentUpdate = false; - this.persistentDraw = false; FlxG.state.openSubState(new DebugMenuSubState()); } diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 1a4e13ab1..7bd4a84af 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -290,18 +290,6 @@ class TitleState extends MusicBeatState // do controls.PAUSE | controls.ACCEPT instead? var pressedEnter:Bool = FlxG.keys.justPressed.ENTER; - if (FlxG.onMobile) - { - for (touch in FlxG.touches.list) - { - if (touch.justPressed) - { - FlxG.switchState(() -> new FreeplayState()); - pressedEnter = true; - } - } - } - var gamepad:FlxGamepad = FlxG.gamepads.lastActive; if (gamepad != null) diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index 980c264e3..d913b8099 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -210,7 +210,8 @@ class LoadingState extends MusicBeatState } // Load and cache the song's charts. - if (params?.targetSong != null) + // Don't do this if we already provided the music and charts. + if (params?.targetSong != null && !params.overrideMusic) { params.targetSong.cacheCharts(true); } diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 5d355f2da..70899ce92 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -60,6 +60,11 @@ class Constants */ // ============================== + /** + * Link to buy merch for the game. + */ + public static final URL_MERCH:String = 'https://needlejuicerecords.com/pages/friday-night-funkin'; + /** * Preloader sitelock. * Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work. @@ -181,6 +186,12 @@ class Constants */ public static final DEFAULT_DIFFICULTY_LIST:Array = ['easy', 'normal', 'hard']; + /** + * List of all difficulties used by the base game. + * Includes Erect and Nightmare. + */ + public static final DEFAULT_DIFFICULTY_LIST_FULL:Array = ['easy', 'normal', 'hard', 'erect', 'nightmare']; + /** * Default player character for charts. */