Merge branch 'rewrite/master' into feature/freeplay-ost-text

This commit is contained in:
Cameron Taylor 2024-04-01 22:21:29 -04:00
commit 03cbba4a4e
26 changed files with 779 additions and 133 deletions

View file

@ -3,6 +3,10 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
create-nightly-html5: create-nightly-html5:
runs-on: [self-hosted, linux] runs-on: [self-hosted, linux]

View file

@ -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);

View file

@ -183,6 +183,7 @@
<haxedef name="haxeui_focus_out_on_click" /> <haxedef name="haxeui_focus_out_on_click" />
<!-- Required to use haxe.ui.backend.flixel.UIState with build macros. --> <!-- Required to use haxe.ui.backend.flixel.UIState with build macros. -->
<haxedef name="haxeui_dont_impose_base_class" /> <haxedef name="haxeui_dont_impose_base_class" />
<haxedef name="HARDCODED_CREDITS" />
<!-- Skip the Intro --> <!-- Skip the Intro -->
<section if="debug"> <section if="debug">

View file

@ -59,6 +59,7 @@ abstract Tallies(RawTallies)
totalNotes: 0, totalNotes: 0,
totalNotesHit: 0, totalNotesHit: 0,
maxCombo: 0, maxCombo: 0,
score: 0,
isNewHighscore: false isNewHighscore: false
} }
} }
@ -81,6 +82,9 @@ typedef RawTallies =
var good:Int; var good:Int;
var sick:Int; var sick:Int;
var maxCombo:Int; var maxCombo:Int;
var score:Int;
var isNewHighscore:Bool; var isNewHighscore:Bool;
/** /**

View file

@ -402,10 +402,16 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return sound; return sound;
} }
@:nullSafety(Off)
public override function destroy():Void public override function destroy():Void
{ {
// trace('[FunkinSound] Destroying sound "${this._label}"'); // trace('[FunkinSound] Destroying sound "${this._label}"');
super.destroy(); super.destroy();
if (fadeTween != null)
{
fadeTween.cancel();
fadeTween = null;
}
FlxTween.cancelTweensOf(this); FlxTween.cancelTweensOf(this);
this._label = 'unknown'; this._label = 'unknown';
} }

View file

@ -158,11 +158,19 @@ class VoicesGroup extends SoundGroup
} }
public override function destroy():Void public override function destroy():Void
{
if (playerVoices != null)
{ {
playerVoices.destroy(); playerVoices.destroy();
playerVoices = null; playerVoices = null;
}
if (opponentVoices != null)
{
opponentVoices.destroy(); opponentVoices.destroy();
opponentVoices = null; opponentVoices = null;
}
super.destroy(); super.destroy();
} }
} }

View file

@ -325,12 +325,3 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
} }
} }
} }
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
contents:String
};

View file

@ -0,0 +1,10 @@
package funkin.data;
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
contents:String
};

View file

@ -427,7 +427,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return ScriptedSong.listScriptClasses(); return ScriptedSong.listScriptClasses();
} }
function loadEntryMetadataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> function loadEntryMetadataFile(id:String, ?variation:String):Null<JsonFile>
{ {
variation = variation == null ? Constants.DEFAULT_VARIATION : variation; variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == 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<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson}; return {fileName: entryFilePath, contents: rawJson};
} }
function loadMusicDataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> function loadMusicDataFile(id:String, ?variation:String):Null<JsonFile>
{ {
variation = variation == null ? Constants.DEFAULT_VARIATION : variation; variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json'); var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
@ -460,7 +460,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return openfl.Assets.exists(entryFilePath); return openfl.Assets.exists(entryFilePath);
} }
function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile> function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
{ {
variation = variation == null ? Constants.DEFAULT_VARIATION : variation; variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');

View file

@ -238,11 +238,11 @@ class GameOverSubState extends MusicBeatSubState
} }
else if (PlayStatePlaylist.isStoryMode) else if (PlayStatePlaylist.isStoryMode)
{ {
FlxG.switchState(() -> new StoryMenuState()); openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
} }
else else
{ {
FlxG.switchState(() -> new FreeplayState()); openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(sticker)));
} }
} }

View file

@ -12,6 +12,7 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.ui.freeplay.FreeplayState;
import funkin.graphics.FunkinSprite; import funkin.graphics.FunkinSprite;
import funkin.play.cutscene.VideoCutscene; import funkin.play.cutscene.VideoCutscene;
import funkin.play.PlayState; import funkin.play.PlayState;
@ -230,7 +231,7 @@ class PauseSubState extends MusicBeatSubState
*/ */
function startPauseMusic():Void 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); pauseMusic = FunkinSound.load(pauseMusicPath, true, true);
if (pauseMusic == null) if (pauseMusic == null)
@ -567,6 +568,8 @@ class PauseSubState extends MusicBeatSubState
PlayStatePlaylist.campaignDifficulty = difficulty; PlayStatePlaylist.campaignDifficulty = difficulty;
PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty; PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
FreeplayState.rememberedDifficulty = difficulty;
PlayState.instance.needsReset = true; PlayState.instance.needsReset = true;
state.close(); state.close();
@ -658,7 +661,7 @@ class PauseSubState extends MusicBeatSubState
} }
else 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)));
} }
} }

