Files
CodenameEngine/source/funkin/menus/FreeplayState.hx
2026-01-21 18:17:48 +01:00

659 lines
20 KiB
Haxe

package funkin.menus;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.backend.chart.Chart;
import funkin.backend.chart.ChartData.ChartMetaData;
import funkin.backend.scripting.events.menu.MenuChangeEvent;
import funkin.backend.scripting.events.menu.freeplay.*;
import funkin.backend.system.Conductor;
import funkin.game.HealthIcon;
import funkin.savedata.FunkinSave;
import haxe.io.Path;
using StringTools;
class FreeplayState extends MusicBeatState
{
/**
* Array containing all of the songs' metadata.
*/
public var songs:Array<ChartMetaData> = [];
/**
* Current song metadata
*/
public var curSong:Null<ChartMetaData>;
/**
* Current song difficulties
*/
public var curDifficulties:Array<String>;
/**
* songs[curSelected].metas.get(curDiffMetaIndices[curDifficulty])
*/
public var curDiffMetaKeys:Array<String> = [];
/*
* Array containing all labels for game modes.
*/
public var gameModeLabels:Array<FreeplayGameMode> = [];
/**
* Currently selected song
*/
public var curSelected:Int = 0;
/**
* Currently selected difficulty
*/
public var curDifficulty:Int = 1;
/**
* Currently selected game mode
*/
public var curGameMode:Int = 0;
/**
* Text containing the score info (PERSONAL BEST: 0)
*/
public var scoreText:FlxText;
/**
* Text containing the current difficulty (< HARD >)
*/
public var diffText:FlxText;
/**
* Text containing the current game mode, for example: ([KEYBINDS] Co-Op mode)
*/
public var gameModeText:FlxText;
/**
* Currently lerped score. Is updated to go towards `intendedScore`.
*/
public var lerpScore:Float = 0;
/**
* Destination for the currently lerped score.
*/
public var intendedScore:Int = 0;
/**
* Assigned FreeplaySonglist item.
*/
public var songList:FreeplaySonglist;
/**
* Black background around the score, the difficulty text and the co-op text.
*/
public var scoreBG:FlxSprite;
/**
* Background.
*/
public var bg:FlxSprite;
/**
* Whenever the player can navigate and select
*/
public var canSelect:Bool = true;
/**
* Group containing all of the alphabets
*/
public var grpSongs:FlxTypedGroup<Alphabet>;
/**
* Whenever the currently selected song is playing.
*/
public var curPlaying:Bool = false;
/**
* Array containing all of the icons.
*/
public var iconArray:Array<HealthIcon> = [];
/**
* FlxInterpolateColor object for smooth transition between Freeplay colors.
*/
public var interpColor:FlxInterpolateColor;
override function create()
{
CoolUtil.playMenuSong();
songList = FreeplaySonglist.get();
songs = songList.songs;
for(k=>s in songs) {
if (s.name == Options.freeplayLastSong) {
curSelected = k;
}
}
updateCurDifficulties();
for(i=>diff in curDifficulties) {
if (curDiffMetaKeys[i] == Options.freeplayLastVariation && diff == Options.freeplayLastDifficulty)
curDifficulty = i;
}
updateCurSong();
gameModeLabels = FreeplayGameMode.get();
DiscordUtil.call("onMenuLoaded", ["Freeplay"]);
super.create();
// LOAD CHARACTERS
bg = new FlxSprite(0, 0).loadAnimatedGraphic(Paths.image('menus/menuDesat'));
if (songs.length > 0)
bg.color = songs[0].color;
bg.antialiasing = true;
add(bg);
grpSongs = new FlxTypedGroup<Alphabet>();
add(grpSongs);
for (i in 0...songs.length)
{
var songText:Alphabet = new Alphabet(0, (70 * i) + 30, songs[i].displayName, "bold");
songText.isMenuItem = true;
songText.targetY = i;
grpSongs.add(songText);
var icon:HealthIcon = new HealthIcon(songs[i].icon);
icon.sprTracker = songText;
if (Math.max(icon.width, icon.height) > 150) icon.setUnstretchedGraphicSize(150, 150);
// using a FlxGroup is too much fuss!
iconArray.push(icon);
add(icon);
// songText.x += 40;
// DON'T PUT X IN THE FIRST PARAMETER OF new ALPHABET() !!
// songText.screenCenter(X);
}
scoreText = new FlxText(FlxG.width * 0.7, 5, 0, "", 32);
scoreText.setFormat(Paths.font("vcr.ttf"), 32, FlxColor.WHITE, RIGHT);
scoreBG = new FlxSprite(scoreText.x - 6, 0).makeGraphic(1, 1, 0xFF000000);
scoreBG.alpha = 0.6;
add(scoreBG);
diffText = new FlxText(scoreText.x, scoreText.y + 36, 0, "", 24);
diffText.font = scoreText.font;
add(diffText);
gameModeText = new FlxText(diffText.x, diffText.y + diffText.height + 2, 0, "", 24);
gameModeText.font = scoreText.font;
add(gameModeText);
add(scoreText);
changeSelection(0, true);
changeDiff(0, true);
changeGameMode(0, true);
interpColor = new FlxInterpolateColor(bg.color);
}
#if PRELOAD_ALL
/**
* How much time a song stays selected until it autoplays.
*/
public var timeUntilAutoplay:Float = 1;
/**
* Whenever the song autoplays when hovered over.
*/
public var disableAutoPlay:Bool = false;
/**
* Whenever the autoplayed song gets async loaded.
*/
public var disableAsyncLoading:Bool = #if desktop false #else true #end;
/**
* Time elapsed since last autoplay. If this time exceeds `timeUntilAutoplay`, the currently selected song will play.
*/
public var autoplayElapsed:Float = 0;
/**
* Whenever the currently selected song instrumental is playing.
*/
public var songInstPlaying:Bool = true;
/**
* Path to the currently playing song instrumental.
*/
public var curPlayingInst:String = null;
/**
* If it should play the song automatically.
*/
public var autoplayShouldPlay:Bool = true;
#end
private var TEXT_FREEPLAY_SCORE = TU.getRaw("freeplay.score");
override function update(elapsed:Float)
{
super.update(elapsed);
if (FlxG.sound.music != null && FlxG.sound.music.volume < 0.7)
{
FlxG.sound.music.volume += 0.5 * elapsed;
}
lerpScore = lerp(lerpScore, intendedScore, 0.4);
if (Math.abs(lerpScore - intendedScore) <= 10)
lerpScore = intendedScore;
if (canSelect) {
changeSelection((controls.UP_P ? -1 : 0) + (controls.DOWN_P ? 1 : 0) - FlxG.mouse.wheel);
changeDiff((controls.LEFT_P ? -1 : 0) + (controls.RIGHT_P ? 1 : 0));
changeGameMode((controls.CHANGE_MODE ? 1 : 0));
// putting it before so that its actually smooth
updateOptionsAlpha();
}
scoreText.text = TEXT_FREEPLAY_SCORE.format([Math.round(lerpScore)]);
scoreBG.scale.set(MathUtil.maxSmart(diffText.width, scoreText.width, gameModeText.width) + 8, (gameModeText.visible ? gameModeText.y + gameModeText.height : 66));
scoreBG.updateHitbox();
scoreBG.x = FlxG.width - scoreBG.width;
scoreText.x = gameModeText.x = scoreBG.x + 4;
diffText.x = Std.int(scoreBG.x + ((scoreBG.width - diffText.width) / 2));
interpColor.fpsLerpTo(curSong.color, 0.0625);
bg.color = interpColor.color;
#if PRELOAD_ALL
var dontPlaySongThisFrame = false;
autoplayElapsed += elapsed;
if (!disableAutoPlay && !songInstPlaying && (autoplayElapsed > timeUntilAutoplay)) {
if (curPlayingInst != (curPlayingInst = Paths.inst(curSong.name, curDifficulties[curDifficulty], curSong.instSuffix))) {
var streamed = false;
if (Options.streamedMusic) {
var sound = Assets.getMusic(curPlayingInst, true, false);
streamed = sound != null;
if (streamed && autoplayShouldPlay) {
FlxG.sound.playMusic(sound, 0);
Conductor.changeBPM(curSong.bpm, curSong.beatsPerMeasure, curSong.stepsPerBeat);
}
}
if (!streamed) {
var huh:Void->Void = function() {
var soundPath = curPlayingInst;
var sound = null;
if (Assets.exists(soundPath, SOUND) || Assets.exists(soundPath, MUSIC))
sound = Assets.getSound(soundPath);
else
FlxG.log.error('Could not find a Sound asset with an ID of \'$soundPath\'.');
if (sound != null && autoplayShouldPlay) {
FlxG.sound.playMusic(sound, 0);
Conductor.changeBPM(curSong.bpm, curSong.beatsPerMeasure, curSong.stepsPerBeat);
}
}
if (!disableAsyncLoading) Main.execAsync(huh);
else huh();
}
}
songInstPlaying = true;
if (disableAsyncLoading && !Options.streamedMusic) dontPlaySongThisFrame = true;
}
#end
if (controls.BACK)
{
CoolUtil.playMenuSFX(CANCEL, 0.7);
FlxG.switchState(new MainMenuState());
}
#if sys
if (FlxG.keys.justPressed.EIGHT && Sys.args().contains("-livereload"))
convertChart();
#end
if (controls.ACCEPT #if PRELOAD_ALL && !dontPlaySongThisFrame #end)
select();
}
/**
* Selects the current song.
*/
public function select() {
if (curDifficulties.length == 0) return;
var event = event("onSelect", EventManager.get(FreeplaySongSelectEvent).recycle(curSong.name, curDifficulties[curDifficulty], curSong.variant, gameModeLabels[curGameMode]));
if (event.cancelled) return;
#if PRELOAD_ALL
autoplayShouldPlay = false;
#end
Options.freeplayLastSong = curSong.name;
Options.freeplayLastDifficulty = curDifficulties[curDifficulty];
Options.freeplayLastVariation = curSong.variant;
PlayState.loadSong(event.song, event.difficulty, event.variant, event.opponentMode, event.coopMode);
FlxG.switchState(new PlayState());
}
public function convertChart() {
trace('Converting ${curSong.name} ${curDifficulties[curDifficulty]} ${curSong.variant} to Codename format...');
var chart = Chart.parse(curSong.name, curDifficulties[curDifficulty], curSong.variant);
Chart.save(chart, curDifficulties[curDifficulty], curSong.variant);
}
/**
* Changes the current difficulty
* @param change How much to change.
* @param force Force the change if `change` is equal to 0
*/
public function changeDiff(change:Int = 0, force:Bool = false) {
if (change == 0 && !force) return;
var validDifficulties = curDifficulties.length > 0;
var event = event("onChangeDiff", EventManager.get(MenuChangeEvent).recycle(curDifficulty, validDifficulties ? FlxMath.wrap(curDifficulty + change, 0, curDifficulties.length-1) : 0, change));
if (event.cancelled) {
if (force) updateCurSong();
return;
}
var prevSong = curSong;
curDifficulty = event.value;
updateCurSong();
updateScore();
#if PRELOAD_ALL
if (curSong != prevSong) {
autoplayElapsed = 0;
songInstPlaying = false;
}
#end
var text = validDifficulties ? curDifficulties[curDifficulty].toUpperCase() + (curSong != songs[curSelected] ? ' (${curSong.variant.toUpperCase()})' : '') : '-';
diffText.text = curDifficulties.length > 1 ? '< $text >' : text;
}
function updateScore() {
if (curDifficulties.length == 0) {
intendedScore = 0;
return;
}
var changes:Array<HighscoreChange> = [];
if (__coopMode) changes.push(CCoopMode);
if (__opponentMode) changes.push(COpponentMode);
var saveData = FunkinSave.getSongHighscore(curSong.name, curDifficulties[curDifficulty], curSong.variant, changes);
intendedScore = saveData.score;
}
/**
* Change the current coop mode context.
* @param change How much to change
* @param force Force the change, even if `change` is equal to 0.
*/
public function changeGameMode(change:Int = 0, force:Bool = false) {
if (change == 0 && !force) return;
var i = 0;
var allowed = getAllowedGameModesID();
var wrapped = FlxMath.wrap(curGameMode + change, 0, gameModeLabels.length - 1);
while (!allowed.contains(gameModeLabels[wrapped].modeID)) { // Skipping the blacklisted ones - Nex
wrapped = FlxMath.wrap(wrapped + change, 0, gameModeLabels.length - 1);
if (i++ > allowed.length) {
curGameMode = 0; // Idk honestly if I should modify the other event's variables - Nex
break;
}
}
var e = EventManager.get(MenuChangeEvent).recycle(curGameMode, wrapped, change);
event("onChangeCoopMode", e); // Backwards compat - Nex
if (event("onChangeGameMode", e).cancelled) return;
// Getting from scratch the allowed game modes just in case they changed when the event got called - Nex
if (gameModeText.visible = getAllowedGameModesID().length > 0) gameModeText.text = "[TAB] " + gameModeLabels[curGameMode = e.value].modeName;
updateScore();
}
public inline function getAllowedGameModesID() {
var excluded = songs[curSelected].excludedGameModes;
return [for (mode in gameModeLabels) if (!excluded.contains(mode.modeID)) mode.modeID];
}
/**
* Change the current selection.
* @param change How much to change
* @param force Force the change, even if `change` is equal to 0.
*/
public function changeSelection(change:Int = 0, force:Bool = false) {
if (change == 0 && !force) return;
var event = event("onChangeSelection", EventManager.get(MenuChangeEvent).recycle(curSelected, FlxMath.wrap(curSelected + change, 0, songs.length-1), change));
if (event.cancelled) return;
curSelected = event.value;
if (event.playMenuSFX) CoolUtil.playMenuSFX(SCROLL, 0.7);
var prevDiff = curDifficulties[curDifficulty], prevVariant = curDiffMetaKeys[curDifficulty];
updateCurDifficulties();
for (i => diff in curDifficulties) if (diff == prevDiff && curDiffMetaKeys[i] == prevVariant) {
curDifficulty = i;
break;
}
changeDiff(0, true);
changeGameMode(0, true);
#if PRELOAD_ALL
autoplayElapsed = 0;
songInstPlaying = false;
#end
}
function updateOptionsAlpha() {
var event = event("onUpdateOptionsAlpha", EventManager.get(FreeplayAlphaUpdateEvent).recycle(0.6, 0.45, 1, 1, 0.25));
if (event.cancelled) return;
final idleAlpha = #if PRELOAD_ALL songInstPlaying ? event.idlePlayingAlpha : #end event.idleAlpha;
final selectedAlpha = #if PRELOAD_ALL songInstPlaying ? event.selectedPlayingAlpha : #end event.selectedAlpha;
for (i in 0...iconArray.length)
iconArray[i].alpha = lerp(iconArray[i].alpha, idleAlpha, event.lerp);
iconArray[curSelected].alpha = selectedAlpha;
for (i => item in grpSongs.members)
{
item.targetY = i - curSelected;
item.alpha = lerp(item.alpha, idleAlpha, event.lerp);
if (item.targetY == 0)
item.alpha = selectedAlpha;
}
}
function updateCurDifficulties() {
curDiffMetaKeys.resize(0);
curDifficulties = songs[curSelected].difficulties.copy();
for (i in 0...curDifficulties.length) curDiffMetaKeys.push(null);
if (songs[curSelected].variants != null) {
var meta:ChartMetaData;
for (variant in songs[curSelected].variants) if ((meta = songs[curSelected].metas.get(variant)) != null) {
curDifficulties = curDifficulties.concat(meta.difficulties);
for (i in 0...meta.difficulties.length) curDiffMetaKeys.push(variant);
}
}
}
function updateCurSong() {
var song = songs[curSelected];
if (song == null) curSong = null;
else if ((curSong = song.metas.get(curDiffMetaKeys[curDifficulty])) == null)
curSong = song;
}
// Backwards compat
@:noCompletion public function changeCoopMode(change:Int = 0, force:Bool = false) return changeGameMode(change, force);
@:noCompletion public var coopText(get, set):FlxText;
@:noCompletion private function get_coopText() return gameModeText;
@:noCompletion private function set_coopText(val:FlxText) return gameModeText = val;
@:noCompletion function updateCoopModes() {}
@:noCompletion var __opponentMode(get, default):Bool = false; // God this is so cursed - Nex
@:noCompletion private function get___opponentMode() return getAllowedGameModesID().contains("codename.opponent");
@:noCompletion var __coopMode(get, default):Bool = false;
@:noCompletion private function get___coopMode() return getAllowedGameModesID().contains("codename.coop");
@:noCompletion var curCoopMode(get, set):Int;
@:noCompletion private function get_curCoopMode() return curGameMode;
@:noCompletion private function set_curCoopMode(val:Int) return curGameMode = val;
@:noCompletion var coopLabels(get, set):Array<String>;
@:noCompletion private function get_coopLabels() return [for(mode in gameModeLabels) mode.modeName];
@:noCompletion private function set_coopLabels(names:Array<String>) {
gameModeLabels = [for(name in names) {
if (name.startsWith("[TAB] ")) name = name.substr("[TAB] ".length);
new FreeplayGameMode(name, switch(name) {
case "Solo": "codename.solo";
case "Opponent Mode": "codename.opponent";
case "Co-Op Mode": "codename.coop";
case "Co-Op Mode (Switched)": "codename.coop-opponent";
default: name.toLowerCase().replace(" ", "-");
}, switch(name) {
case "Solo": ["solo"];
case "Opponent Mode": ["opponent"];
case "Co-Op Mode": ["coop"];
case "Co-Op Mode (Switched)": ["coop-switched", "opponent", "coop"];
default: null;
});
}];
return names;
}
}
/**
* Class used for the Freeplay menu game modes.
*
* **NOTE**: `fields` is useable not just for boolean values!!
*/
class FreeplayGameMode {
public var modeName:String;
public var modeID:String;
public var scripts(default, set):Array<String>;
public var fields(default, set):Dynamic;
@:noCompletion function set_scripts(val:Array<String>) return scripts = (val == null ? [] : val);
@:noCompletion function set_fields(val:Dynamic) return fields = (val == null ? {} : val);
public function new(modeName:String, modeID:String, ?scripts:Array<String>, ?fields:Dynamic) {
this.modeName = modeName;
this.modeID = modeID;
this.scripts = scripts;
this.fields = fields;
}
public function toModifiers():Array<Dynamic> {
var fields = this.fields;
var modifiers:Array<Dynamic> = [];
for (field in {var _ = Reflect.fields(fields); _.sort(Reflect.compare); _;}) {
var val:Dynamic = Reflect.field(fields, field);
if (val != null && val != false) { // allow true, and floats/ints
modifiers.push(field);
modifiers.push(val);
}
}
return modifiers;
}
public static inline function generateDefault() return new FreeplayGameMode("Solo Mode", "solo");
public static inline function getGameModesFromSource(source:AssetSource = BOTH, useTxt:Bool = true):Array<FreeplayGameMode> {
var path = 'data/gamemodes';
var txt = Paths.txt('gamemodes/list');
var list = useTxt && Paths.assetsTree.existsSpecific(txt, "TEXT", source) ? CoolUtil.coolTextFile(txt) : Paths.getFolderContent(path, false, source);
return [for (file in list) {
if (useTxt) file += ".json";
else if (Path.extension(file) != "json") continue;
var meta:FreeplayGameMode = null;
try {
var data = CoolUtil.parseJson(Paths.file(path + "/" + file)); var id = data.modeID;
meta = new FreeplayGameMode(CoolUtil.getDefault(data.displayName, id), id, [Path.withoutExtension(file)].concat(CoolUtil.getDefault(data.scripts, [])), data.fields);
} catch(e) Logs.trace('Failed to load game mode metadata for $file ($path): ${Std.string(e)}', ERROR);
if (meta != null) meta;
}];
}
public static function get(useTxt:Bool = true):Array<FreeplayGameMode> {
var list:Array<FreeplayGameMode>;
switch(Flags.GAME_MODES_LIST_MOD_MODE) {
case 'prepend':
list = getGameModesFromSource(MODS, useTxt).concat(getGameModesFromSource(SOURCE, useTxt));
case 'append':
list = getGameModesFromSource(SOURCE, useTxt).concat(getGameModesFromSource(MODS, useTxt));
case 'override':
list = getGameModesFromSource(BOTH, useTxt);
default /*case 'oneOFtwo'*/:
if ((list = getGameModesFromSource(MODS, useTxt)).length == 0)
list = getGameModesFromSource(SOURCE, useTxt);
}
return list;
}
}
class FreeplaySonglist {
public var songs:Array<ChartMetaData> = [];
public function new() {}
public function getSongsFromSource(source:funkin.backend.assets.AssetSource, useTxt:Bool = true) {
var songsFound:Array<String> = null;
if (useTxt) {
var oldPath = Paths.txt('freeplaySonglist');
var newPath = Paths.txt('config/freeplaySonglist');
if (Paths.assetsTree.existsSpecific(newPath, "TEXT", source)) songsFound = CoolUtil.coolTextFile(newPath);
else if (Paths.assetsTree.existsSpecific(oldPath, "TEXT", source)) {
Logs.warn("data/freeplaySonglist.txt is deprecated and will be removed in the future. Please move the file to data/config/", DARKYELLOW, "FreeplaySonglist");
songsFound = CoolUtil.coolTextFile(oldPath);
}
}
if (songsFound == null) songsFound = Paths.getFolderDirectories("songs", false, source);
if (songsFound.length > 0) {
for (s in songsFound) songs.push(Chart.loadChartMeta(s, source == MODS));
return false;
}
return true;
}
public static function get(useTxt:Bool = true) {
var songList = new FreeplaySonglist();
switch(Flags.SONGS_LIST_MOD_MODE) {
case 'prepend':
songList.getSongsFromSource(MODS, useTxt);
songList.getSongsFromSource(SOURCE, useTxt);
case 'append':
songList.getSongsFromSource(SOURCE, useTxt);
songList.getSongsFromSource(MODS, useTxt);
case 'override':
songList.getSongsFromSource(BOTH, useTxt);
default /*case 'oneOFtwo'*/:
if (songList.getSongsFromSource(MODS, useTxt))
songList.getSongsFromSource(SOURCE, useTxt);
}
return songList;
}
}