diff --git a/assets b/assets index 1f00d2413..75ac8ec25 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 1f00d24134231433180affecc67a617d54169ffa +Subproject commit 75ac8ec2564c9a56e8282b0853091ecd8b4f2dfd diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 399f52498..0ad3c19b8 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -50,11 +50,13 @@ class InitState extends FlxState */ public override function create():Void { + // Setup a bunch of important Flixel stuff. setupShit(); - // loadSaveData(); // Moved to Main.hx // Load player options from save data. + // Flixel has already loaded the save data, so we can just use it. Preferences.init(); + // Load controls from save data. PlayerSettings.init(); @@ -198,8 +200,13 @@ class InitState extends FlxState // // FLIXEL PLUGINS // + // Plugins provide a useful interface for globally active Flixel objects, + // that receive update events regardless of the current state. + // TODO: Move Module behavior to a Flixel plugin. funkin.util.plugins.EvacuateDebugPlugin.initialize(); funkin.util.plugins.ReloadAssetsDebugPlugin.initialize(); + funkin.util.plugins.ScreenshotPlugin.initialize(); + funkin.util.plugins.VolumePlugin.initialize(); funkin.util.plugins.WatchPlugin.initialize(); // @@ -302,15 +309,11 @@ class InitState extends FlxState return; } - // Load and cache the song's charts. - // TODO: Do this in the loading state. - songData.cacheCharts(true); - - LoadingState.loadAndSwitchState(() -> new funkin.play.PlayState( + LoadingState.loadPlayState( { targetSong: songData, targetDifficulty: difficultyId, - })); + }); } /** @@ -336,11 +339,11 @@ class InitState extends FlxState var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId); - LoadingState.loadAndSwitchState(() -> new funkin.play.PlayState( + LoadingState.loadPlayState( { targetSong: targetSong, targetDifficulty: difficultyId, - })); + }); } function defineSong():String diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx index 6b0911ede..039a4c285 100644 --- a/source/funkin/Preferences.hx +++ b/source/funkin/Preferences.hx @@ -20,7 +20,10 @@ class Preferences static function set_naughtyness(value:Bool):Bool { - return Save.get().options.naughtyness = value; + var save = Save.get(); + save.options.naughtyness = value; + save.flush(); + return value; } /** @@ -36,7 +39,10 @@ class Preferences static function set_downscroll(value:Bool):Bool { - return Save.get().options.downscroll = value; + var save = Save.get(); + save.options.downscroll = value; + save.flush(); + return value; } /** @@ -52,7 +58,10 @@ class Preferences static function set_flashingLights(value:Bool):Bool { - return Save.get().options.flashingLights = value; + var save = Save.get(); + save.options.flashingLights = value; + save.flush(); + return value; } /** @@ -68,7 +77,10 @@ class Preferences static function set_zoomCamera(value:Bool):Bool { - return Save.get().options.zoomCamera = value; + var save = Save.get(); + save.options.zoomCamera = value; + save.flush(); + return value; } /** @@ -89,7 +101,10 @@ class Preferences toggleDebugDisplay(value); } - return Save.get().options.debugDisplay = value; + var save = Save.get(); + save.options.debugDisplay = value; + save.flush(); + return value; } /** @@ -107,7 +122,10 @@ class Preferences { if (value != Save.get().options.autoPause) FlxG.autoPause = value; - return Save.get().options.autoPause = value; + var save = Save.get(); + save.options.autoPause = value; + save.flush(); + return value; } public static function init():Void diff --git a/source/funkin/Preloader.hx b/source/funkin/Preloader.hx index 24015be05..2a73d8199 100644 --- a/source/funkin/Preloader.hx +++ b/source/funkin/Preloader.hx @@ -8,6 +8,9 @@ import flash.display.Sprite; import flixel.system.FlxBasePreloader; import openfl.display.Sprite; import funkin.util.CLIUtil; +import openfl.text.TextField; +import openfl.text.TextFormat; +import flixel.system.FlxAssets; @:bitmap("art/preloaderArt.png") class LogoImage extends BitmapData {} @@ -21,12 +24,26 @@ class Preloader extends FlxBasePreloader } var logo:Sprite; + var _text:TextField; override function create():Void { this._width = Lib.current.stage.stageWidth; this._height = Lib.current.stage.stageHeight; + _text = new TextField(); + _text.width = 500; + _text.text = "Loading FNF"; + _text.defaultTextFormat = new TextFormat(FlxAssets.FONT_DEFAULT, 16, 0xFFFFFFFF); + _text.embedFonts = true; + _text.selectable = false; + _text.multiline = false; + _text.wordWrap = false; + _text.autoSize = LEFT; + _text.x = 2; + _text.y = 2; + addChild(_text); + var ratio:Float = this._width / 2560; // This allows us to scale assets depending on the size of the screen. logo = new Sprite(); @@ -34,27 +51,14 @@ class Preloader extends FlxBasePreloader logo.scaleX = logo.scaleY = ratio; logo.x = ((this._width) / 2) - ((logo.width) / 2); logo.y = (this._height / 2) - ((logo.height) / 2); - addChild(logo); // Adds the graphic to the NMEPreloader's buffer. + // addChild(logo); // Adds the graphic to the NMEPreloader's buffer. super.create(); } override function update(Percent:Float):Void { - if (Percent < 69) - { - logo.scaleX += Percent / 1920; - logo.scaleY += Percent / 1920; - logo.x -= Percent * 0.6; - logo.y -= Percent / 2; - } - else - { - logo.scaleX = this._width / 1280; - logo.scaleY = this._width / 1280; - logo.x = ((this._width) / 2) - ((logo.width) / 2); - logo.y = (this._height / 2) - ((logo.height) / 2); - } + _text.text = "FNF: " + Math.round(Percent * 100) + "%"; super.update(Percent); } diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 73ecbce14..bba5f899f 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -898,20 +898,26 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw> /** * The kind of the note. * This can allow the note to include information used for custom behavior. - * Defaults to blank or `Constants.DEFAULT_DIFFICULTY`. + * Defaults to `null` for no kind. */ @:alias("k") - @:default("normal") @:optional - public var kind(get, default):String = ''; + @:isVar + public var kind(get, set):Null<String> = null; - function get_kind():String + function get_kind():Null<String> { - if (this.kind == null || this.kind == '') return 'normal'; + if (this.kind == null || this.kind == '') return null; return this.kind; } + function set_kind(value:Null<String>):Null<String> + { + if (value == '') value = null; + return this.kind = value; + } + public function new(time:Float, data:Int, length:Float = 0, kind:String = '') { this.time = time; @@ -1061,13 +1067,13 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw if (this == null) return other == null; if (other == null) return false; - if (this.kind == '') + if (this.kind == null || this.kind == '') { - if (other.kind != '' && other.kind != 'normal') return false; + if (other.kind != '' && this.kind != null) return false; } else { - if (other.kind == '' || other.kind != this.kind) return false; + if (other.kind == '' || this.kind == null) return false; } return this.time == other.time && this.data == other.data && this.length == other.length; @@ -1082,11 +1088,11 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw if (this.kind == '') { - if (other.kind != '' && other.kind != 'normal') return true; + if (other.kind != '') return true; } else { - if (other.kind == '' || other.kind != this.kind) return true; + if (other.kind == '') return true; } return this.time != other.time || this.data != other.data || this.length != other.length; diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index 01ea2da32..7f3b01eb4 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -210,14 +210,13 @@ class SongDataUtils */ public static function writeItemsToClipboard(data:SongClipboardItems):Void { - var writer = new json2object.JsonWriter<SongClipboardItems>(); + var ignoreNullOptionals = true; + var writer = new json2object.JsonWriter<SongClipboardItems>(ignoreNullOptionals); var dataString:String = writer.write(data, ' '); ClipboardUtil.setClipboard(dataString); trace('Wrote ' + data.notes.length + ' notes and ' + data.events.length + ' events to clipboard.'); - - trace(dataString); } /** diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx index 201c222a3..c4760cf5f 100644 --- a/source/funkin/input/Controls.hx +++ b/source/funkin/input/Controls.hx @@ -63,12 +63,10 @@ class Controls extends FlxActionSet var _debug_menu = new FlxActionDigital(Action.DEBUG_MENU); var _debug_chart = new FlxActionDigital(Action.DEBUG_CHART); var _debug_stage = new FlxActionDigital(Action.DEBUG_STAGE); + var _screenshot = new FlxActionDigital(Action.SCREENSHOT); var _volume_up = new FlxActionDigital(Action.VOLUME_UP); var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN); var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE); - #if CAN_CHEAT - var _cheat = new FlxActionDigital(Action.CHEAT); - #end var byName:Map<String, FlxActionDigital> = new Map<String, FlxActionDigital>(); @@ -235,6 +233,11 @@ class Controls extends FlxActionSet inline function get_DEBUG_STAGE() return _debug_stage.check(); + public var SCREENSHOT(get, never):Bool; + + inline function get_SCREENSHOT() + return _screenshot.check(); + public var VOLUME_UP(get, never):Bool; inline function get_VOLUME_UP() @@ -255,13 +258,6 @@ class Controls extends FlxActionSet inline function get_RESET() return _reset.check(); - #if CAN_CHEAT - public var CHEAT(get, never):Bool; - - inline function get_CHEAT() - return _cheat.check(); - #end - public function new(name, scheme:KeyboardScheme = null) { super(name); @@ -295,13 +291,14 @@ class Controls extends FlxActionSet add(_pause); add(_cutscene_advance); add(_cutscene_skip); + add(_debug_menu); + add(_debug_chart); + add(_debug_stage); + add(_screenshot); add(_volume_up); add(_volume_down); add(_volume_mute); add(_reset); - #if CAN_CHEAT - add(_cheat); - #end for (action in digitalActions) byName[action.name] = action; @@ -391,12 +388,10 @@ class Controls extends FlxActionSet case DEBUG_MENU: _debug_menu; case DEBUG_CHART: _debug_chart; case DEBUG_STAGE: _debug_stage; + case SCREENSHOT: _screenshot; case VOLUME_UP: _volume_up; case VOLUME_DOWN: _volume_down; case VOLUME_MUTE: _volume_mute; - #if CAN_CHEAT - case CHEAT: _cheat; - #end } } @@ -464,6 +459,8 @@ class Controls extends FlxActionSet func(_debug_chart, JUST_PRESSED); case DEBUG_STAGE: func(_debug_stage, JUST_PRESSED); + case SCREENSHOT: + func(_screenshot, JUST_PRESSED); case VOLUME_UP: func(_volume_up, JUST_PRESSED); case VOLUME_DOWN: @@ -472,10 +469,6 @@ class Controls extends FlxActionSet func(_volume_mute, JUST_PRESSED); case RESET: func(_reset, JUST_PRESSED); - #if CAN_CHEAT - case CHEAT: - func(_cheat, JUST_PRESSED); - #end } } @@ -666,6 +659,8 @@ class Controls extends FlxActionSet bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU)); bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART)); bindKeys(Control.DEBUG_STAGE, getDefaultKeybinds(scheme, Control.DEBUG_STAGE)); + bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET)); + bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT)); bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP)); bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN)); bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE)); @@ -693,6 +688,7 @@ class Controls extends FlxActionSet case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_CHART: return []; case Control.DEBUG_STAGE: return []; + case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen case Control.VOLUME_UP: return [PLUS, NUMPADPLUS]; case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS]; case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO]; @@ -716,6 +712,7 @@ class Controls extends FlxActionSet case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_CHART: return []; case Control.DEBUG_STAGE: return []; + case Control.SCREENSHOT: return [PRINTSCREEN]; case Control.VOLUME_UP: return [PLUS]; case Control.VOLUME_DOWN: return [MINUS]; case Control.VOLUME_MUTE: return [ZERO]; @@ -739,6 +736,7 @@ class Controls extends FlxActionSet case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_CHART: return []; case Control.DEBUG_STAGE: return []; + case Control.SCREENSHOT: return [PRINTSCREEN]; case Control.VOLUME_UP: return [NUMPADPLUS]; case Control.VOLUME_DOWN: return [NUMPADMINUS]; case Control.VOLUME_MUTE: return [NUMPADZERO]; @@ -845,6 +843,7 @@ class Controls extends FlxActionSet Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT), Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT), Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE), + // Control.SCREENSHOT => [], // Control.VOLUME_UP => [RIGHT_SHOULDER], // Control.VOLUME_DOWN => [LEFT_SHOULDER], // Control.VOLUME_MUTE => [RIGHT_TRIGGER], @@ -852,8 +851,7 @@ class Controls extends FlxActionSet Control.CUTSCENE_SKIP => getDefaultGamepadBinds(Control.CUTSCENE_SKIP), // Control.DEBUG_MENU // Control.DEBUG_CHART - Control.RESET => getDefaultGamepadBinds(Control.RESET), - #if CAN_CHEAT, Control.CHEAT => getDefaultGamepadBinds(Control.CHEAT) #end + Control.RESET => getDefaultGamepadBinds(Control.RESET) ]); } @@ -870,6 +868,7 @@ class Controls extends FlxActionSet case Control.NOTE_LEFT: return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT]; case Control.NOTE_RIGHT: return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT]; case Control.PAUSE: return [START]; + case Control.SCREENSHOT: return []; case Control.VOLUME_UP: return []; case Control.VOLUME_DOWN: return []; case Control.VOLUME_MUTE: return []; @@ -878,7 +877,6 @@ class Controls extends FlxActionSet case Control.DEBUG_MENU: return []; case Control.DEBUG_CHART: return []; case Control.RESET: return [RIGHT_SHOULDER]; - #if CAN_CHEAT, Control.CHEAT: return [X]; #end default: // Fallthrough. } @@ -1236,6 +1234,8 @@ enum Control // CUTSCENE CUTSCENE_ADVANCE; CUTSCENE_SKIP; + // SCREENSHOT + SCREENSHOT; // VOLUME VOLUME_UP; VOLUME_DOWN; @@ -1244,9 +1244,6 @@ enum Control DEBUG_MENU; DEBUG_CHART; DEBUG_STAGE; - #if CAN_CHEAT - CHEAT; - #end } enum @@ -1289,13 +1286,12 @@ abstract Action(String) to String from String var VOLUME_UP = "volume_up"; var VOLUME_DOWN = "volume_down"; var VOLUME_MUTE = "volume_mute"; + // SCREENSHOT + var SCREENSHOT = "screenshot"; // DEBUG var DEBUG_MENU = "debug_menu"; var DEBUG_CHART = "debug_chart"; var DEBUG_STAGE = "debug_stage"; - #if CAN_CHEAT - var CHEAT = "cheat"; - #end } enum Device diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index be4fab254..1dbba5b54 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -123,6 +123,11 @@ typedef PlayStateParams = * and must be loaded externally. */ ?overrideMusic:Bool, + /** + * The initial camera follow point. + * Used to persist the position of the `cameraFollowPosition` between levels. + */ + ?cameraFollowPoint:FlxPoint, } /** @@ -216,7 +221,7 @@ class PlayState extends MusicBeatSubState * The camera follow point from the last stage. * Used to persist the position of the `cameraFollowPosition` between levels. */ - public var previousCameraFollowPoint:FlxSprite = null; + public var previousCameraFollowPoint:FlxPoint = null; /** * The current camera zoom level. @@ -354,6 +359,11 @@ class PlayState extends MusicBeatSubState */ var startingSong:Bool = false; + /** + * False if `FlxG.sound.music` + */ + var musicPausedBySubState:Bool = false; + /** * False until `create()` has completed. */ @@ -539,6 +549,7 @@ class PlayState extends MusicBeatSubState isMinimalMode = params.minimalMode ?? false; startTimestamp = params.startTimestamp ?? 0.0; overrideMusic = params.overrideMusic ?? false; + previousCameraFollowPoint = params.cameraFollowPoint; // Don't do anything else here! Wait until create() when we attach to the camera. } @@ -697,7 +708,7 @@ class PlayState extends MusicBeatSubState function assertChartExists():Bool { // Returns null if the song failed to load or doesn't have the selected difficulty. - if (currentSong == null || currentChart == null) + if (currentSong == null || currentChart == null || currentChart.notes == null) { // We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic. criticalFailure = true; @@ -716,6 +727,10 @@ class PlayState extends MusicBeatSubState { message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.'; } + else if (currentChart.notes == null) + { + message = 'The was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.'; + } // Display a popup. This blocks the application until the user clicks OK. lime.app.Application.current.window.alert(message, 'Error loading PlayState'); @@ -1042,6 +1057,7 @@ class PlayState extends MusicBeatSubState // Pause the music. if (FlxG.sound.music != null) { + musicPausedBySubState = FlxG.sound.music.playing; FlxG.sound.music.pause(); if (vocals != null) vocals.pause(); } @@ -1049,7 +1065,6 @@ class PlayState extends MusicBeatSubState // Pause the countdown. Countdown.pauseCountdown(); } - else {} super.openSubState(subState); } @@ -1069,7 +1084,10 @@ class PlayState extends MusicBeatSubState if (event.eventCanceled) return; // Resume - FlxG.sound.music.play(FlxG.sound.music.time); + if (musicPausedBySubState) + { + FlxG.sound.music.play(FlxG.sound.music.time); + } if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals(); @@ -2613,38 +2631,25 @@ class PlayState extends MusicBeatSubState FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() { // no camFollow so it centers on horror tree var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId); - // Load and cache the song's charts. - // TODO: Do this in the loading state. - targetSong.cacheCharts(true); - - LoadingState.loadAndSwitchState(() -> { - var nextPlayState:PlayState = new PlayState( - { - targetSong: targetSong, - targetDifficulty: PlayStatePlaylist.campaignDifficulty, - targetVariation: currentVariation, - }); - nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y); - return nextPlayState; - }); + LoadingState.loadPlayState( + { + targetSong: targetSong, + targetDifficulty: PlayStatePlaylist.campaignDifficulty, + targetVariation: currentVariation, + cameraFollowPoint: cameraFollowPoint.getPosition(), + }); }); } else { var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId); - // Load and cache the song's charts. - // TODO: Do this in the loading state. - targetSong.cacheCharts(true); - LoadingState.loadAndSwitchState(() -> { - var nextPlayState:PlayState = new PlayState( - { - targetSong: targetSong, - targetDifficulty: PlayStatePlaylist.campaignDifficulty, - targetVariation: currentVariation, - }); - nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y); - return nextPlayState; - }); + LoadingState.loadPlayState( + { + targetSong: targetSong, + targetDifficulty: PlayStatePlaylist.campaignDifficulty, + targetVariation: currentVariation, + cameraFollowPoint: cameraFollowPoint.getPosition(), + }); } } } diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index ce06950f2..b1468bd29 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -115,6 +115,9 @@ abstract Save(RawSaveData) }; } + /** + * NOTE: Modifications will not be saved without calling `Save.flush()`! + */ public var options(get, never):SaveDataOptions; function get_options():SaveDataOptions @@ -122,6 +125,9 @@ abstract Save(RawSaveData) return this.options; } + /** + * NOTE: Modifications will not be saved without calling `Save.flush()`! + */ public var modOptions(get, never):Map<String, Dynamic>; function get_modOptions():Map<String, Dynamic> diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx index 3d91e3a9a..d54fd5b8f 100644 --- a/source/funkin/ui/MusicBeatState.hx +++ b/source/funkin/ui/MusicBeatState.hx @@ -59,19 +59,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler Conductor.stepHit.remove(this.stepHit); } - function handleControls():Void - { - var isHaxeUIFocused:Bool = haxe.ui.focus.FocusManager.instance?.focus != null; - - if (!isHaxeUIFocused) - { - // Rebindable volume keys. - if (controls.VOLUME_MUTE) FlxG.sound.toggleMuted(); - else if (controls.VOLUME_UP) FlxG.sound.changeVolume(0.1); - else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1); - } - } - function handleFunctionControls():Void { // Emergency exit button. @@ -85,8 +72,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler { super.update(elapsed); - handleControls(); - dispatchEvent(new UpdateScriptEvent(elapsed)); } diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx index 4671a9063..91822c557 100644 --- a/source/funkin/ui/MusicBeatSubState.hx +++ b/source/funkin/ui/MusicBeatSubState.hx @@ -54,11 +54,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler { super.update(elapsed); - // Rebindable volume keys. - if (controls.VOLUME_MUTE) FlxG.sound.toggleMuted(); - else if (controls.VOLUME_UP) FlxG.sound.changeVolume(0.1); - else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1); - // Emergency exit button. if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState()); diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index 718130356..8caf105d3 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -9,6 +9,7 @@ import funkin.ui.debug.charting.ChartEditorState; import funkin.ui.MusicBeatSubState; import funkin.util.logging.CrashHandler; import flixel.addons.transition.FlxTransitionableState; +import funkin.util.FileUtil; class DebugMenuSubState extends MusicBeatSubState { @@ -121,16 +122,7 @@ class DebugMenuSubState extends MusicBeatSubState #if sys function openLogFolder() { - #if windows - Sys.command('explorer', [CrashHandler.LOG_FOLDER]); - #elseif mac - // mac could be fuckie with where the log folder is relative to the game file... - // if this comment is still here... it means it has NOT been verified on mac yet! - Sys.command('open', [CrashHandler.LOG_FOLDER]); - #end - - // TODO: implement linux - // some shit with xdg-open :thinking: emoji... + FileUtil.openFolder(CrashHandler.LOG_FOLDER); } #end diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 6c64f952b..48a6e70c9 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -162,8 +162,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState public static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview'); public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); public static final CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:String = Paths.ui('chart-editor/toolbox/offsets'); - public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); - public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); + public static final CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/note-data'); + public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/event-data'); public static final CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:String = Paths.ui('chart-editor/toolbox/freeplay'); public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties'); @@ -538,9 +538,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Tools Status /** - * The note kind to use for notes being placed in the chart. Defaults to `''`. + * The note kind to use for notes being placed in the chart. Defaults to `null`. */ - var noteKindToPlace:String = ''; + var noteKindToPlace:Null<String> = null; /** * The event type to use for events being placed in the chart. Defaults to `''`. @@ -2458,11 +2458,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ override public function draw():Void { - if (selectionBoxStartPos != null) - { - trace('selectionBoxSprite: ${selectionBoxSprite.visible} ${selectionBoxSprite.exists} ${this.members.contains(selectionBoxSprite)}'); - } - super.draw(); } @@ -2968,7 +2963,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value); menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value); menubarItemToggleToolboxOffsets.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT, event.value); - menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value); + menubarItemToggleToolboxNoteData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT, event.value); menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value); menubarItemToggleToolboxFreeplay.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT, event.value); menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value); @@ -5286,6 +5281,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0); + FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace); + FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace); + FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels); diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx index d6c5beeac..88f73cfed 100644 --- a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx @@ -59,6 +59,17 @@ class SelectItemsCommand implements ChartEditorCommand state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT); } + // If we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note. + if (this.events.length == 0 && this.notes.length >= 1) + { + var noteSelected = this.notes[0]; + + state.noteKindToPlace = noteSelected.kind; + + // This code is here to parse note data that's not built as a struct for some reason. + state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT); + } + state.noteDisplayDirty = true; state.notePreviewDirty = true; } diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx index 35a00e562..5cc89e137 100644 --- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx @@ -56,6 +56,16 @@ class SetItemSelectionCommand implements ChartEditorCommand state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT); } + // IF we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note. + if (this.events.length == 0 && this.notes.length >= 1) + { + var noteSelected = this.notes[0]; + + state.noteKindToPlace = noteSelected.kind; + + state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT); + } + state.noteDisplayDirty = true; } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index 9e22ba833..f32cc2bfb 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -38,6 +38,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox; +import funkin.ui.debug.charting.toolboxes.ChartEditorNoteDataToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox; import haxe.ui.containers.Frame; import haxe.ui.containers.Grid; @@ -79,17 +80,16 @@ class ChartEditorToolboxHandler switch (id) { - case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: - onShowToolboxNoteData(state, toolbox); + case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT: + cast(toolbox, ChartEditorBaseToolbox).refresh(); case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT: - // TODO: Fix this. + // TODO: Make these better. cast(toolbox, ChartEditorBaseToolbox).refresh(); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: onShowToolboxPlaytestProperties(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT: cast(toolbox, ChartEditorBaseToolbox).refresh(); case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT: - // TODO: Fix this. cast(toolbox, ChartEditorBaseToolbox).refresh(); case ChartEditorState.CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT: cast(toolbox, ChartEditorBaseToolbox).refresh(); @@ -124,10 +124,6 @@ class ChartEditorToolboxHandler switch (id) { - case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: - onHideToolboxNoteData(state, toolbox); - case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT: - onHideToolboxEventData(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: onHideToolboxPlaytestProperties(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: @@ -196,7 +192,7 @@ class ChartEditorToolboxHandler var toolbox:Null<CollapsibleDialog> = null; switch (id) { - case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: + case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT: toolbox = buildToolboxNoteDataLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT: toolbox = buildToolboxEventDataLayout(state); @@ -262,58 +258,13 @@ class ChartEditorToolboxHandler static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog> { - var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT); + var toolbox:ChartEditorBaseToolbox = ChartEditorNoteDataToolbox.build(state); if (toolbox == null) return null; - // Starting position. - toolbox.x = 75; - toolbox.y = 100; - - toolbox.onDialogClosed = function(event:DialogEvent) { - state.menubarItemToggleToolboxNotes.selected = false; - } - - var toolboxNotesNoteKind:Null<DropDown> = toolbox.findComponent('toolboxNotesNoteKind', DropDown); - if (toolboxNotesNoteKind == null) throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesNoteKind component.'; - var toolboxNotesCustomKindLabel:Null<Label> = toolbox.findComponent('toolboxNotesCustomKindLabel', Label); - if (toolboxNotesCustomKindLabel == null) - throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesCustomKindLabel component.'; - var toolboxNotesCustomKind:Null<TextField> = toolbox.findComponent('toolboxNotesCustomKind', TextField); - if (toolboxNotesCustomKind == null) throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesCustomKind component.'; - - toolboxNotesNoteKind.onChange = function(event:UIEvent) { - var isCustom:Bool = (event.data.id == '~CUSTOM~'); - - if (isCustom) - { - toolboxNotesCustomKindLabel.hidden = false; - toolboxNotesCustomKind.hidden = false; - - state.noteKindToPlace = toolboxNotesCustomKind.text; - } - else - { - toolboxNotesCustomKindLabel.hidden = true; - toolboxNotesCustomKind.hidden = true; - - state.noteKindToPlace = event.data.id; - } - } - - toolboxNotesCustomKind.onChange = function(event:UIEvent) { - state.noteKindToPlace = toolboxNotesCustomKind.text; - } - return toolbox; } - static function onShowToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - - static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - - static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx new file mode 100644 index 000000000..d4fc69fc1 --- /dev/null +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx @@ -0,0 +1,115 @@ +package funkin.ui.debug.charting.toolboxes; + +import haxe.ui.components.DropDown; +import haxe.ui.components.TextField; +import haxe.ui.events.UIEvent; +import funkin.ui.debug.charting.util.ChartEditorDropdowns; + +/** + * The toolbox which allows modifying information like Note Kind. + */ +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml")) +class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox +{ + var toolboxNotesNoteKind:DropDown; + var toolboxNotesCustomKind:TextField; + + var _initializing:Bool = true; + + public function new(chartEditorState2:ChartEditorState) + { + super(chartEditorState2); + + initialize(); + + this.onDialogClosed = onClose; + + this._initializing = false; + } + + function onClose(event:UIEvent) + { + chartEditorState.menubarItemToggleToolboxNoteData.selected = false; + } + + function initialize():Void + { + toolboxNotesNoteKind.onChange = function(event:UIEvent) { + var noteKind:Null<String> = event?.data?.id ?? null; + if (noteKind == '') noteKind = null; + + trace('ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Note kind changed: $noteKind'); + + // Edit the note data to place. + if (noteKind == '~CUSTOM~') + { + showCustom(); + toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace; + } + else + { + hideCustom(); + chartEditorState.noteKindToPlace = noteKind; + toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace; + } + + if (!_initializing && chartEditorState.currentNoteSelection.length > 0) + { + // Edit the note data of any selected notes. + for (note in chartEditorState.currentNoteSelection) + { + note.kind = chartEditorState.noteKindToPlace; + } + chartEditorState.saveDataDirty = true; + chartEditorState.noteDisplayDirty = true; + chartEditorState.notePreviewDirty = true; + } + }; + var startingValueNoteKind = ChartEditorDropdowns.populateDropdownWithNoteKinds(toolboxNotesNoteKind, ''); + toolboxNotesNoteKind.value = startingValueNoteKind; + + toolboxNotesCustomKind.onChange = function(event:UIEvent) { + var customKind:Null<String> = event?.target?.text; + chartEditorState.noteKindToPlace = customKind; + + if (chartEditorState.currentEventSelection.length > 0) + { + // Edit the note data of any selected notes. + for (note in chartEditorState.currentNoteSelection) + { + note.kind = chartEditorState.noteKindToPlace; + } + chartEditorState.saveDataDirty = true; + chartEditorState.noteDisplayDirty = true; + chartEditorState.notePreviewDirty = true; + } + }; + toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace; + } + + public override function refresh():Void + { + super.refresh(); + + toolboxNotesNoteKind.value = ChartEditorDropdowns.lookupNoteKind(chartEditorState.noteKindToPlace); + toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace; + } + + function showCustom():Void + { + toolboxNotesCustomKindLabel.hidden = false; + toolboxNotesCustomKind.hidden = false; + } + + function hideCustom():Void + { + toolboxNotesCustomKindLabel.hidden = true; + toolboxNotesCustomKind.hidden = true; + } + + public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox + { + return new ChartEditorNoteDataToolbox(chartEditorState); + } +} diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx index 14c07440b..b26082f98 100644 --- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx +++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx @@ -108,6 +108,55 @@ class ChartEditorDropdowns return returnValue; } + static final NOTE_KINDS:Map<String, String> = [ + // Base + "" => "Default", + "~CUSTOM~" => "Custom", + // Weeks 1-7 + "mom" => "Mom Sings (Week 5)", + "ugh" => "Ugh (Week 7)", + "hehPrettyGood" => "Heh, Pretty Good (Week 7)", + // Weekend 1 + "weekend-1-lightcan" => "Light Can (2hot)", + "weekend-1-kickcan" => "Kick Can (2hot)", + "weekend-1-kneecan" => "Knee Can (2hot)", + "weekend-1-cockgun" => "Cock Gun (2hot)", + "weekend-1-firegun" => "Fire Gun (2hot)", + "weekend-1-punchlow" => "Punch Low (Blazin)", + "weekend-1-punchhigh" => "Punch High (Blazin)", + "weekend-1-punchlowblocked" => "Punch Low Blocked (Blazin)", + "weekend-1-punchhighblocked" => "Punch High Blocked (Blazin)", + "weekend-1-dodgelow" => "Dodge Low (Blazin)", + "weekend-1-blockhigh" => "Block High (Blazin)", + "weekend-1-fakeout" => "Fakeout (Blazin)", + ]; + + public static function populateDropdownWithNoteKinds(dropDown:DropDown, startingKindId:String):DropDownEntry + { + dropDown.dataSource.clear(); + + var returnValue:DropDownEntry = lookupNoteKind('~CUSTOM'); + + for (noteKindId in NOTE_KINDS.keys()) + { + var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Default'; + + var value:DropDownEntry = {id: noteKindId, text: noteKind}; + if (startingKindId == noteKindId) returnValue = value; + + dropDown.dataSource.add(value); + } + + return returnValue; + } + + public static function lookupNoteKind(noteKindId:Null<String>):DropDownEntry + { + if (noteKindId == null) return lookupNoteKind(''); + if (!NOTE_KINDS.exists(noteKindId)) return {id: '~CUSTOM~', text: 'Custom'}; + return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Default'}; + } + /** * Populate a dropdown with a list of song variations. */ diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index e7c615313..39cab8759 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1135,18 +1135,14 @@ class FreeplayState extends MusicBeatSubState FlxG.sound.play(Paths.sound('confirmMenu')); dj.confirm(); - // Load and cache the song's charts. - // TODO: Do this in the loading state. - targetSong.cacheCharts(true); - new FlxTimer().start(1, function(tmr:FlxTimer) { Paths.setCurrentLevel(cap.songData.levelId); - LoadingState.loadAndSwitchState(() -> new PlayState( + LoadingState.loadPlayState( { targetSong: targetSong, targetDifficulty: targetDifficulty, targetVariation: targetVariation, - }), true); + }, true); }); } diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index ea6940c4a..c93ad41a6 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -187,6 +187,10 @@ class Level implements IRegistryEntry<LevelData> if (_data.props.length == 0) return props; + var hiddenProps:Array<LevelProp> = props.splice(_data.props.length - 1, props.length - 1); + for (hiddenProp in hiddenProps) + hiddenProp.visible = false; + for (propIndex in 0..._data.props.length) { var propData = _data.props[propIndex]; @@ -198,6 +202,7 @@ class Level implements IRegistryEntry<LevelData> { existingProp.propData = propData; existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex; + existingProp.visible = true; } else { diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 1905a7c57..bd7a05f91 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -554,22 +554,15 @@ class StoryMenuState extends MusicBeatState 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( + LoadingState.loadPlayState( { targetSong: targetSong, targetDifficulty: PlayStatePlaylist.campaignDifficulty, - }), true); + }, true); }); } @@ -597,7 +590,9 @@ class StoryMenuState extends MusicBeatState { // 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); + // cancels potential tween in progress, and tweens from there + FlxTween.cancelTweensOf(levelBackground); + FlxTween.color(levelBackground, 0.9, levelBackground.color, currentColor, {ease: FlxEase.quartOut}); } else { @@ -637,10 +632,10 @@ class StoryMenuState extends MusicBeatState function updateProps():Void { - for (prop in currentLevel.buildProps(levelProps.members)) + for (ind => prop in currentLevel.buildProps(levelProps.members)) { prop.zIndex = 1000; - levelProps.add(prop); + if (levelProps.members[ind] != prop) levelProps.replace(levelProps.members[ind], prop) ?? levelProps.add(prop); } refresh(); diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index 63dcb8f68..86f443d1d 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -9,7 +9,6 @@ import funkin.graphics.shaders.ScreenWipeShader; import funkin.play.PlayState; import funkin.play.PlayStatePlaylist; import funkin.play.song.Song.SongDifficulty; -import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatState; import haxe.io.Path; import funkin.graphics.FunkinSprite; @@ -27,17 +26,19 @@ class LoadingState extends MusicBeatState inline static var MIN_TIME = 1.0; var target:NextState; - var stopMusic = false; + var playParams:Null<PlayStateParams>; + var stopMusic:Bool = false; var callbacks:MultiCallback; - var danceLeft = false; + var danceLeft:Bool = false; var loadBar:FlxSprite; var funkay:FlxSprite; - function new(target:NextState, stopMusic:Bool) + function new(target:NextState, stopMusic:Bool, playParams:Null<PlayStateParams> = null) { super(); this.target = target; + this.playParams = playParams; this.stopMusic = stopMusic; } @@ -62,10 +63,18 @@ class LoadingState extends MusicBeatState callbacks = new MultiCallback(onLoad); var introComplete = callbacks.add('introComplete'); - if (Std.isOfType(target, PlayState)) + if (playParams != null) { - var targetPlayState:PlayState = cast target; - var targetChart:SongDifficulty = targetPlayState.currentChart; + // Load and cache the song's charts. + if (playParams.targetSong != null) + { + playParams.targetSong.cacheCharts(true); + } + + // Preload the song for the play state. + var difficulty:String = playParams.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY; + var variation:String = playParams.targetVariation ?? Constants.DEFAULT_VARIATION; + var targetChart:SongDifficulty = playParams.targetSong?.getDifficulty(difficulty, variation); var instPath:String = Paths.inst(targetChart.song.id); var voicesPaths:Array<String> = targetChart.buildVoiceList(); @@ -172,25 +181,36 @@ class LoadingState extends MusicBeatState return Paths.inst(PlayState.instance.currentSong.id); } - inline static public function loadAndSwitchState(nextState:NextState, shouldStopMusic = false):Void - { - FlxG.switchState(getNextState(nextState, shouldStopMusic)); - } - - static function getNextState(nextState:NextState, shouldStopMusic = false):NextState + /** + * Starts the transition to a new `PlayState` to start a new song. + * First switches to the `LoadingState` if assets need to be loaded. + * @param params The parameters for the next `PlayState`. + * @param shouldStopMusic Whether to stop the current music while loading. + */ + public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false):Void { Paths.setCurrentLevel(PlayStatePlaylist.campaignId); + var playStateCtor:NextState = () -> new PlayState(params); #if NO_PRELOAD_ALL - // var loaded = isSoundLoaded(getSongPath()) - // && (!PlayState.currentSong.needsVoices || isSoundLoaded(getVocalPath())) - // && isLibraryLoaded('shared'); - // - if (true) return () -> new LoadingState(nextState, shouldStopMusic); - #end - if (shouldStopMusic && FlxG.sound.music != null) FlxG.sound.music.stop(); + // Switch to loading state while we load assets (default on HTML5 target). + var loadStateCtor:NextState = () -> new LoadingState(playStateCtor, shouldStopMusic, params); + FlxG.switchState(loadStateCtor); + #else + // All assets preloaded, switch directly to play state (defualt on other targets). + if (shouldStopMusic && FlxG.sound.music != null) + { + FlxG.sound.music.stop(); + } - return nextState; + // Load and cache the song's charts. + if (params?.targetSong != null) + { + params.targetSong.cacheCharts(true); + } + + FlxG.switchState(playStateCtor); + #end } #if NO_PRELOAD_ALL diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 612737680..7a7b1422c 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -20,6 +20,7 @@ class FileUtil { public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc"); public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip"); + public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png"); public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo = { @@ -31,6 +32,11 @@ class FileUtil extension: 'zip', label: 'ZIP Archive', }; + public static final FILE_EXTENSION_INFO_PNG:FileDialogExtensionInfo = + { + extension: 'png', + label: 'PNG Image', + }; /** * Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected. @@ -639,6 +645,23 @@ class FileUtil }; } + public static function openFolder(pathFolder:String) + { + #if windows + Sys.command('explorer', [pathFolder]); + #elseif mac + // mac could be fuckie with where the log folder is relative to the game file... + // if this comment is still here... it means it has NOT been verified on mac yet! + // + // FileUtil.hx note: this was originally used to open the logs specifically! + // thats why the above comment is there! + Sys.command('open', [pathFolder]); + #end + + // TODO: implement linux + // some shit with xdg-open :thinking: emoji... + } + static function convertTypeFilter(typeFilter:Array<FileFilter>):String { var filter:String = null; diff --git a/source/funkin/util/MathUtil.hx b/source/funkin/util/MathUtil.hx index 3cb6621a7..5fed1d3e1 100644 --- a/source/funkin/util/MathUtil.hx +++ b/source/funkin/util/MathUtil.hx @@ -5,6 +5,12 @@ package funkin.util; */ class MathUtil { + /** + * Euler's constant and the base of the natural logarithm. + * Math.E is not a constant in Haxe, so we'll just define it ourselves. + */ + public static final E:Float = 2.71828182845904523536; + /** * Perform linear interpolation between the base and the target, based on the current framerate. */ @@ -24,8 +30,44 @@ class MathUtil * @param value The value to get the logarithm of. * @return `log_base(value)` */ - public static function logBase(base:Float, value:Float):Float + public static function logBase(base:Float, value:Float) { return Math.log(value) / Math.log(base); } + + /** + * @returns `2^x` + */ + public static function exp2(x:Float) + { + return Math.pow(2, x); + } + + /** + * Linearly interpolate between two values. + * @param base The starting value, when `progress <= 0`. + * @param target The ending value, when `progress >= 1`. + * @param progress Value used to interpolate between `base` and `target`. + */ + public static function lerp(base:Float, target:Float, progress:Float) + { + return base + progress * (target - base); + } + + /** + * Perform a framerate-independent linear interpolation between the base value and the target. + * @param current The current value. + * @param target The target value. + * @param elapsed The time elapsed since the last frame. + * @param duration The total duration of the interpolation. Nominal duration until remaining distance is less than `precision`. + * @param precision The target precision of the interpolation. Defaults to 1% of distance remaining. + * @see https://twitter.com/FreyaHolmer/status/1757918211679650262 + */ + public static function smoothLerp(current:Float, target:Float, elapsed:Float, duration:Float, precision:Float = 1 / 100):Float + { + // var halfLife:Float = -duration / logBase(2, precision); + // lerp(current, target, 1 - exp2(-elapsed / halfLife)); + + return lerp(current, target, 1 - Math.pow(precision, elapsed / duration)); + } } diff --git a/source/funkin/util/plugins/ScreenshotPlugin.hx b/source/funkin/util/plugins/ScreenshotPlugin.hx new file mode 100644 index 000000000..16d0c7244 --- /dev/null +++ b/source/funkin/util/plugins/ScreenshotPlugin.hx @@ -0,0 +1,320 @@ +package funkin.util.plugins; + +import flixel.FlxBasic; +import flixel.FlxCamera; +import flixel.FlxG; +import flixel.FlxState; +import flixel.graphics.FlxGraphic; +import flixel.input.keyboard.FlxKey; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import flixel.util.FlxSignal; +import flixel.util.FlxTimer; +import funkin.graphics.FunkinSprite; +import funkin.input.Cursor; +import openfl.display.Bitmap; +import openfl.display.Sprite; +import openfl.display.BitmapData; +import openfl.display.PNGEncoderOptions; +import openfl.geom.Matrix; +import openfl.geom.Rectangle; +import openfl.utils.ByteArray; +import openfl.events.MouseEvent; + +typedef ScreenshotPluginParams = +{ + hotkeys:Array<FlxKey>, + ?region:Rectangle, + shouldHideMouse:Bool, + flashColor:Null<FlxColor>, + fancyPreview:Bool, +}; + +/** + * What if `flixel.addons.plugin.screengrab.FlxScreenGrab` but it's better? + * TODO: Contribute this upstream. + */ +class ScreenshotPlugin extends FlxBasic +{ + public static final SCREENSHOT_FOLDER = 'screenshots'; + + var _hotkeys:Array<FlxKey>; + + var _region:Null<Rectangle>; + + var _shouldHideMouse:Bool; + + var _flashColor:Null<FlxColor>; + + var _fancyPreview:Bool; + + /** + * A signal fired before the screenshot is taken. + */ + public var onPreScreenshot(default, null):FlxTypedSignal<Void->Void>; + + /** + * A signal fired after the screenshot is taken. + * @param bitmap The bitmap that was captured. + */ + public var onPostScreenshot(default, null):FlxTypedSignal<Bitmap->Void>; + + public function new(params:ScreenshotPluginParams) + { + super(); + + _hotkeys = params.hotkeys; + _region = params.region ?? null; + _shouldHideMouse = params.shouldHideMouse; + _flashColor = params.flashColor; + _fancyPreview = params.fancyPreview; + + onPreScreenshot = new FlxTypedSignal<Void->Void>(); + onPostScreenshot = new FlxTypedSignal<Bitmap->Void>(); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (hasPressedScreenshot()) + { + capture(); + } + } + + /** + * Initialize the screenshot plugin. + */ + public static function initialize():Void + { + FlxG.plugins.addPlugin(new ScreenshotPlugin( + { + flashColor: Preferences.flashingLights ? FlxColor.WHITE : null, // Was originally a black flash. + + // TODO: Add a way to configure screenshots from the options menu. + hotkeys: [FlxKey.F3], + shouldHideMouse: false, + fancyPreview: true, + })); + } + + public function hasPressedScreenshot():Bool + { + return PlayerSettings.player1.controls.SCREENSHOT; + } + + public function updatePreferences():Void + { + _flashColor = Preferences.flashingLights ? FlxColor.WHITE : null; + } + + /** + * Defines the region of the screen that should be captured. + * You don't need to call this method if you want to capture the entire screen, that's the default behavior. + */ + public function defineCaptureRegion(x:Int, y:Int, width:Int, height:Int):Void + { + _region = new Rectangle(x, y, width, height); + } + + /** + * Capture the game screen as a bitmap. + */ + public function capture():Void + { + onPreScreenshot.dispatch(); + + var captureRegion = _region != null ? _region : new Rectangle(0, 0, FlxG.stage.stageWidth, FlxG.stage.stageHeight); + + var wasMouseHidden = false; + if (_shouldHideMouse && FlxG.mouse.visible) + { + wasMouseHidden = true; + Cursor.hide(); + } + + // The actual work. + // var bitmap = new Bitmap(new BitmapData(Math.floor(captureRegion.width), Math.floor(captureRegion.height), true, 0x00000000)); // Create a transparent empty bitmap. + // var drawMatrix = new Matrix(1, 0, 0, 1, -captureRegion.x, -captureRegion.y); // Modifying this will scale or skew the bitmap. + // bitmap.bitmapData.draw(FlxG.stage, drawMatrix); + var bitmap = new Bitmap(BitmapData.fromImage(FlxG.stage.window.readPixels())); + + if (wasMouseHidden) + { + Cursor.show(); + } + + // Save the bitmap to a file. + saveScreenshot(bitmap); + + // Show some feedback. + showCaptureFeedback(); + if (_fancyPreview) + { + showFancyPreview(bitmap); + } + + onPostScreenshot.dispatch(bitmap); + } + + final CAMERA_FLASH_DURATION = 0.25; + + /** + * Visual (and audio?) feedback when a screenshot is taken. + */ + function showCaptureFeedback():Void + { + var flashBitmap = new Bitmap(new BitmapData(Std.int(FlxG.stage.width), Std.int(FlxG.stage.height), false, 0xFFFFFFFF)); + var flashSpr = new Sprite(); + flashSpr.addChild(flashBitmap); + FlxG.stage.addChild(flashSpr); + FlxTween.tween(flashSpr, {alpha: 0}, 0.15, {ease: FlxEase.quadOut, onComplete: _ -> FlxG.stage.removeChild(flashSpr)}); + } + + static final PREVIEW_INITIAL_DELAY = 0.25; // How long before the preview starts fading in. + static final PREVIEW_FADE_IN_DURATION = 0.3; // How long the preview takes to fade in. + static final PREVIEW_FADE_OUT_DELAY = 1.25; // How long the preview stays on screen. + static final PREVIEW_FADE_OUT_DURATION = 0.3; // How long the preview takes to fade out. + + function showFancyPreview(bitmap:Bitmap):Void + { + // ermmm stealing this?? + var wasMouseHidden = false; + if (!FlxG.mouse.visible) + { + wasMouseHidden = true; + Cursor.show(); + } + + // so that it doesnt change the alpha when tweening in/out + var changingAlpha:Bool = false; + + // fuck it, cursed locally scoped functions, purely because im lazy + // (and so we can check changingAlpha, which is locally scoped.... because I'm lazy...) + var onHover = function(e:MouseEvent) { + if (!changingAlpha) e.target.alpha = 0.6; + }; + + var onHoverOut = function(e:MouseEvent) { + if (!changingAlpha) e.target.alpha = 1; + } + + var scale:Float = 0.25; + var w:Int = Std.int(bitmap.bitmapData.width * scale); + var h:Int = Std.int(bitmap.bitmapData.height * scale); + + var preview:BitmapData = new BitmapData(w, h, true); + var matrix:openfl.geom.Matrix = new openfl.geom.Matrix(); + matrix.scale(scale, scale); + preview.draw(bitmap.bitmapData, matrix); + + // used for movement + button stuff + var previewSprite = new Sprite(); + + previewSprite.buttonMode = true; + previewSprite.addEventListener(MouseEvent.MOUSE_DOWN, openScreenshotsFolder); + previewSprite.addEventListener(MouseEvent.MOUSE_OVER, onHover); + previewSprite.addEventListener(MouseEvent.MOUSE_OUT, onHoverOut); + + FlxG.stage.addChild(previewSprite); + + previewSprite.alpha = 0.0; + previewSprite.y -= 10; + + var previewBitmap = new Bitmap(preview); + previewSprite.addChild(previewBitmap); + + // Wait to fade in. + new FlxTimer().start(PREVIEW_INITIAL_DELAY, function(_) { + // Fade in. + changingAlpha = true; + FlxTween.tween(previewSprite, {alpha: 1.0, y: 0}, PREVIEW_FADE_IN_DURATION, + { + ease: FlxEase.quartOut, + onComplete: function(_) { + changingAlpha = false; + // Wait to fade out. + new FlxTimer().start(PREVIEW_FADE_OUT_DELAY, function(_) { + changingAlpha = true; + // Fade out. + FlxTween.tween(previewSprite, {alpha: 0.0, y: 10}, PREVIEW_FADE_OUT_DURATION, + { + ease: FlxEase.quartInOut, + onComplete: function(_) { + if (wasMouseHidden) + { + Cursor.hide(); + } + + previewSprite.removeEventListener(MouseEvent.MOUSE_DOWN, openScreenshotsFolder); + previewSprite.removeEventListener(MouseEvent.MOUSE_OVER, onHover); + previewSprite.removeEventListener(MouseEvent.MOUSE_OUT, onHoverOut); + + FlxG.stage.removeChild(previewSprite); + } + }); + }); + } + }); + }); + } + + function openScreenshotsFolder(e:MouseEvent):Void + { + FileUtil.openFolder(SCREENSHOT_FOLDER); + } + + static function getCurrentState():FlxState + { + var state = FlxG.state; + while (state.subState != null) + { + state = state.subState; + } + return state; + } + + static function getScreenshotPath():String + { + return '$SCREENSHOT_FOLDER/screenshot-${DateUtil.generateTimestamp()}.png'; + } + + static function makeScreenshotPath():Void + { + FileUtil.createDirIfNotExists(SCREENSHOT_FOLDER); + } + + /** + * Convert a Bitmap to a PNG ByteArray to save to a file. + */ + static function encodePNG(bitmap:Bitmap):ByteArray + { + return bitmap.bitmapData.encode(bitmap.bitmapData.rect, new PNGEncoderOptions()); + } + + /** + * Save the generated bitmap to a file. + * @param bitmap The bitmap to save. + */ + static function saveScreenshot(bitmap:Bitmap) + { + makeScreenshotPath(); + var targetPath:String = getScreenshotPath(); + + var pngData = encodePNG(bitmap); + + if (pngData == null) + { + trace('[WARN] Failed to encode PNG data.'); + return; + } + else + { + trace('Saving screenshot to: ' + targetPath); + // TODO: Make this work on browser. + FileUtil.writeBytesToPath(targetPath, pngData); + } + } +} diff --git a/source/funkin/util/plugins/VolumePlugin.hx b/source/funkin/util/plugins/VolumePlugin.hx new file mode 100644 index 000000000..5dbe60abf --- /dev/null +++ b/source/funkin/util/plugins/VolumePlugin.hx @@ -0,0 +1,34 @@ +package funkin.util.plugins; + +import flixel.FlxBasic; + +/** + * Handles volume control in a way that is compatible with alternate control schemes. + */ +class VolumePlugin extends FlxBasic +{ + public function new() + { + super(); + } + + public static function initialize() + { + FlxG.plugins.addPlugin(new VolumePlugin()); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + var isHaxeUIFocused:Bool = haxe.ui.focus.FocusManager.instance?.focus != null; + + if (!isHaxeUIFocused) + { + // Rebindable volume keys. + if (PlayerSettings.player1.controls.VOLUME_MUTE) FlxG.sound.toggleMuted(); + else if (PlayerSettings.player1.controls.VOLUME_UP) FlxG.sound.changeVolume(0.1); + else if (PlayerSettings.player1.controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1); + } + } +}