Funkin/source/funkin/ui/story/StoryMenuState.hx

661 lines
18 KiB
Haxe

package funkin.ui.story;
import funkin.ui.mainmenu.MainMenuState;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import openfl.utils.Assets;
import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.text.FlxText;
import flixel.addons.transition.FlxTransitionableState;
import flixel.tweens.FlxEase;
import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.data.level.LevelRegistry;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.ui.mainmenu.MainMenuState;
import funkin.play.song.Song;
import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import funkin.util.MathUtil;
import funkin.ui.transition.LoadingState;
import funkin.ui.transition.StickerSubState;
class StoryMenuState extends MusicBeatState
{
static final DEFAULT_BACKGROUND_COLOR:FlxColor = FlxColor.fromString("#F9CF51");
static final BACKGROUND_HEIGHT:Int = 400;
var currentDifficultyId:String = 'normal';
var currentLevelId:String = 'tutorial';
var currentLevel:Level;
var isLevelUnlocked:Bool;
var currentLevelTitle:LevelTitle;
var highScore:Int = 42069420;
var highScoreLerp:Int = 12345678;
var exitingMenu:Bool = false;
var selectedLevel:Bool = false;
var displayingModdedLevels:Bool = false;
//
// RENDER OBJECTS
//
/**
* The title of the level at the top.
*/
var levelTitleText:FlxText;
/**
* The score text at the top.
*/
var scoreText:FlxText;
/**
* The mode text at the top-middle.
*/
var modeText:FlxText;
/**
* The list of songs on the left.
*/
var tracklistText:FlxText;
/**
* The titles of the levels in the middle.
*/
var levelTitles:FlxTypedGroup<LevelTitle>;
/**
* The props in the center.
*/
var levelProps:FlxTypedGroup<LevelProp>;
/**
* The background behind the props.
*/
var levelBackground:FlxSprite;
/**
* The left arrow of the difficulty selector.
*/
var leftDifficultyArrow:FlxSprite;
/**
* The right arrow of the difficulty selector.
*/
var rightDifficultyArrow:FlxSprite;
/**
* The text of the difficulty selector.
*/
var difficultySprite:FlxSprite;
var difficultySprites:Map<String, FlxSprite>;
var stickerSubState:StickerSubState;
static var rememberedLevelId:Null<String> = null;
static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
public function new(?stickers:StickerSubState = null)
{
super();
if (stickers != null)
{
stickerSubState = stickers;
}
}
override function create():Void
{
super.create();
difficultySprites = new Map<String, FlxSprite>();
transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut;
playMenuMusic();
if (stickerSubState != null)
{
this.persistentUpdate = true;
this.persistentDraw = true;
openSubState(stickerSubState);
stickerSubState.degenStickers();
}
persistentUpdate = persistentDraw = true;
updateData();
rememberSelection();
// Explicitly define the background color.
this.bgColor = FlxColor.BLACK;
levelTitles = new FlxTypedGroup<LevelTitle>();
levelTitles.zIndex = 15;
add(levelTitles);
updateBackground();
var black:FunkinSprite = new FunkinSprite(levelBackground.x, 0).makeSolidColor(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK);
black.zIndex = levelBackground.zIndex - 1;
add(black);
levelProps = new FlxTypedGroup<LevelProp>();
levelProps.zIndex = 1000;
add(levelProps);
updateProps();
tracklistText = new FlxText(FlxG.width * 0.05, levelBackground.x + levelBackground.height + 100, 0, "Tracks", 32);
tracklistText.setFormat("VCR OSD Mono", 32);
tracklistText.alignment = CENTER;
tracklistText.color = 0xFFe55777;
add(tracklistText);
scoreText = new FlxText(10, 10, 0, 'HIGH SCORE: 42069420');
scoreText.setFormat("VCR OSD Mono", 32);
scoreText.zIndex = 1000;
add(scoreText);
modeText = new FlxText(10, 10, 0, 'Base Game Levels [TAB to switch]');
modeText.setFormat("VCR OSD Mono", 32);
modeText.screenCenter(X);
modeText.visible = hasModdedLevels();
modeText.zIndex = 1000;
add(modeText);
levelTitleText = new FlxText(FlxG.width * 0.7, 10, 0, 'LEVEL 1');
levelTitleText.setFormat("VCR OSD Mono", 32, FlxColor.WHITE, RIGHT);
levelTitleText.alpha = 0.7;
levelTitleText.zIndex = 1000;
add(levelTitleText);
buildLevelTitles();
leftDifficultyArrow = new FlxSprite(levelTitles.members[0].x + levelTitles.members[0].width + 10, levelTitles.members[0].y + 10);
leftDifficultyArrow.frames = Paths.getSparrowAtlas('storymenu/ui/arrows');
leftDifficultyArrow.animation.addByPrefix('idle', 'leftIdle0');
leftDifficultyArrow.animation.addByPrefix('press', 'leftConfirm0');
leftDifficultyArrow.animation.play('idle');
add(leftDifficultyArrow);
buildDifficultySprite(Constants.DEFAULT_DIFFICULTY);
buildDifficultySprite();
rightDifficultyArrow = new FlxSprite(difficultySprite.x + difficultySprite.width + 10, leftDifficultyArrow.y);
rightDifficultyArrow.frames = leftDifficultyArrow.frames;
rightDifficultyArrow.animation.addByPrefix('idle', 'rightIdle0');
rightDifficultyArrow.animation.addByPrefix('press', 'rightConfirm0');
rightDifficultyArrow.animation.play('idle');
add(rightDifficultyArrow);
add(difficultySprite);
updateText();
changeDifficulty();
changeLevel();
refresh();
#if discord_rpc
// Updating Discord Rich Presence
DiscordClient.changePresence("In the Menus", null);
#end
}
function rememberSelection():Void
{
if (rememberedLevelId != null)
{
currentLevelId = rememberedLevelId;
}
if (rememberedDifficulty != null)
{
currentDifficultyId = rememberedDifficulty;
}
}
function playMenuMusic():Void
{
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
{
var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
if (freakyMenuMetadata != null)
{
Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges);
}
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
}
}
function updateData():Void
{
currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId);
isLevelUnlocked = currentLevel == null ? false : currentLevel.isUnlocked();
}
function buildDifficultySprite(?diff:String):Void
{
if (diff == null) diff = currentDifficultyId;
remove(difficultySprite);
difficultySprite = difficultySprites.get(diff);
if (difficultySprite == null)
{
difficultySprite = new FlxSprite(leftDifficultyArrow.x + leftDifficultyArrow.width + 10, leftDifficultyArrow.y);
if (Assets.exists(Paths.file('images/storymenu/difficulties/${diff}.xml')))
{
difficultySprite.frames = Paths.getSparrowAtlas('storymenu/difficulties/${diff}');
difficultySprite.animation.addByPrefix('idle', 'idle0', 24, true);
if (Preferences.flashingLights) difficultySprite.animation.play('idle');
}
else
{
difficultySprite.loadGraphic(Paths.image('storymenu/difficulties/${diff}'));
}
difficultySprites.set(diff, difficultySprite);
difficultySprite.x += (difficultySprites.get(Constants.DEFAULT_DIFFICULTY).width - difficultySprite.width) / 2;
}
difficultySprite.alpha = 0;
difficultySprite.y = leftDifficultyArrow.y - 15;
var targetY:Float = leftDifficultyArrow.y + 10;
targetY -= (difficultySprite.height - difficultySprites.get(Constants.DEFAULT_DIFFICULTY).height) / 2;
FlxTween.tween(difficultySprite, {y: targetY, alpha: 1}, 0.07);
add(difficultySprite);
}
function buildLevelTitles():Void
{
levelTitles.clear();
var levelIds:Array<String> = displayingModdedLevels ? LevelRegistry.instance.listModdedLevelIds() : LevelRegistry.instance.listBaseGameLevelIds();
if (levelIds.length == 0) levelIds = ['tutorial']; // Make sure there's at least one level to display.
for (levelIndex in 0...levelIds.length)
{
var levelId:String = levelIds[levelIndex];
var level:Level = LevelRegistry.instance.fetchEntry(levelId);
if (level == null) continue;
var levelTitleItem:LevelTitle = new LevelTitle(0, Std.int(levelBackground.y + levelBackground.height + 10), level);
levelTitleItem.targetY = ((levelTitleItem.height + 20) * levelIndex);
levelTitleItem.screenCenter(X);
levelTitles.add(levelTitleItem);
}
}
function switchMode(moddedLevels:Bool):Void
{
displayingModdedLevels = moddedLevels;
buildLevelTitles();
changeLevel(999999); // Jump past the end of the list to the beginning.
changeDifficulty(0);
}
override function update(elapsed:Float)
{
Conductor.instance.update();
highScoreLerp = Std.int(MathUtil.coolLerp(highScoreLerp, highScore, 0.5));
scoreText.text = 'LEVEL SCORE: ${Math.round(highScoreLerp)}';
modeText.text = displayingModdedLevels ? 'Mods [TAB to switch]' : 'Base Game [TAB to switch]';
modeText.screenCenter(X);
levelTitleText.text = currentLevel.getTitle();
levelTitleText.x = FlxG.width - (levelTitleText.width + 10); // Right align.
handleKeyPresses();
super.update(elapsed);
}
function handleKeyPresses():Void
{
if (!exitingMenu)
{
if (!selectedLevel)
{
if (controls.UI_UP_P)
{
changeLevel(-1);
changeDifficulty(0);
}
if (controls.UI_DOWN_P)
{
changeLevel(1);
changeDifficulty(0);
}
if (controls.UI_RIGHT)
{
rightDifficultyArrow.animation.play('press');
}
else
{
rightDifficultyArrow.animation.play('idle');
}
if (controls.UI_LEFT)
{
leftDifficultyArrow.animation.play('press');
}
else
{
leftDifficultyArrow.animation.play('idle');
}
if (controls.UI_RIGHT_P)
{
changeDifficulty(1);
}
if (controls.UI_LEFT_P)
{
changeDifficulty(-1);
}
if (FlxG.keys.justPressed.TAB && modeText.visible)
{
switchMode(!displayingModdedLevels);
}
}
if (controls.ACCEPT)
{
selectLevel();
}
}
if (controls.BACK && !exitingMenu && !selectedLevel)
{
FlxG.sound.play(Paths.sound('cancelMenu'));
exitingMenu = true;
FlxG.switchState(() -> new MainMenuState());
}
}
function hasModdedLevels():Bool
{
return LevelRegistry.instance.listModdedLevelIds().length > 0;
}
/**
* Changes the selected level.
* @param change +1 (down), -1 (up)
*/
function changeLevel(change:Int = 0):Void
{
var levelList:Array<String> = displayingModdedLevels ? LevelRegistry.instance.listModdedLevelIds() : LevelRegistry.instance.listBaseGameLevelIds();
if (levelList.length == 0) levelList = ['tutorial'];
var currentIndex:Int = levelList.indexOf(currentLevelId);
currentIndex += change;
// Wrap around
if (currentIndex < 0) currentIndex = levelList.length - 1;
if (currentIndex >= levelList.length) currentIndex = 0;
var previousLevelId:String = currentLevelId;
currentLevelId = levelList[currentIndex];
rememberedLevelId = currentLevelId;
updateData();
for (index in 0...levelTitles.members.length)
{
var item:LevelTitle = levelTitles.members[index];
item.targetY = (index - currentIndex) * 120 + 480;
if (index == currentIndex)
{
currentLevelTitle = item;
item.alpha = 1.0;
}
else
{
item.alpha = 0.6;
}
}
updateText();
updateBackground(previousLevelId);
updateProps();
refresh();
}
/**
* Changes the selected difficulty.
* @param change +1 (right) to increase difficulty, -1 (left) to decrease difficulty
*/
function changeDifficulty(change:Int = 0):Void
{
var difficultyList:Array<String> = currentLevel.getDifficulties();
var currentIndex:Int = difficultyList.indexOf(currentDifficultyId);
currentIndex += change;
// Wrap around
if (currentIndex < 0) currentIndex = difficultyList.length - 1;
if (currentIndex >= difficultyList.length) currentIndex = 0;
var hasChanged:Bool = currentDifficultyId != difficultyList[currentIndex];
currentDifficultyId = difficultyList[currentIndex];
rememberedDifficulty = currentDifficultyId;
if (difficultyList.length <= 1)
{
leftDifficultyArrow.visible = false;
rightDifficultyArrow.visible = false;
}
else
{
leftDifficultyArrow.visible = true;
rightDifficultyArrow.visible = true;
}
if (hasChanged)
{
buildDifficultySprite();
// Disable the funny music thing for now.
// funnyMusicThing();
}
}
final FADE_OUT_TIME:Float = 1.5;
function funnyMusicThing():Void
{
if (currentDifficultyId == "nightmare")
{
FlxG.sound.music.fadeOut(FADE_OUT_TIME, 0.0);
}
else
{
FlxG.sound.music.fadeOut(FADE_OUT_TIME, 1.0);
}
}
public override function dispatchEvent(event:ScriptEvent):Void
{
// super.dispatchEvent(event) dispatches event to module scripts.
super.dispatchEvent(event);
if (levelProps != null && levelProps.length > 0)
{
// Dispatch event to props.
for (prop in levelProps)
{
ScriptEventDispatcher.callEvent(prop, event);
}
}
}
function selectLevel()
{
if (!currentLevel.isUnlocked())
{
FlxG.sound.play(Paths.sound('cancelMenu'));
return;
}
if (selectedLevel) return;
selectedLevel = true;
FlxG.sound.play(Paths.sound('confirmMenu'));
currentLevelTitle.isFlashing = true;
for (prop in levelProps.members)
{
prop.playConfirm();
}
Paths.setCurrentLevel(currentLevel.id);
PlayStatePlaylist.playlistSongIds = currentLevel.getSongs();
PlayStatePlaylist.isStoryMode = true;
PlayStatePlaylist.campaignScore = 0;
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
PlayStatePlaylist.campaignId = currentLevel.id;
PlayStatePlaylist.campaignTitle = currentLevel.getTitle();
PlayStatePlaylist.campaignDifficulty = currentDifficultyId;
if (targetSong != null)
{
// Load and cache the song's charts.
// TODO: Do this in the loading state.
targetSong.cacheCharts(true);
}
new FlxTimer().start(1, function(tmr:FlxTimer) {
FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false;
LoadingState.loadAndSwitchState(() -> new PlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
}), true);
});
}
function updateBackground(?previousLevelId:String = ''):Void
{
if (levelBackground == null || previousLevelId == '')
{
// Build a new background and display it immediately.
levelBackground = currentLevel.buildBackground();
levelBackground.x = 0;
levelBackground.y = 56;
levelBackground.zIndex = 100;
levelBackground.alpha = 1.0; // Not hidden.
add(levelBackground);
}
else
{
var previousLevel = LevelRegistry.instance.fetchEntry(previousLevelId);
if (currentLevel.isBackgroundSimple() && previousLevel.isBackgroundSimple())
{
var previousColor:FlxColor = previousLevel.getBackgroundColor();
var currentColor:FlxColor = currentLevel.getBackgroundColor();
if (previousColor != currentColor)
{
// Both the previous and current level were simple backgrounds.
// Fade between colors directly, rather than fading one background out and another in.
FlxTween.color(levelBackground, 0.4, previousColor, currentColor);
}
else
{
// Do no fade at all if the colors aren't different.
}
}
else
{
// Either the previous or current level has a complex background.
// We need to fade the old background out and the new one in.
// Reference the old background and fade it out.
var oldBackground:FlxSprite = levelBackground;
FlxTween.tween(oldBackground, {alpha: 0.0}, 0.6,
{
ease: FlxEase.linear,
onComplete: function(_) {
remove(oldBackground);
}
});
// Build a new background and fade it in.
levelBackground = currentLevel.buildBackground();
levelBackground.x = 0;
levelBackground.y = 56;
levelBackground.alpha = 0.0; // Hidden to start.
levelBackground.zIndex = 100;
add(levelBackground);
FlxTween.tween(levelBackground, {alpha: 1.0}, 0.6,
{
ease: FlxEase.linear
});
}
}
}
function updateProps():Void
{
for (prop in currentLevel.buildProps(levelProps.members))
{
prop.zIndex = 1000;
levelProps.add(prop);
}
refresh();
}
function updateText():Void
{
tracklistText.text = 'TRACKS\n\n';
tracklistText.text += currentLevel.getSongDisplayNames(currentDifficultyId).join('\n');
tracklistText.screenCenter(X);
tracklistText.x -= FlxG.width * 0.35;
var levelScore:Null<SaveScoreData> = Save.get().getLevelScore(currentLevelId, currentDifficultyId);
highScore = levelScore?.score ?? 0;
// levelScore.accuracy
}
}