View file

@ -728,6 +728,10 @@ class PlayState extends MusicBeatSubState
#end #end
initialized = true; 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 public override function draw():Void
@ -1720,8 +1724,6 @@ class PlayState extends MusicBeatSubState
playerStrumline.fadeInArrows(); playerStrumline.fadeInArrows();
opponentStrumline.fadeInArrows(); opponentStrumline.fadeInArrows();
} }
this.refresh();
} }
/** /**
@ -2441,7 +2443,8 @@ class PlayState extends MusicBeatSubState
if (Highscore.tallies.combo != 0) if (Highscore.tallies.combo != 0)
{ {
// Break the combo. // Break the combo.
Highscore.tallies.combo = comboPopUps.displayCombo(0); if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
Highscore.tallies.combo = 0;
} }
if (playSound) if (playSound)
@ -2568,32 +2571,38 @@ class PlayState extends MusicBeatSubState
*/ */
function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
{ {
vocals.playerVolume = 1;
if (daRating == 'miss') if (daRating == 'miss')
{ {
// If daRating is 'miss', that means we made a mistake and should not continue. // 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. // TODO: Remove this.
comboPopUps.displayRating('miss'); comboPopUps.displayRating('miss');
return; return;
} }
vocals.playerVolume = 1;
var isComboBreak = false; var isComboBreak = false;
switch (daRating) switch (daRating)
{ {
case 'sick': case 'sick':
Highscore.tallies.sick += 1; Highscore.tallies.sick += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good': case 'good':
Highscore.tallies.good += 1; Highscore.tallies.good += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad': case 'bad':
Highscore.tallies.bad += 1; Highscore.tallies.bad += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit': case 'shit':
Highscore.tallies.shit += 1; Highscore.tallies.shit += 1;
Highscore.tallies.totalNotesHit++;
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
default:
FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!');
} }
health += healthChange; health += healthChange;
@ -2601,18 +2610,18 @@ class PlayState extends MusicBeatSubState
if (isComboBreak) if (isComboBreak)
{ {
// Break the combo, but don't increment tallies.misses. // 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 else
{ {
Highscore.tallies.combo++; 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;
} }
playerStrumline.hitNote(daNote, !isComboBreak); playerStrumline.hitNote(daNote, !isComboBreak);
if (daRating == "sick") if (daRating == 'sick')
{ {
playerStrumline.playNoteSplash(daNote.noteData.getDirection()); playerStrumline.playNoteSplash(daNote.noteData.getDirection());
} }
@ -2724,7 +2733,7 @@ class PlayState extends MusicBeatSubState
*/ */
public function endSong(rightGoddamnNow:Bool = false):Void 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; vocals.volume = 0;
mayPauseGame = false; mayPauseGame = false;
@ -2742,6 +2751,8 @@ class PlayState extends MusicBeatSubState
deathCounter = 0; deathCounter = 0;
var isNewHighscore = false;
if (currentSong != null && currentSong.validScore) if (currentSong != null && currentSong.validScore)
{ {
// crackhead double thingie, sets whether was new highscore, AND saves the song! // crackhead double thingie, sets whether was new highscore, AND saves the song!
@ -2772,11 +2783,14 @@ class PlayState extends MusicBeatSubState
#if newgrounds #if newgrounds
NGio.postScore(score, currentSong.id); NGio.postScore(score, currentSong.id);
#end #end
isNewHighscore = true;
} }
} }
if (PlayStatePlaylist.isStoryMode) if (PlayStatePlaylist.isStoryMode)
{ {
isNewHighscore = false;
PlayStatePlaylist.campaignScore += songScore; PlayStatePlaylist.campaignScore += songScore;
// Pop the next song ID from the list. // Pop the next song ID from the list.
@ -2785,18 +2799,6 @@ class PlayState extends MusicBeatSubState
if (targetSongId == null) 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) if (currentSong.validScore)
{ {
NGio.unlockMedal(60961); NGio.unlockMedal(60961);
@ -2826,6 +2828,7 @@ class PlayState extends MusicBeatSubState
#if newgrounds #if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}'); NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end #end
isNewHighscore = true;
} }
} }
@ -2837,11 +2840,11 @@ class PlayState extends MusicBeatSubState
{ {
if (rightGoddamnNow) if (rightGoddamnNow)
{ {
moveToResultsScreen(); moveToResultsScreen(isNewHighscore);
} }
else else
{ {
zoomIntoResultsScreen(); zoomIntoResultsScreen(isNewHighscore);
} }
} }
} }
@ -2902,11 +2905,11 @@ class PlayState extends MusicBeatSubState
{ {
if (rightGoddamnNow) if (rightGoddamnNow)
{ {
moveToResultsScreen(); moveToResultsScreen(isNewHighscore);
} }
else 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. * 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!'); trace('WENT TO RESULTS SCREEN!');
@ -3037,7 +3040,7 @@ class PlayState extends MusicBeatSubState
{ {
ease: FlxEase.expoIn, ease: FlxEase.expoIn,
onComplete: function(_) { onComplete: function(_) {
moveToResultsScreen(); moveToResultsScreen(isNewHighscore);
} }
}); });
}); });
@ -3046,7 +3049,7 @@ class PlayState extends MusicBeatSubState
/** /**
* Move to the results screen right goddamn now. * Move to the results screen right goddamn now.
*/ */
function moveToResultsScreen():Void function moveToResultsScreen(isNewHighscore:Bool):Void
{ {
persistentUpdate = false; persistentUpdate = false;
vocals.stop(); vocals.stop();
@ -3058,7 +3061,24 @@ class PlayState extends MusicBeatSubState
{ {
storyMode: PlayStatePlaylist.isStoryMode, storyMode: PlayStatePlaylist.isStoryMode,
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), 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; res.camera = camHUD;
openSubState(res); openSubState(res);
@ -3205,7 +3225,10 @@ class PlayState extends MusicBeatSubState
// Don't go back in time to before the song started. // Don't go back in time to before the song started.
targetTimeMs = Math.max(0, targetTimeMs); targetTimeMs = Math.max(0, targetTimeMs);
if (FlxG.sound.music != null)
{
FlxG.sound.music.time = targetTimeMs; FlxG.sound.music.time = targetTimeMs;
}
handleSkippedNotes(); handleSkippedNotes();
SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition); SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition);

View file

@ -1,5 +1,6 @@
package funkin.play; package funkin.play;
import funkin.util.MathUtil;
import funkin.ui.story.StoryMenuState; import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite; import flixel.FlxSprite;
@ -16,6 +17,8 @@ import flixel.tweens.FlxTween;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import flixel.util.FlxGradient; import flixel.util.FlxGradient;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.graphics.shaders.LeftMaskShader; import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter; import funkin.play.components.TallyCounter;
@ -42,12 +45,15 @@ class ResultState extends MusicBeatSubState
override function create():Void override function create():Void
{ {
if (params.tallies.sick == params.tallies.totalNotesHit /*
&& params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT; if (params.scoreData.sick == params.scoreData.totalNotesHit
else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50) && 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! resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else else
resultsVariation = NORMAL; resultsVariation = NORMAL;
*/
resultsVariation = NORMAL;
FunkinSound.playMusic('results$resultsVariation', FunkinSound.playMusic('results$resultsVariation',
{ {
@ -130,12 +136,16 @@ class ResultState extends MusicBeatSubState
var diffSpr:String = switch (PlayState.instance.currentDifficulty) var diffSpr:String = switch (PlayState.instance.currentDifficulty)
{ {
case 'EASY': case 'easy':
'difEasy'; 'difEasy';
case 'NORMAL': case 'normal':
'difNormal'; 'difNormal';
case 'HARD': case 'hard':
'difHard'; 'difHard';
case 'erect':
'difErect';
case 'nightmare':
'difNightmare';
case _: case _:
'difNormal'; '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. * 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); 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); ratingGrp.add(maxCombo);
hStuf += 2; hStuf += 2;
var extraYOffset:Float = 5; 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); 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); 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); 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); 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); 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) for (ind => rating in ratingGrp.members)
{ {
rating.visible = false; rating.visible = false;
@ -235,9 +249,16 @@ class ResultState extends MusicBeatSubState
scorePopin.animation.play("score"); scorePopin.animation.play("score");
scorePopin.visible = true; scorePopin.visible = true;
if (params.isNewHighscore)
{
highscoreNew.visible = true; highscoreNew.visible = true;
highscoreNew.animation.play("new"); highscoreNew.animation.play("new");
FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
}
else
{
highscoreNew.visible = false;
}
}; };
switch (resultsVariation) switch (resultsVariation)
@ -276,8 +297,6 @@ class ResultState extends MusicBeatSubState
} }
}); });
if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
super.create(); super.create();
} }
@ -365,7 +384,7 @@ class ResultState extends MusicBeatSubState
} }
else 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; var title:String;
/**
* Whether the displayed score is a new highscore
*/
var isNewHighscore:Bool;
/** /**
* The score, accuracy, and judgements. * The score, accuracy, and judgements.
*/ */
var tallies:Highscore.Tallies; var scoreData:SaveScoreData;
}; };

