From 5ff546baccf803127087faea055537dd0a1e5b05 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 26 Jul 2023 16:52:58 -0400 Subject: [PATCH] First iteration of song playtesting from editor! --- .../transition/FlxTransitionableSubState.hx | 234 ++++++++++++++++++ source/funkin/Controls.hx | 1 - source/funkin/FreeplayState.hx | 7 + source/funkin/MainMenuState.hx | 1 - source/funkin/MusicBeatState.hx | 4 +- source/funkin/MusicBeatSubState.hx | 21 +- source/funkin/PauseSubState.hx | 25 +- source/funkin/play/PlayState.hx | 110 ++++++-- source/funkin/play/song/Song.hx | 67 +++-- source/funkin/play/song/SongData.hx | 4 +- source/funkin/ui/debug/DebugMenuSubState.hx | 4 + .../charting/ChartEditorDialogHandler.hx | 2 + .../ui/debug/charting/ChartEditorState.hx | 78 +++++- source/funkin/ui/story/StoryMenuState.hx | 7 + source/funkin/util/tools/ArrayTools.hx | 11 + 15 files changed, 520 insertions(+), 56 deletions(-) create mode 100644 source/flixel/addons/transition/FlxTransitionableSubState.hx diff --git a/source/flixel/addons/transition/FlxTransitionableSubState.hx b/source/flixel/addons/transition/FlxTransitionableSubState.hx new file mode 100644 index 000000000..7bb536bb2 --- /dev/null +++ b/source/flixel/addons/transition/FlxTransitionableSubState.hx @@ -0,0 +1,234 @@ +package flixel.addons.transition; + +import flixel.FlxSubState; +import flixel.addons.transition.FlxTransitionableState; + +/** + * A `FlxSubState` which can perform visual transitions + * + * Usage: + * + * First, extend `FlxTransitionableSubState` as ie, `FooState`. + * + * Method 1: + * + * ```haxe + * var in:TransitionData = new TransitionData(...); // add your data where "..." is + * var out:TransitionData = new TransitionData(...); + * + * FlxG.switchState(new FooState(in,out)); + * ``` + * + * Method 2: + * + * ```haxe + * FlxTransitionableSubState.defaultTransIn = new TransitionData(...); + * FlxTransitionableSubState.defaultTransOut = new TransitionData(...); + * + * FlxG.switchState(new FooState()); + * ``` + */ +class FlxTransitionableSubState extends FlxSubState +{ + // global default transitions for ALL states, used if transIn/transOut are null + public static var defaultTransIn(get, set):TransitionData; + + static function get_defaultTransIn():TransitionData + { + return FlxTransitionableState.defaultTransIn; + } + + static function set_defaultTransIn(value:TransitionData):TransitionData + { + return FlxTransitionableState.defaultTransIn = value; + } + + public static var defaultTransOut(get, set):TransitionData; + + static function get_defaultTransOut():TransitionData + { + return FlxTransitionableState.defaultTransOut; + } + + static function set_defaultTransOut(value:TransitionData):TransitionData + { + return FlxTransitionableState.defaultTransOut = value; + } + + public static var skipNextTransIn(get, set):Bool; + + static function get_skipNextTransIn():Bool + { + return FlxTransitionableState.skipNextTransIn; + } + + static function set_skipNextTransIn(value:Bool):Bool + { + return FlxTransitionableState.skipNextTransIn = value; + } + + public static var skipNextTransOut(get, set):Bool; + + static function get_skipNextTransOut():Bool + { + return FlxTransitionableState.skipNextTransOut; + } + + static function set_skipNextTransOut(value:Bool):Bool + { + return FlxTransitionableState.skipNextTransOut = value; + } + + // beginning & ending transitions for THIS state: + public var transIn:TransitionData; + public var transOut:TransitionData; + + public var hasTransIn(get, never):Bool; + public var hasTransOut(get, never):Bool; + + /** + * Create a state with the ability to do visual transitions + * @param TransIn Plays when the state begins + * @param TransOut Plays when the state ends + */ + public function new(?TransIn:TransitionData, ?TransOut:TransitionData) + { + transIn = TransIn; + transOut = TransOut; + + if (transIn == null && defaultTransIn != null) + { + transIn = defaultTransIn; + } + if (transOut == null && defaultTransOut != null) + { + transOut = defaultTransOut; + } + super(); + } + + override function destroy():Void + { + super.destroy(); + transIn = null; + transOut = null; + _onExit = null; + } + + override function create():Void + { + super.create(); + transitionIn(); + } + + override function startOutro(onOutroComplete:() -> Void) + { + if (!hasTransOut) onOutroComplete(); + else if (!_exiting) + { + // play the exit transition, and when it's done call FlxG.switchState + _exiting = true; + transitionOut(onOutroComplete); + + if (skipNextTransOut) + { + skipNextTransOut = false; + finishTransOut(); + } + } + } + + /** + * Starts the in-transition. Can be called manually at any time. + */ + public function transitionIn():Void + { + if (transIn != null && transIn.type != NONE) + { + if (skipNextTransIn) + { + skipNextTransIn = false; + if (finishTransIn != null) + { + finishTransIn(); + } + return; + } + + var _trans = createTransition(transIn); + + _trans.setStatus(FULL); + openSubState(_trans); + + _trans.finishCallback = finishTransIn; + _trans.start(OUT); + } + } + + /** + * Starts the out-transition. Can be called manually at any time. + */ + public function transitionOut(?OnExit:Void->Void):Void + { + _onExit = OnExit; + if (hasTransOut) + { + var _trans = createTransition(transOut); + + _trans.setStatus(EMPTY); + openSubState(_trans); + + _trans.finishCallback = finishTransOut; + _trans.start(IN); + } + else + { + _onExit(); + } + } + + var transOutFinished:Bool = false; + + var _exiting:Bool = false; + var _onExit:Void->Void; + + function get_hasTransIn():Bool + { + return transIn != null && transIn.type != NONE; + } + + function get_hasTransOut():Bool + { + return transOut != null && transOut.type != NONE; + } + + function createTransition(data:TransitionData):Transition + { + return switch (data.type) + { + case TILES: new Transition(data); + case FADE: new Transition(data); + default: null; + } + } + + function finishTransIn() + { + closeSubState(); + } + + function finishTransOut() + { + transOutFinished = true; + + if (!_exiting) + { + closeSubState(); + } + + if (_onExit != null) + { + _onExit(); + } + } +} diff --git a/source/funkin/Controls.hx b/source/funkin/Controls.hx index 88b637e72..e8a66cb14 100644 --- a/source/funkin/Controls.hx +++ b/source/funkin/Controls.hx @@ -16,7 +16,6 @@ import flixel.input.keyboard.FlxKey; import flixel.input.mouse.FlxMouseButton.FlxMouseButtonID; import flixel.math.FlxAngle; import flixel.math.FlxPoint; -import flixel.ui.FlxVirtualPad; import flixel.util.FlxColor; import flixel.util.FlxTimer; import lime.ui.Haptic; diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index 608898a5f..a86bfe8cc 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -890,6 +890,13 @@ class FreeplayState extends MusicBeatSubState FlxG.sound.play(Paths.sound('confirmMenu')); dj.confirm(); + if (targetSong != null) + { + // Load and cache the song's charts. + // TODO: Do this in the loading state. + targetSong.cacheCharts(true); + } + new FlxTimer().start(1, function(tmr:FlxTimer) { LoadingState.loadAndSwitchState(new PlayState( { diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx index 2c251635c..bca20980c 100644 --- a/source/funkin/MainMenuState.hx +++ b/source/funkin/MainMenuState.hx @@ -12,7 +12,6 @@ import flixel.input.touch.FlxTouch; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; -import flixel.ui.FlxButton; import flixel.util.FlxColor; import flixel.util.FlxTimer; import funkin.NGio; diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx index 20330f257..9a986a8b5 100644 --- a/source/funkin/MusicBeatState.hx +++ b/source/funkin/MusicBeatState.hx @@ -3,7 +3,7 @@ package funkin; import funkin.modding.IScriptedClass.IEventHandler; import flixel.FlxState; import flixel.FlxSubState; -import flixel.addons.ui.FlxUIState; +import flixel.addons.transition.FlxTransitionableState; import flixel.text.FlxText; import flixel.util.FlxColor; import flixel.util.FlxSort; @@ -16,7 +16,7 @@ import funkin.util.SortUtil; * MusicBeatState actually represents the core utility FlxState of the game. * It includes functionality for event handling, as well as maintaining BPM-based update events. */ -class MusicBeatState extends FlxUIState implements IEventHandler +class MusicBeatState extends FlxTransitionableState implements IEventHandler { var controls(get, never):Controls; diff --git a/source/funkin/MusicBeatSubState.hx b/source/funkin/MusicBeatSubState.hx index 244d2ceea..1958c6074 100644 --- a/source/funkin/MusicBeatSubState.hx +++ b/source/funkin/MusicBeatSubState.hx @@ -1,24 +1,28 @@ package funkin; +import flixel.addons.transition.FlxTransitionableSubState; import flixel.FlxSubState; -import funkin.modding.IScriptedClass.IEventHandler; +import flixel.text.FlxText; import flixel.util.FlxColor; import funkin.modding.events.ScriptEvent; +import funkin.modding.IScriptedClass.IEventHandler; import funkin.modding.module.ModuleHandler; -import flixel.text.FlxText; import funkin.modding.PolymodHandler; +import funkin.util.SortUtil; +import flixel.util.FlxSort; /** * MusicBeatSubState reincorporates the functionality of MusicBeatState into an FlxSubState. */ -class MusicBeatSubState extends FlxSubState implements IEventHandler +class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandler { public var leftWatermarkText:FlxText = null; public var rightWatermarkText:FlxText = null; public function new(bgColor:FlxColor = FlxColor.TRANSPARENT) { - super(bgColor); + super(); + this.bgColor = bgColor; } var controls(get, never):Controls; @@ -67,6 +71,15 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler FlxG.resetState(); } + /** + * Refreshes the state, by redoing the render order of all sprites. + * It does this based on the `zIndex` of each prop. + */ + public function refresh() + { + sort(SortUtil.byZIndex, FlxSort.ASCENDING); + } + /** * Called when a step is hit in the current song. * Continues outside of PlayState, for things like animations in menus. diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index d5584fbc7..9133a8fab 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -16,14 +16,17 @@ class PauseSubState extends MusicBeatSubState { var grpMenuShit:FlxTypedGroup; - var pauseOG:Array = [ + var pauseOptionsBase:Array = [ 'Resume', 'Restart Song', 'Change Difficulty', 'Toggle Practice Mode', 'Exit to Menu' ]; - var difficultyChoices:Array = ['EASY', 'NORMAL', 'HARD', 'ERECT', 'BACK']; + + var pauseOptionsDifficulty:Array = ['EASY', 'NORMAL', 'HARD', 'ERECT', 'BACK']; + + var pauseOptionsCharting:Array = ['Resume', 'Restart Song', 'Exit to Chart Editor']; var menuItems:Array = []; var curSelected:Int = 0; @@ -36,11 +39,15 @@ class PauseSubState extends MusicBeatSubState var bg:FlxSprite; var metaDataGrp:FlxTypedGroup; - public function new() + var isChartingMode:Bool; + + public function new(?isChartingMode:Bool = false) { super(); - menuItems = pauseOG; + this.isChartingMode = isChartingMode; + + menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase; if (PlayStatePlaylist.campaignId == 'week6') { @@ -180,14 +187,13 @@ class PauseSubState extends MusicBeatSubState { var daSelected:String = menuItems[curSelected]; - // TODO: Why is this based on the menu item's name? Make this an enum or something. switch (daSelected) { case 'Resume': close(); case 'Change Difficulty': - menuItems = difficultyChoices; + menuItems = pauseOptionsDifficulty; regenMenu(); case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT': @@ -199,7 +205,7 @@ class PauseSubState extends MusicBeatSubState close(); case 'BACK': - menuItems = pauseOG; + menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase; regenMenu(); case 'Toggle Practice Mode': @@ -226,6 +232,11 @@ class PauseSubState extends MusicBeatSubState if (PlayStatePlaylist.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY)); else openSubState(new funkin.ui.StickerSubState(null, FREEPLAY)); + + case 'Exit to Chart Editor': + this.close(); + if (FlxG.sound.music != null) FlxG.sound.music.stop(); + PlayState.instance.close(); // This only works because PlayState is a substate! } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index c0705bd96..cf12db06b 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1,5 +1,6 @@ package funkin.play; +import funkin.ui.debug.charting.ChartEditorState; import haxe.Int64; import funkin.play.notes.notestyle.NoteStyle; import funkin.data.notestyle.NoteStyleData; @@ -77,12 +78,23 @@ typedef PlayStateParams = * @default `bf`, or the first character in the song's character list. */ ?targetCharacter:String, + /** + * Whether the song should start in Practice Mode. + * @default `false` + */ + ?practiceMode:Bool, + /** + * Whether the song should be in minimal mode. + * @default `false` + */ + ?minimalMode:Bool, } /** * The gameplay state, where all the rhythm gaming happens. + * SubState so it can be loaded as a child of the chart editor. */ -class PlayState extends MusicBeatState +class PlayState extends MusicBeatSubState { /** * STATIC VARIABLES @@ -209,6 +221,11 @@ class PlayState extends MusicBeatState */ public var isPracticeMode:Bool = false; + /** + * In Minimal Mode, the stage and characters are not loaded and a standard background is used. + */ + public var isMinimalMode:Bool = false; + /** * Whether the game is currently in an animated cutscene, and gameplay should be stopped. */ @@ -219,6 +236,20 @@ class PlayState extends MusicBeatState */ public var disableKeys:Bool = false; + public var isSubState(get, null):Bool; + + function get_isSubState():Bool + { + return this._parentState != null; + } + + public var isChartingMode(get, null):Bool; + + function get_isChartingMode():Bool + { + return this._parentState != null && Std.isOfType(this._parentState, ChartEditorState); + } + /** * The current dialogue. */ @@ -438,6 +469,8 @@ class PlayState extends MusicBeatState currentSong = params.targetSong; if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty; if (params.targetCharacter != null) currentPlayerId = params.targetCharacter; + isPracticeMode = params.practiceMode ?? false; + isMinimalMode = params.minimalMode ?? false; // Don't do anything else here! Wait until create() when we attach to the camera. } @@ -458,13 +491,6 @@ class PlayState extends MusicBeatState NoteSplash.buildSplashFrames(); - if (currentSong != null) - { - // Load and cache the song's charts. - // TODO: Do this in the loading state. - currentSong.cacheCharts(true); - } - // Returns null if the song failed to load or doesn't have the selected difficulty. if (currentSong == null || currentChart == null) { @@ -490,7 +516,14 @@ class PlayState extends MusicBeatState lime.app.Application.current.window.alert(message, 'Error loading PlayState'); // Force the user back to the main menu. - FlxG.switchState(new MainMenuState()); + if (isSubState) + { + this.close(); + } + else + { + FlxG.switchState(new MainMenuState()); + } return; } @@ -532,8 +565,15 @@ class PlayState extends MusicBeatState // The song is now loaded. We can continue to initialize the play state. initCameras(); initHealthBar(); - initStage(); - initCharacters(); + if (!isMinimalMode) + { + initStage(); + initCharacters(); + } + else + { + initMinimalMode(); + } initStrumlines(); // Initialize the judgements and combo meter. @@ -706,7 +746,7 @@ class PlayState extends MusicBeatState // There is a 1/1000 change to use a special pause menu. // This prevents the player from resuming, but that's the point. // It's a reference to Gitaroo Man, which doesn't let you pause the game. - if (event.gitaroo) + if (!isSubState && event.gitaroo) { FlxG.switchState(new GitarooPause( { @@ -725,7 +765,7 @@ class PlayState extends MusicBeatState boyfriendPos = currentStage.getBoyfriend().getScreenPosition(); } - var pauseSubState:FlxSubState = new PauseSubState(); + var pauseSubState:FlxSubState = new PauseSubState(isChartingMode); openSubState(pauseSubState); pauseSubState.camera = camHUD; @@ -1202,6 +1242,19 @@ class PlayState extends MusicBeatState loadStage(currentStageId); } + function initMinimalMode():Void + { + // Create the green background. + var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat')); + menuBG.color = 0xFF4CAF50; + menuBG.setGraphicSize(Std.int(menuBG.width * 1.1)); + menuBG.updateHitbox(); + menuBG.screenCenter(); + menuBG.scrollFactor.set(0, 0); + menuBG.zIndex = -1000; + add(menuBG); + } + /** * Loads stage data from cache, assembles the props, * and adds it to the state. @@ -2132,6 +2185,7 @@ class PlayState extends MusicBeatState if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible; #end + // Eject button if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState()); if (FlxG.keys.justPressed.F5) debug_refreshModules(); @@ -2163,7 +2217,10 @@ class PlayState extends MusicBeatState } // 8: Move to the offset editor. - if (FlxG.keys.justPressed.EIGHT) FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState()); + if (FlxG.keys.justPressed.EIGHT) + { + lime.app.Application.current.window.alert("Press ~ on the main menu to get to the editor", 'LOL'); + } // 9: Toggle the old icon. if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon(); @@ -2384,7 +2441,14 @@ class PlayState extends MusicBeatState // FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked; FlxG.save.flush(); - moveToResultsScreen(); + if (isSubState) + { + this.close(); + } + else + { + moveToResultsScreen(); + } } else { @@ -2438,10 +2502,23 @@ class PlayState extends MusicBeatState } else { - moveToResultsScreen(); + if (isSubState) + { + this.close(); + } + else + { + moveToResultsScreen(); + } } } + public override function close():Void + { + performCleanup(); + super.close(); + } + /** * Perform necessary cleanup before leaving the PlayState. */ @@ -2552,6 +2629,7 @@ class PlayState extends MusicBeatState FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04); FlxG.camera.targetOffset.set(); FlxG.camera.zoom = defaultCameraZoom; + // Snap the camera to the follow point immediately. FlxG.camera.focusOn(cameraFollowPoint.getPosition()); } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 4cbf1ade3..a8f004520 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -56,6 +56,32 @@ class Song implements IPlayStateScriptedClass populateFromMetadata(); } + @:allow(funkin.play.song.Song) + public static function buildRaw(songId:String, metadata:Array, variations:Array, charts:Map, + ?validScore:Bool = false):Song + { + var result:Song = new Song(songId); + + result._metadata.clear(); + for (meta in metadata) + result._metadata.push(meta); + + result.variations.clear(); + for (vari in variations) + result.variations.push(vari); + + result.difficultyIds.clear(); + + result.populateFromMetadata(); + + for (variation => chartData in charts) + result.applyChartData(chartData, variation); + + result.validScore = validScore; + + return result; + } + public function getRawMetadata():Array { return _metadata; @@ -119,28 +145,33 @@ class Song implements IPlayStateScriptedClass for (variation in variations) { var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation); - var chartNotes = chartData.notes; - - for (diffId in chartNotes.keys()) - { - // Retrieve the cached difficulty data. - var difficulty:Null = difficulties.get(diffId); - if (difficulty == null) - { - trace('Fabricated new difficulty for $diffId.'); - difficulty = new SongDifficulty(this, diffId, variation); - difficulties.set(diffId, difficulty); - } - // Add the chart data to the difficulty. - difficulty.notes = chartData.notes.get(diffId); - difficulty.scrollSpeed = chartData.getScrollSpeed(diffId); - - difficulty.events = chartData.events; - } + applyChartData(chartData, variation); } trace('Done caching charts.'); } + function applyChartData(chartData:SongChartData, variation:String):Void + { + var chartNotes = chartData.notes; + + for (diffId in chartNotes.keys()) + { + // Retrieve the cached difficulty data. + var difficulty:Null = difficulties.get(diffId); + if (difficulty == null) + { + trace('Fabricated new difficulty for $diffId.'); + difficulty = new SongDifficulty(this, diffId, variation); + difficulties.set(diffId, difficulty); + } + // Add the chart data to the difficulty. + difficulty.notes = chartData.notes.get(diffId); + difficulty.scrollSpeed = chartData.getScrollSpeed(diffId); + + difficulty.events = chartData.events; + } + } + /** * Retrieve the metadata for a specific difficulty, including the chart if it is loaded. * @param diffId The difficulty ID, such as `easy` or `hard`. diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index c2a701ce9..740fec9d1 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -202,7 +202,7 @@ class SongDataParser static function loadMusicMetadataFile(musicPath:String, variation:String = ''):String { - var musicMetadataFilePath:String = (variation != '') ? Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata-$variation.json') : Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata.json'); + var musicMetadataFilePath:String = (variation != '' || variation == "default") ? Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata-$variation.json') : Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata.json'); var rawJson:String = Assets.getText(musicMetadataFilePath).trim(); @@ -238,7 +238,7 @@ class SongDataParser static function loadSongChartDataFile(songPath:String, variation:String = ''):String { - var songChartDataFilePath:String = (variation != '') ? Paths.json('$SONG_DATA_PATH$songPath/$songPath-chart-$variation') : Paths.json('$SONG_DATA_PATH$songPath/$songPath-chart'); + var songChartDataFilePath:String = (variation != '' || variation == 'default') ? Paths.json('$SONG_DATA_PATH$songPath/$songPath-chart-$variation') : Paths.json('$SONG_DATA_PATH$songPath/$songPath-chart'); var rawJson:String = Assets.getText(songChartDataFilePath).trim(); diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index 9f18acd35..7ef4cb238 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -1,5 +1,6 @@ package funkin.ui.debug; +import flixel.math.FlxPoint; import flixel.FlxObject; import flixel.FlxSprite; import funkin.MusicBeatSubState; @@ -48,6 +49,9 @@ class DebugMenuSubState extends MusicBeatSubState createItem("ANIMATION EDITOR", openAnimationEditor); createItem("STAGE EDITOR", openStageEditor); createItem("TEST STICKERS", testStickers); + + FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y)); + FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y + 500)); } function onMenuChange(selected:TextMenuItem) diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index 9453c8c94..2e8cc78d4 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -580,6 +580,8 @@ class ChartEditorDialogHandler state.isHaxeUIDialogOpen = false; }; + dialog.zIndex = 1000; + return dialog; } } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 2238fff3f..a3fcd0f22 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -2,7 +2,9 @@ package funkin.ui.debug.charting; import flixel.addons.display.FlxSliceSprite; import flixel.addons.display.FlxTiledSprite; +import flixel.FlxCamera; import flixel.FlxSprite; +import flixel.FlxSubState; import flixel.group.FlxSpriteGroup; import flixel.input.keyboard.FlxKey; import flixel.math.FlxPoint; @@ -20,6 +22,7 @@ import funkin.modding.events.ScriptEvent; import funkin.play.HealthIcon; import funkin.play.notes.NoteSprite; import funkin.play.notes.Strumline; +import funkin.play.PlayState; import funkin.play.song.Song; import funkin.play.song.SongData.SongChartData; import funkin.play.song.SongData.SongDataParser; @@ -977,6 +980,13 @@ class ChartEditorState extends HaxeUIState override function create():Void { + // super.create() must be called first, the HaxeUI components get created here. + super.create(); + // Set the z-index of the HaxeUI. + this.component.zIndex = 100; + + fixCamera(); + // Get rid of any music from the previous state. FlxG.sound.music.stop(); @@ -989,8 +999,6 @@ class ChartEditorState extends HaxeUIState buildGrid(); buildSelectionBox(); - // Add the HaxeUI components after the grid so they're on top. - super.create(); buildAdditionalUI(); // Setup the onClick listeners for the UI after it's been created. @@ -999,6 +1007,8 @@ class ChartEditorState extends HaxeUIState setupAutoSave(); + refresh(); + ChartEditorDialogHandler.openWelcomeDialog(this, false); } @@ -1028,6 +1038,7 @@ class ChartEditorState extends HaxeUIState menuBG.updateHitbox(); menuBG.screenCenter(); menuBG.scrollFactor.set(0, 0); + menuBG.zIndex = -100; } /** @@ -1039,28 +1050,33 @@ class ChartEditorState extends HaxeUIState gridTiledSprite.x = FlxG.width / 2 - GRID_SIZE * STRUMLINE_SIZE; // Center the grid. gridTiledSprite.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; // Push down to account for the menu bar. add(gridTiledSprite); + gridTiledSprite.zIndex = 10; gridGhostNote = new ChartEditorNoteSprite(this); gridGhostNote.alpha = 0.6; gridGhostNote.noteData = new SongNoteData(0, 0, 0, ""); gridGhostNote.visible = false; add(gridGhostNote); + gridGhostNote.zIndex = 11; gridGhostEvent = new ChartEditorEventSprite(this); gridGhostEvent.alpha = 0.6; gridGhostEvent.eventData = new SongEventData(-1, "", {}); gridGhostEvent.visible = false; add(gridGhostEvent); + gridGhostEvent.zIndex = 12; buildNoteGroup(); gridPlayheadScrollArea = new FlxSprite(gridTiledSprite.x - PLAYHEAD_SCROLL_AREA_WIDTH, MENU_BAR_HEIGHT).makeGraphic(PLAYHEAD_SCROLL_AREA_WIDTH, FlxG.height - MENU_BAR_HEIGHT, PLAYHEAD_SCROLL_AREA_COLOR); add(gridPlayheadScrollArea); + gridPlayheadScrollArea.zIndex = 25; // The playhead that show the current position in the song. gridPlayhead = new FlxSpriteGroup(); add(gridPlayhead); + gridPlayhead.zIndex = 30; var playheadWidth = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); var playheadBaseYPos = MENU_BAR_HEIGHT + GRID_TOP_PAD; @@ -1082,6 +1098,7 @@ class ChartEditorState extends HaxeUIState healthIconDad.x = gridTiledSprite.x - 15 - (HealthIcon.HEALTH_ICON_SIZE * 0.5); healthIconDad.y = gridTiledSprite.y + 5; add(healthIconDad); + healthIconDad.zIndex = 30; healthIconBF = new HealthIcon('bf'); healthIconBF.autoUpdate = false; @@ -1090,12 +1107,14 @@ class ChartEditorState extends HaxeUIState healthIconBF.y = gridTiledSprite.y + 5; healthIconBF.flipX = true; add(healthIconBF); + healthIconBF.zIndex = 30; } function buildSelectionBox():Void { selectionBoxSprite.scrollFactor.set(0, 0); add(selectionBoxSprite); + selectionBoxSprite.zIndex = 30; setSelectionBoxBounds(); } @@ -1140,18 +1159,22 @@ class ChartEditorState extends HaxeUIState renderedHoldNotes = new FlxTypedSpriteGroup(); renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedHoldNotes); + renderedHoldNotes.zIndex = 24; renderedNotes = new FlxTypedSpriteGroup(); renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedNotes); + renderedNotes.zIndex = 25; renderedEvents = new FlxTypedSpriteGroup(); renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedEvents); + renderedNotes.zIndex = 25; renderedSelectionSquares = new FlxTypedSpriteGroup(); renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedSelectionSquares); + renderedNotes.zIndex = 26; } var playbarHeadLayout:Component; @@ -1159,6 +1182,7 @@ class ChartEditorState extends HaxeUIState function buildAdditionalUI():Void { playbarHeadLayout = buildComponent(CHART_EDITOR_PLAYBARHEAD_LAYOUT); + playbarHeadLayout.zIndex = 110; playbarHeadLayout.width = FlxG.width - 8; playbarHeadLayout.height = 10; @@ -1271,6 +1295,9 @@ class ChartEditorState extends HaxeUIState // addUIClickListener('menubarItemSelectBeforeCursor', _ -> doSomething()); // addUIClickListener('menubarItemSelectAfterCursor', _ -> doSomething()); + addUIClickListener('menubarItemPlaytestFull', _ -> testSongInPlayState(false)); + addUIClickListener('menubarItemPlaytestMinimal', _ -> testSongInPlayState(true)); + addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this)); addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this)); @@ -1423,6 +1450,7 @@ class ChartEditorState extends HaxeUIState handleFileKeybinds(); handleEditKeybinds(); handleViewKeybinds(); + handleTestKeybinds(); handleHelpKeybinds(); // DEBUG @@ -2536,6 +2564,18 @@ class ChartEditorState extends HaxeUIState */ function handleViewKeybinds():Void {} + /** + * Handle keybinds for the Test menu items. + */ + function handleTestKeybinds():Void + { + if (FlxG.keys.justPressed.ENTER) + { + var minimal = FlxG.keys.pressed.SHIFT; + testSongInPlayState(minimal); + } + } + /** * Handle keybinds for Help menu items. */ @@ -2907,9 +2947,9 @@ class ChartEditorState extends HaxeUIState function startAudioPlayback():Void { - if (audioInstTrack != null) audioInstTrack.play(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(); + if (audioInstTrack != null) audioInstTrack.play(false, audioInstTrack.time); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time); setComponentText('playbarPlay', '||'); } @@ -3004,6 +3044,34 @@ class ChartEditorState extends HaxeUIState return this.scrollPositionInPixels; } + /** + * Transitions to the Play State to test the song + */ + public function testSongInPlayState(?minimal:Bool = false):Void + { + var targetSong:Song = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); + + subStateClosed.add(fixCamera); + + openSubState(new PlayState( + { + targetSong: targetSong, + targetDifficulty: selectedDifficulty, + // TODO: Add this. + // targetCharacter: targetCharacter, + practiceMode: true, + minimalMode: minimal, + })); + } + + function fixCamera(_:FlxSubState = null):Void + { + FlxG.cameras.reset(new FlxCamera()); + FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2)); + + add(this.component); + } + /** * Loads an instrumental from an absolute file path, replacing the current instrumental. * diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 12b0f58c5..29bc4beca 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -513,6 +513,13 @@ class StoryMenuState extends MusicBeatState PlayStatePlaylist.campaignId = currentLevel.id; PlayStatePlaylist.campaignTitle = currentLevel.getTitle(); + if (targetSong != null) + { + // Load and cache the song's charts. + // TODO: Do this in the loading state. + targetSong.cacheCharts(true); + } + new FlxTimer().start(1, function(tmr:FlxTimer) { LoadingState.loadAndSwitchState(new PlayState( { diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx index c27f1bf43..67cc1c041 100644 --- a/source/funkin/util/tools/ArrayTools.hx +++ b/source/funkin/util/tools/ArrayTools.hx @@ -37,4 +37,15 @@ class ArrayTools } return null; } + + /** + * Remove all elements from the array, without creating a new array. + * @param array The array to clear. + */ + public static function clear(array:Array):Void + { + // This method is faster than array.splice(0, array.length) + while (array.length > 0) + array.pop(); + } }