View file

@ -85,7 +85,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
comboSpr.velocity.y -= 150; comboSpr.velocity.y -= 150;
comboSpr.velocity.x += FlxG.random.int(1, 10); comboSpr.velocity.x += FlxG.random.int(1, 10);
add(comboSpr); // add(comboSpr);
if (PlayState.instance.currentStageId.startsWith('school')) if (PlayState.instance.currentStageId.startsWith('school'))
{ {

View file

@ -6,6 +6,8 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.text.FlxText.FlxTextAlign;
import funkin.util.MathUtil;
/** /**
* Numerical counters used next to each judgement in the Results screen. * Numerical counters used next to each judgement in the Results screen.
@ -13,18 +15,23 @@ import flixel.tweens.FlxTween;
class TallyCounter extends FlxTypedSpriteGroup<FlxSprite> class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
{ {
public var curNumber:Float = 0; public var curNumber:Float = 0;
public var neededNumber:Int = 0; public var neededNumber:Int = 0;
public var flavour:Int = 0xFFFFFFFF; 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); super(x, y);
this.align = align;
this.flavour = flavour; this.flavour = flavour;
this.neededNumber = neededNumber; this.neededNumber = neededNumber;
drawNumbers();
if (curNumber == neededNumber) drawNumbers();
} }
var tmr:Float = 0; var tmr:Float = 0;
@ -41,6 +48,8 @@ class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
var seperatedScore:Array<Int> = []; var seperatedScore:Array<Int> = [];
var tempCombo:Int = Math.round(curNumber); var tempCombo:Int = Math.round(curNumber);
var fullNumberDigits:Int = Std.int(Math.max(1, Math.ceil(MathUtil.logBase(10, neededNumber))));
while (tempCombo != 0) while (tempCombo != 0)
{ {
seperatedScore.push(tempCombo % 10); seperatedScore.push(tempCombo % 10);
@ -55,7 +64,13 @@ class TallyCounter extends FlxTypedSpriteGroup<FlxSprite>
{ {
if (ind >= members.length) 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); add(numb);
numb.color = flavour; numb.color = flavour;
} }

View file

@ -295,6 +295,11 @@ class Strumline extends FlxSpriteGroup
{ {
if (noteData.length == 0) return; 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 songStart:Float = PlayState.instance?.startTimestamp ?? 0.0;
var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS; var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS;
var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_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. // The note sprite pool is full and all note splashes are active.
// We have to create a new note. // We have to create a new note.
result = new SustainTrail(0, 100, noteStyle); result = new SustainTrail(0, 0, noteStyle);
this.holdNotes.add(result); this.holdNotes.add(result);
} }

View file

@ -0,0 +1,34 @@
package funkin.ui.credits;
/**
* The members of the Funkin' Crew, organized by their roles.
*/
typedef CreditsData =
{
var entries:Array<CreditsDataRole>;
}
/**
* The members of a specific role on the Funkin' Crew.
*/
typedef CreditsDataRole =
{
@:optional
var header:String;
@:optional
@:default([])
var body:Array<CreditsDataMember>;
@:optional
@:default(false)
var appendBackers:Bool;
}
/**
* A member of a specific person on the Funkin' Crew.
*/
typedef CreditsDataMember =
{
var line:String;
}

View file

@ -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<CreditsData>):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<String>
{
// 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<CreditsData> = #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<CreditsData> = null;
static function get_CREDITS_DATA():Null<CreditsData>
{
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<CreditsData>
{
#if !macro
if (file.contents == null) return null;
var parser = new json2object.JsonParser<CreditsData>();
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<json2object.Error>, id:String = ''):Void
{
trace('[CREDITS] Failed to parse credits data: ${id}');
for (error in errors)
funkin.data.DataError.printError(error);
}
#end
}

View file

@ -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<CreditsData>
{
#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<String>
{
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<CreditsData>
{
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
}

View file

@ -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;
}

View file

@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite
var gotSpooked:Bool = false; var gotSpooked:Bool = false;
static final SPOOK_PERIOD:Float = 10.0; static final SPOOK_PERIOD:Float = 120.0;
static final TV_PERIOD:Float = 10.0; static final TV_PERIOD:Float = 180.0;
// Time since dad last SPOOKED you. // Time since dad last SPOOKED you.
var timeSinceSpook:Float = 0; var timeSinceSpook:Float = 0;
@ -43,7 +43,14 @@ class DJBoyfriend extends FlxAtlasSprite
switch (name) switch (name)
{ {
case "Boyfriend DJ watchin tv OG": case "Boyfriend DJ watchin tv OG":
if (number == 85) runTvLogic(); if (number == 80)
{
FunkinSound.playOnce(Paths.sound('remote_click'));
}
if (number == 85)
{
runTvLogic();
}
default: default:
} }
}; };
@ -219,19 +226,17 @@ class DJBoyfriend extends FlxAtlasSprite
if (cartoonSnd == null) if (cartoonSnd == null)
{ {
// tv is OFF, but getting turned on // 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() {
// FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() {
// });
loadCartoon(); loadCartoon();
});
} }
else else
{ {
// plays it smidge after the click // plays it smidge after the click
// new FlxTimer().start(0.1, function(_) { FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() {
// // FunkinSound.playOnce(Paths.sound('channel_switch'));
// });
cartoonSnd.destroy(); cartoonSnd.destroy();
loadCartoon(); loadCartoon();
});
} }
// loadCartoon(); // loadCartoon();

View file

@ -133,8 +133,8 @@ class FreeplayState extends MusicBeatSubState
var stickerSubState:StickerSubState; var stickerSubState:StickerSubState;
static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY; public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
static var rememberedSongId:Null<String> = null; public static var rememberedSongId:Null<String> = null;
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
{ {
@ -145,7 +145,7 @@ class FreeplayState extends MusicBeatSubState
stickerSubState = stickers; stickerSubState = stickers;
} }
super(); super(FlxColor.TRANSPARENT);
} }
override function create():Void override function create():Void
@ -536,21 +536,18 @@ class FreeplayState extends MusicBeatSubState
}); });
} }
var currentFilter:SongFilter = null;
var currentFilteredSongs:Array<FreeplaySongData> = [];
/** /**
* Given the current filter, rebuild the current song list. * Given the current filter, rebuild the current song list.
* *
* @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite) * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite)
* @param force * @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<SongFilter>, force:Bool = false, onlyIfChanged:Bool = true):Void
{ {
curSelected = 1;
for (cap in grpCapsules.members)
{
cap.kill();
}
var tempSongs:Array<FreeplaySongData> = songs; var tempSongs:Array<FreeplaySongData> = songs;
if (filterStuff != null) 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 hsvShader:HSVShader = new HSVShader();
var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem);
@ -658,11 +684,12 @@ class FreeplayState extends MusicBeatSubState
if (FlxG.keys.justPressed.F) if (FlxG.keys.justPressed.F)
{ {
if (songs[curSelected] != null) var targetSong = grpCapsules.members[curSelected]?.songData;
if (targetSong != null)
{ {
var realShit:Int = curSelected; var realShit:Int = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav; targetSong.isFav = !targetSong.isFav;
if (songs[curSelected].isFav) if (targetSong.isFav)
{ {
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{ {
@ -854,11 +881,13 @@ class FreeplayState extends MusicBeatSubState
{ {
dj.resetAFKTimer(); dj.resetAFKTimer();
changeDiff(-1); changeDiff(-1);
generateSongList(currentFilter, true);
} }
if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL)
{ {
dj.resetAFKTimer(); dj.resetAFKTimer();
changeDiff(1); changeDiff(1);
generateSongList(currentFilter, true);
} }
if (controls.BACK && !typing.hasFocus) if (controls.BACK && !typing.hasFocus)
@ -901,7 +930,7 @@ class FreeplayState extends MusicBeatSubState
if (Type.getClass(FlxG.state) == MainMenuState) if (Type.getClass(FlxG.state) == MainMenuState)
{ {
FlxG.state.persistentUpdate = true; FlxG.state.persistentUpdate = false;
FlxG.state.persistentDraw = true; FlxG.state.persistentDraw = true;
} }
@ -928,7 +957,7 @@ class FreeplayState extends MusicBeatSubState
public override function destroy():Void public override function destroy():Void
{ {
super.destroy(); super.destroy();
var daSong:Null<FreeplaySongData> = songs[curSelected]; var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected]?.songData;
if (daSong != null) if (daSong != null)
{ {
clearDaCache(daSong.songName); clearDaCache(daSong.songName);
@ -950,10 +979,10 @@ class FreeplayState extends MusicBeatSubState
currentDifficulty = diffIdsCurrent[currentDifficultyIndex]; currentDifficulty = diffIdsCurrent[currentDifficultyIndex];
var daSong:Null<FreeplaySongData> = songs[curSelected]; var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData;
if (daSong != null) 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; intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0; intendedCompletion = songScore?.accuracy ?? 0.0;
rememberedDifficulty = currentDifficulty; rememberedDifficulty = currentDifficulty;
@ -1105,6 +1134,12 @@ class FreeplayState extends MusicBeatSubState
targetVariation: targetVariation, targetVariation: targetVariation,
practiceMode: false, practiceMode: false,
minimalMode: 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. // TODO: Make these an option! It's currently only accessible via chart editor.
// startTimestamp: 0.0, // startTimestamp: 0.0,
// playbackRate: 0.5, // playbackRate: 0.5,
@ -1117,10 +1152,12 @@ class FreeplayState extends MusicBeatSubState
{ {
if (rememberedSongId != null) if (rememberedSongId != null)
{ {
curSelected = songs.findIndex(function(song) { curSelected = currentFilteredSongs.findIndex(function(song) {
if (song == null) return false; if (song == null) return false;
return song.songId == rememberedSongId; return song.songId == rememberedSongId;
}); });
if (curSelected == -1) curSelected = 0;
} }
if (rememberedDifficulty != null) if (rememberedDifficulty != null)
@ -1129,7 +1166,7 @@ class FreeplayState extends MusicBeatSubState
} }
// Set the difficulty star count on the right. // Set the difficulty star count on the right.
var daSong:Null<FreeplaySongData> = songs[curSelected]; var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected]?.songData;
albumRoll.setDifficultyStars(daSong?.songRating ?? 0); albumRoll.setDifficultyStars(daSong?.songRating ?? 0);
} }
@ -1158,6 +1195,7 @@ class FreeplayState extends MusicBeatSubState
{ {
intendedScore = 0; intendedScore = 0;
intendedCompletion = 0.0; intendedCompletion = 0.0;
diffIdsCurrent = diffIdsTotal;
rememberedSongId = null; rememberedSongId = null;
rememberedDifficulty = null; rememberedDifficulty = null;
albumRoll.albumId = null; albumRoll.albumId = null;
@ -1204,6 +1242,21 @@ class FreeplayState extends MusicBeatSubState
grpCapsules.members[curSelected].selected = true; 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;
}
} }
/** /**

View file

@ -51,12 +51,10 @@ class MainMenuState extends MusicBeatState
transIn = FlxTransitionableState.defaultTransIn; transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut; 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')); var bg:FlxSprite = new FlxSprite(Paths.image('menuBG'));
bg.scrollFactor.x = 0; bg.scrollFactor.x = 0;
@ -109,14 +107,21 @@ class MainMenuState extends MusicBeatState
}); });
#if CAN_OPEN_LINKS #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; var hasPopupBlocker = #if web true #else false #end;
createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker); createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker);
#end #end
createMenuItem('options', 'mainmenu/options', function() { createMenuItem('options', 'mainmenu/options', function() {
startExitState(() -> new funkin.ui.options.OptionsState()); startExitState(() -> new funkin.ui.options.OptionsState());
}); });
createMenuItem('credits', 'mainmenu/credits', function() {
startExitState(() -> new funkin.ui.credits.CreditsState());
});
// Reset position of menu items. // Reset position of menu items.
var spacing = 160; var spacing = 160;
var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2; var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2;
@ -125,6 +130,9 @@ class MainMenuState extends MusicBeatState
var menuItem = menuItems.members[i]; var menuItem = menuItems.members[i];
menuItem.x = FlxG.width / 2; menuItem.x = FlxG.width / 2;
menuItem.y = top + spacing * i; 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(); resetCamStuff();
@ -212,6 +220,11 @@ class MainMenuState extends MusicBeatState
{ {
WindowUtil.openURL(Constants.URL_ITCH); WindowUtil.openURL(Constants.URL_ITCH);
} }
function selectMerch()
{
WindowUtil.openURL(Constants.URL_MERCH);
}
#end #end
#if newgrounds #if newgrounds
@ -311,8 +324,6 @@ class MainMenuState extends MusicBeatState
// Open the debug menu, defaults to ` / ~ // Open the debug menu, defaults to ` / ~
if (controls.DEBUG_MENU) if (controls.DEBUG_MENU)
{ {
this.persistentUpdate = false;
this.persistentDraw = false;
FlxG.state.openSubState(new DebugMenuSubState()); FlxG.state.openSubState(new DebugMenuSubState());
} }

View file

@ -290,18 +290,6 @@ class TitleState extends MusicBeatState
// do controls.PAUSE | controls.ACCEPT instead? // do controls.PAUSE | controls.ACCEPT instead?
var pressedEnter:Bool = FlxG.keys.justPressed.ENTER; 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; var gamepad:FlxGamepad = FlxG.gamepads.lastActive;
if (gamepad != null) if (gamepad != null)

View file

@ -210,7 +210,8 @@ class LoadingState extends MusicBeatState
} }
// Load and cache the song's charts. // 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); params.targetSong.cacheCharts(true);
} }

View file

@ -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. * Preloader sitelock.
* Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work. * 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<String> = ['easy', 'normal', 'hard']; public static final DEFAULT_DIFFICULTY_LIST:Array<String> = ['easy', 'normal', 'hard'];
/**
* List of all difficulties used by the base game.
* Includes Erect and Nightmare.
*/
public static final DEFAULT_DIFFICULTY_LIST_FULL:Array<String> = ['easy', 'normal', 'hard', 'erect', 'nightmare'];
/** /**
* Default player character for charts. * Default player character for charts.
*/ */