diff --git a/assets b/assets index 84b157429..8b914574f 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 84b1574294e7a7af21adf1a0e4894c88431ca43d +Subproject commit 8b914574fc4724c5fe483f4f9d81081bb1518c12 diff --git a/hmm.json b/hmm.json index 700b42dfe..836b01c9b 100644 --- a/hmm.json +++ b/hmm.json @@ -73,8 +73,8 @@ "name": "hxCodec", "type": "git", "dir": null, - "ref": "c8c47e706ad82a423783006ed901b6d93c89a421", - "url": "https://github.com/polybiusproxy/hxCodec" + "ref": "387e1665d6feb5762358134f168e6ebfe46acec8", + "url": "https://github.com/FunkinCrew/hxCodec" }, { "name": "hxcpp", diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 33674439d..7dc20b385 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -202,6 +202,9 @@ class InitState extends FlxState // Plugins provide a useful interface for globally active Flixel objects, // that receive update events regardless of the current state. // TODO: Move scripted Module behavior to a Flixel plugin. + #if debug + funkin.util.plugins.MemoryGCPlugin.initialize(); + #end funkin.util.plugins.EvacuateDebugPlugin.initialize(); funkin.util.plugins.ForceCrashPlugin.initialize(); funkin.util.plugins.ReloadAssetsDebugPlugin.initialize(); diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index ba157ed8e..06ba48e81 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -25,6 +25,9 @@ class FunkinSound extends FlxSound implements ICloneable { static final MAX_VOLUME:Float = 1.0; + /** + * Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible! + */ static var cache(default, null):FlxTypedGroup = new FlxTypedGroup(); public var muted(default, set):Bool = false; @@ -174,10 +177,14 @@ class FunkinSound extends FlxSound implements ICloneable */ override function onFocus():Void { - if (!_alreadyPaused && this._shouldPlay) + if (!_alreadyPaused) { resume(); } + else + { + trace('Not resuming audio on focus!'); + } } /** @@ -185,6 +192,7 @@ class FunkinSound extends FlxSound implements ICloneable */ override function onFocusLost():Void { + trace('Focus lost, pausing audio!'); _alreadyPaused = _paused; pause(); } @@ -252,6 +260,8 @@ class FunkinSound extends FlxSound implements ICloneable { var sound:FunkinSound = cache.recycle(construct); + // Load the sound. + // Sets `exists = true` as a side effect. sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete); if (embeddedSound is String) diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index 0e81a0901..a26537c2a 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -80,8 +80,9 @@ class SoundGroup extends FlxTypedGroup // We have to play, then pause the sound to set the time, // else the sound will restart immediately when played. - result.play(true, 0.0); - result.pause(); + // TODO: Past me experienced that issue but present me didn't? Investigate. + // result.play(true, 0.0); + // result.pause(); result.time = this.time; result.onComplete = function() { diff --git a/source/funkin/graphics/video/FlxVideo.hx b/source/funkin/graphics/video/FlxVideo.hx index e95d7caa6..5e178efb3 100644 --- a/source/funkin/graphics/video/FlxVideo.hx +++ b/source/funkin/graphics/video/FlxVideo.hx @@ -40,6 +40,32 @@ class FlxVideo extends FlxBasic netStream.play(videoPath); } + public function pauseVideo():Void + { + if (netStream != null) + { + netStream.pause(); + } + } + + public function resumeVideo():Void + { + // Resume playing the video. + if (netStream != null) + { + netStream.resume(); + } + } + + public function restartVideo():Void + { + // Seek to the beginning of the video. + if (netStream != null) + { + netStream.seek(0); + } + } + public function finishVideo():Void { netStream.dispose(); diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx index c4760cf5f..0857678d0 100644 --- a/source/funkin/input/Controls.hx +++ b/source/funkin/input/Controls.hx @@ -58,12 +58,11 @@ class Controls extends FlxActionSet var _back = new FlxActionDigital(Action.BACK); var _pause = new FlxActionDigital(Action.PAUSE); var _reset = new FlxActionDigital(Action.RESET); + var _screenshot = new FlxActionDigital(Action.SCREENSHOT); var _cutscene_advance = new FlxActionDigital(Action.CUTSCENE_ADVANCE); - var _cutscene_skip = new FlxActionDigital(Action.CUTSCENE_SKIP); 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); @@ -208,16 +207,21 @@ class Controls extends FlxActionSet inline function get_PAUSE() return _pause.check(); + public var RESET(get, never):Bool; + + inline function get_RESET() + return _reset.check(); + + public var SCREENSHOT(get, never):Bool; + + inline function get_SCREENSHOT() + return _screenshot.check(); + public var CUTSCENE_ADVANCE(get, never):Bool; inline function get_CUTSCENE_ADVANCE() return _cutscene_advance.check(); - public var CUTSCENE_SKIP(get, never):Bool; - - inline function get_CUTSCENE_SKIP() - return _cutscene_skip.check(); - public var DEBUG_MENU(get, never):Bool; inline function get_DEBUG_MENU() @@ -233,11 +237,6 @@ 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() @@ -253,11 +252,6 @@ class Controls extends FlxActionSet inline function get_VOLUME_MUTE() return _volume_mute.check(); - public var RESET(get, never):Bool; - - inline function get_RESET() - return _reset.check(); - public function new(name, scheme:KeyboardScheme = null) { super(name); @@ -289,16 +283,15 @@ class Controls extends FlxActionSet add(_accept); add(_back); add(_pause); + add(_reset); + add(_screenshot); 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); for (action in digitalActions) byName[action.name] = action; @@ -383,12 +376,11 @@ class Controls extends FlxActionSet case BACK: _back; case PAUSE: _pause; case RESET: _reset; + case SCREENSHOT: _screenshot; case CUTSCENE_ADVANCE: _cutscene_advance; - case CUTSCENE_SKIP: _cutscene_skip; 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; @@ -449,26 +441,24 @@ class Controls extends FlxActionSet func(_back, JUST_PRESSED); case PAUSE: func(_pause, JUST_PRESSED); + case RESET: + func(_reset, JUST_PRESSED); + case SCREENSHOT: + func(_screenshot, JUST_PRESSED); case CUTSCENE_ADVANCE: func(_cutscene_advance, JUST_PRESSED); - case CUTSCENE_SKIP: - func(_cutscene_skip, PRESSED); case DEBUG_MENU: func(_debug_menu, JUST_PRESSED); case DEBUG_CHART: 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: func(_volume_down, JUST_PRESSED); case VOLUME_MUTE: func(_volume_mute, JUST_PRESSED); - case RESET: - func(_reset, JUST_PRESSED); } } @@ -654,13 +644,12 @@ class Controls extends FlxActionSet bindKeys(Control.ACCEPT, getDefaultKeybinds(scheme, Control.ACCEPT)); bindKeys(Control.BACK, getDefaultKeybinds(scheme, Control.BACK)); bindKeys(Control.PAUSE, getDefaultKeybinds(scheme, Control.PAUSE)); + bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET)); + bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT)); bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE)); - bindKeys(Control.CUTSCENE_SKIP, getDefaultKeybinds(scheme, Control.CUTSCENE_SKIP)); 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)); @@ -683,16 +672,15 @@ class Controls extends FlxActionSet case Control.ACCEPT: return [Z, SPACE, ENTER]; case Control.BACK: return [X, BACKSPACE, ESCAPE]; case Control.PAUSE: return [P, ENTER, ESCAPE]; + case Control.RESET: return [R]; + case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen case Control.CUTSCENE_ADVANCE: return [Z, ENTER]; - case Control.CUTSCENE_SKIP: return [P, ESCAPE]; 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]; - case Control.RESET: return [R]; } case Duo(true): switch (control) { @@ -707,16 +695,15 @@ class Controls extends FlxActionSet case Control.ACCEPT: return [G, Z]; case Control.BACK: return [H, X]; case Control.PAUSE: return [ONE]; + case Control.RESET: return [R]; + case Control.SCREENSHOT: return [PRINTSCREEN]; case Control.CUTSCENE_ADVANCE: return [G, Z]; - case Control.CUTSCENE_SKIP: return [ONE]; 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]; - case Control.RESET: return [R]; } case Duo(false): switch (control) { @@ -731,16 +718,15 @@ class Controls extends FlxActionSet case Control.ACCEPT: return [ENTER]; case Control.BACK: return [ESCAPE]; case Control.PAUSE: return [ONE]; + case Control.RESET: return [R]; + case Control.SCREENSHOT: return [PRINTSCREEN]; case Control.CUTSCENE_ADVANCE: return [ENTER]; - case Control.CUTSCENE_SKIP: return [ONE]; 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]; - case Control.RESET: return [R]; } default: // Fallthrough. @@ -843,15 +829,14 @@ class Controls extends FlxActionSet Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT), Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT), Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE), + Control.RESET => getDefaultGamepadBinds(Control.RESET), // Control.SCREENSHOT => [], // Control.VOLUME_UP => [RIGHT_SHOULDER], // Control.VOLUME_DOWN => [LEFT_SHOULDER], // Control.VOLUME_MUTE => [RIGHT_TRIGGER], Control.CUTSCENE_ADVANCE => getDefaultGamepadBinds(Control.CUTSCENE_ADVANCE), - Control.CUTSCENE_SKIP => getDefaultGamepadBinds(Control.CUTSCENE_SKIP), // Control.DEBUG_MENU // Control.DEBUG_CHART - Control.RESET => getDefaultGamepadBinds(Control.RESET) ]); } @@ -868,15 +853,14 @@ 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.RESET: return [RIGHT_SHOULDER]; case Control.SCREENSHOT: return []; case Control.VOLUME_UP: return []; case Control.VOLUME_DOWN: return []; case Control.VOLUME_MUTE: return []; case Control.CUTSCENE_ADVANCE: return [A]; - case Control.CUTSCENE_SKIP: return [START]; case Control.DEBUG_MENU: return []; case Control.DEBUG_CHART: return []; - case Control.RESET: return [RIGHT_SHOULDER]; default: // Fallthrough. } @@ -1228,14 +1212,13 @@ enum Control UI_RIGHT; UI_DOWN; RESET; + SCREENSHOT; ACCEPT; BACK; PAUSE; // CUTSCENE CUTSCENE_ADVANCE; - CUTSCENE_SKIP; // SCREENSHOT - SCREENSHOT; // VOLUME VOLUME_UP; VOLUME_DOWN; @@ -1279,15 +1262,14 @@ abstract Action(String) to String from String var BACK = "back"; var PAUSE = "pause"; var RESET = "reset"; + // SCREENSHOT + var SCREENSHOT = "screenshot"; // CUTSCENE var CUTSCENE_ADVANCE = "cutscene_advance"; - var CUTSCENE_SKIP = "cutscene_skip"; // VOLUME 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"; diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index 1ae96268d..2b705ea9e 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -1,313 +1,736 @@ package funkin.play; -import funkin.play.PlayStatePlaylist; -import flixel.FlxSprite; import flixel.addons.transition.FlxTransitionableState; -import flixel.group.FlxGroup.FlxTypedGroup; -import funkin.ui.MusicBeatSubState; -import flixel.sound.FlxSound; +import flixel.FlxG; +import flixel.util.FlxTimer; +import flixel.FlxSprite; +import flixel.group.FlxSpriteGroup; +import flixel.math.FlxMath; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; -import funkin.play.PlayState; +import funkin.audio.FunkinSound; import funkin.data.song.SongRegistry; -import funkin.ui.Alphabet; import funkin.graphics.FunkinSprite; +import funkin.play.cutscene.VideoCutscene; +import funkin.play.PlayState; +import funkin.ui.AtlasText; +import funkin.ui.MusicBeatSubState; +import funkin.ui.transition.StickerSubState; +/** + * Parameters for initializing the PauseSubState. + */ +typedef PauseSubStateParams = +{ + /** + * Which mode to start in. Dictates what entries are displayed. + */ + ?mode:PauseMode, +}; + +/** + * The menu displayed when the Play State is paused. + */ class PauseSubState extends MusicBeatSubState { - var grpMenuShit:FlxTypedGroup; + // =============== + // Constants + // =============== - final pauseOptionsBase:Array = [ - 'Resume', - 'Restart Song', - 'Change Difficulty', - 'Toggle Practice Mode', - 'Exit to Menu' + /** + * Pause menu entries for when the game is paused during a song. + */ + static final PAUSE_MENU_ENTRIES_STANDARD:Array = [ + {text: 'Resume', callback: resume}, + {text: 'Restart Song', callback: restartPlayState}, + {text: 'Change Difficulty', callback: switchMode.bind(_, Difficulty)}, + {text: 'Enable Practice Mode', callback: enablePracticeMode, filter: () -> !(PlayState.instance?.isPracticeMode ?? false)}, + {text: 'Exit to Menu', callback: quitToMenu}, ]; - final pauseOptionsCharting:Array = ['Resume', 'Restart Song', 'Exit to Chart Editor']; - final pauseOptionsDifficultyBase:Array = ['BACK']; + /** + * Pause menu entries for when the game is paused in the Chart Editor preview. + */ + static final PAUSE_MENU_ENTRIES_CHARTING:Array = [ + {text: 'Resume', callback: resume}, + {text: 'Restart Song', callback: restartPlayState}, + {text: 'Return to Chart Editor', callback: quitToChartEditor}, + ]; - var pauseOptionsDifficulty:Array = []; // AUTO-POPULATED + /** + * Pause menu entries for when the user selects "Change Difficulty". + */ + static final PAUSE_MENU_ENTRIES_DIFFICULTY:Array = [ + {text: 'Back', callback: switchMode.bind(_, Standard)} + // Other entries are added dynamically. + ]; - var menuItems:Array = []; - var curSelected:Int = 0; + /** + * Pause menu entries for when the game is paused during a video cutscene. + */ + static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array = [ + {text: 'Resume', callback: resume}, + {text: 'Restart Cutscene', callback: restartVideoCutscene}, + {text: 'Skip Cutscene', callback: skipVideoCutscene}, + {text: 'Exit to Menu', callback: quitToMenu}, + ]; - var pauseMusic:FlxSound; + /** + * Pause menu entries for when the game is paused during a conversation. + */ + static final PAUSE_MENU_ENTRIES_CONVERSATION:Array = [ + {text: 'Resume', callback: resume}, + {text: 'Restart Dialogue', callback: restartConversation}, + {text: 'Skip Dialogue', callback: skipConversation}, + {text: 'Exit to Menu', callback: quitToMenu}, + ]; - var practiceText:FlxText; + /** + * Duration for the music to fade in when the pause menu is opened. + */ + static final MUSIC_FADE_IN_TIME:Float = 5; - public var exitingToMenu:Bool = false; + /** + * The final volume for the music when the pause menu is opened. + */ + static final MUSIC_FINAL_VOLUME:Float = 0.75; - var bg:FlxSprite; - var metaDataGrp:FlxTypedGroup; + /** + * Defines which pause music to use. + */ + public static var musicSuffix:String = ''; - var isChartingMode:Bool; + /** + * Reset the pause configuration to the default. + */ + public static function reset():Void + { + musicSuffix = ''; + } - public function new(isChartingMode:Bool = false) + // =============== + // Status Variables + // =============== + + /** + * Disallow input until transitions are complete! + * This prevents the pause menu from immediately closing when opened, among other things. + */ + public var allowInput:Bool = false; + + /** + * The entries currently displayed in the pause menu. + */ + var currentMenuEntries:Array; + + /** + * The index of `currentMenuEntries` that is currently selected. + */ + var currentEntry:Int = 0; + + /** + * The mode that the pause menu is currently in. + */ + var currentMode:PauseMode; + + // =============== + // Graphics Variables + // =============== + + /** + * The semi-transparent black background that appears when the game is paused. + */ + var background:FunkinSprite; + + /** + * The metadata displayed in the top right. + */ + var metadata:FlxTypedSpriteGroup; + + /** + * A text object that displays the current practice mode status. + */ + var metadataPractice:FlxText; + + /** + * A text object that displays the current death count. + */ + var metadataDeaths:FlxText; + + /** + * The actual text objects for the menu entries. + */ + var menuEntryText:FlxTypedSpriteGroup; + + // =============== + // Audio Variables + // =============== + var pauseMusic:FunkinSound; + + // =============== + // Constructor + // =============== + + public function new(?params:PauseSubStateParams) { super(); - - this.isChartingMode = isChartingMode; - - menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase; - var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation); - trace('DIFFICULTIES: ${difficultiesInVariation}'); - - pauseOptionsDifficulty = difficultiesInVariation.map(function(item:String):String { - return item.toUpperCase(); - }).concat(pauseOptionsDifficultyBase); - - if (PlayStatePlaylist.campaignId == 'week6') - { - pauseMusic = new FlxSound().loadEmbedded(Paths.music('breakfast-pixel'), true, true); - } - else - { - pauseMusic = new FlxSound().loadEmbedded(Paths.music('breakfast'), true, true); - } - pauseMusic.volume = 0; - pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2))); - - FlxG.sound.list.add(pauseMusic); - - bg = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK); - bg.alpha = 0; - bg.scrollFactor.set(); - add(bg); - - metaDataGrp = new FlxTypedGroup(); - add(metaDataGrp); - - var levelInfo:FlxText = new FlxText(20, 15, 0, '', 32); - if (PlayState.instance.currentChart != null) - { - levelInfo.text += '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}'; - } - levelInfo.scrollFactor.set(); - levelInfo.setFormat(Paths.font('vcr.ttf'), 32); - levelInfo.updateHitbox(); - metaDataGrp.add(levelInfo); - - var levelDifficulty:FlxText = new FlxText(20, 15 + 32, 0, '', 32); - levelDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase(); - levelDifficulty.scrollFactor.set(); - levelDifficulty.setFormat(Paths.font('vcr.ttf'), 32); - levelDifficulty.updateHitbox(); - metaDataGrp.add(levelDifficulty); - - var deathCounter:FlxText = new FlxText(20, 15 + 64, 0, '', 32); - deathCounter.text = 'Blue balled: ${PlayState.instance.deathCounter}'; - FlxG.watch.addQuick('totalNotesHit', Highscore.tallies.totalNotesHit); - FlxG.watch.addQuick('totalNotes', Highscore.tallies.totalNotes); - deathCounter.scrollFactor.set(); - deathCounter.setFormat(Paths.font('vcr.ttf'), 32); - deathCounter.updateHitbox(); - metaDataGrp.add(deathCounter); - - practiceText = new FlxText(20, 15 + 64 + 32, 0, 'PRACTICE MODE', 32); - practiceText.scrollFactor.set(); - practiceText.setFormat(Paths.font('vcr.ttf'), 32); - practiceText.updateHitbox(); - practiceText.x = FlxG.width - (practiceText.width + 20); - practiceText.visible = PlayState.instance.isPracticeMode; - metaDataGrp.add(practiceText); - - levelDifficulty.alpha = 0; - levelInfo.alpha = 0; - deathCounter.alpha = 0; - - levelInfo.x = FlxG.width - (levelInfo.width + 20); - levelDifficulty.x = FlxG.width - (levelDifficulty.width + 20); - deathCounter.x = FlxG.width - (deathCounter.width + 20); - - FlxTween.tween(bg, {alpha: 0.6}, 0.4, {ease: FlxEase.quartInOut}); - FlxTween.tween(levelInfo, {alpha: 1, y: 20}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.3}); - FlxTween.tween(levelDifficulty, {alpha: 1, y: levelDifficulty.y + 5}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.5}); - FlxTween.tween(deathCounter, {alpha: 1, y: deathCounter.y + 5}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.7}); - - grpMenuShit = new FlxTypedGroup(); - add(grpMenuShit); - - regenMenu(); - - // cameras = [FlxG.cameras.list[FlxG.cameras.list.length - 1]]; + this.currentMode = params?.mode ?? Standard; } - function regenMenu():Void + // =============== + // Lifecycle Functions + // =============== + + /** + * Called when the state is first loaded. + */ + public override function create():Void { - while (grpMenuShit.members.length > 0) - { - grpMenuShit.remove(grpMenuShit.members[0], true); - } + super.create(); - for (i in 0...menuItems.length) - { - var songText:Alphabet = new Alphabet(0, (70 * i) + 30, menuItems[i], true, false); - songText.isMenuItem = true; - songText.targetY = i; - grpMenuShit.add(songText); - } + startPauseMusic(); - curSelected = 0; - changeSelection(); + buildBackground(); + + buildMetadata(); + + regenerateMenu(); + + transitionIn(); } - override function update(elapsed:Float):Void + /** + * Called every frame. + * @param elapsed The time elapsed since the last frame, in seconds. + */ + public override function update(elapsed:Float):Void { - if (pauseMusic.volume < 0.5) pauseMusic.volume += 0.01 * elapsed; - super.update(elapsed); handleInputs(); } + /** + * Called when the state is closed. + */ + public override function destroy():Void + { + super.destroy(); + pauseMusic.stop(); + } + + // =============== + // Initialization Functions + // =============== + + /** + * Play the pause music. + */ + function startPauseMusic():Void + { + var pauseMusicPath:String = Paths.music('breakfast$musicSuffix'); + pauseMusic = FunkinSound.load(pauseMusicPath, true, true); + + if (pauseMusic == null) + { + FlxG.log.warn('Could not play pause music: ${pauseMusicPath} does not exist!'); + } + + // Start playing at a random point in the song. + pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2))); + pauseMusic.fadeIn(MUSIC_FADE_IN_TIME, 0, MUSIC_FINAL_VOLUME); + } + + /** + * Render the semi-transparent black background. + */ + function buildBackground():Void + { + // Using state.bgColor causes bugs! + background = new FunkinSprite(0, 0); + background.makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK); + background.alpha = 0.0; + background.scrollFactor.set(0, 0); + background.updateHitbox(); + add(background); + } + + /** + * Render the metadata in the top right. + */ + function buildMetadata():Void + { + metadata = new FlxTypedSpriteGroup(); + metadata.scrollFactor.set(0, 0); + add(metadata); + + var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name - Artist'); + metadataSong.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); + if (PlayState.instance?.currentChart != null) + { + metadataSong.text = '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}'; + } + metadataSong.scrollFactor.set(0, 0); + metadata.add(metadataSong); + + var metadataDifficulty:FlxText = new FlxText(20, 15 + 32, FlxG.width - 40, 'Difficulty: '); + metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); + if (PlayState.instance?.currentDifficulty != null) + { + metadataDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase(); + } + metadataDifficulty.scrollFactor.set(0, 0); + metadata.add(metadataDifficulty); + + metadataDeaths = new FlxText(20, 15 + 64, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls'); + metadataDeaths.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); + metadataDeaths.scrollFactor.set(0, 0); + metadata.add(metadataDeaths); + + metadataPractice = new FlxText(20, 15 + 96, FlxG.width - 40, 'PRACTICE MODE'); + metadataPractice.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT); + metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false; + metadataPractice.scrollFactor.set(0, 0); + metadata.add(metadataPractice); + + updateMetadataText(); + } + + /** + * Perform additional animations to transition the pause menu in when it is first displayed. + */ + function transitionIn():Void + { + FlxTween.tween(background, {alpha: 0.6}, 0.8, {ease: FlxEase.quartOut}); + + // Animate each element a little bit downwards. + var delay:Float = 0.1; + for (child in metadata.members) + { + FlxTween.tween(child, {alpha: 1, y: child.y + 5}, 1.8, {ease: FlxEase.quartOut, startDelay: delay}); + delay += 0.1; + } + + new FlxTimer().start(0.2, (_) -> { + allowInput = true; + }); + } + + // =============== + // Input Handling + // =============== + + /** + * Process user inputs every frame. + */ function handleInputs():Void { - var upP = controls.UI_UP_P; - var downP = controls.UI_DOWN_P; - var accepted = controls.ACCEPT; + if (!allowInput) return; + + if (controls.UI_UP_P) + { + changeSelection(-1); + } + if (controls.UI_DOWN_P) + { + changeSelection(1); + } + + if (controls.ACCEPT) + { + currentMenuEntries[currentEntry].callback(this); + } + else if (controls.PAUSE) + { + resume(this); + } #if (debug || FORCE_DEBUG_VERSION) // to pause the game and get screenshots easy, press H on pause menu! if (FlxG.keys.justPressed.H) { - bg.visible = !bg.visible; - grpMenuShit.visible = !grpMenuShit.visible; - metaDataGrp.visible = !metaDataGrp.visible; + var visible = !metadata.visible; + + metadata.visible = visible; + menuEntryText.visible = visible; + this.bgColor = visible ? 0x99000000 : 0x00000000; // 60% or fully transparent black } #end - - if (!exitingToMenu) - { - if (upP) - { - changeSelection(-1); - } - if (downP) - { - changeSelection(1); - } - - var androidPause:Bool = false; - - #if android - androidPause = FlxG.android.justPressed.BACK; - #end - - if (androidPause) close(); - - if (accepted) - { - var daSelected:String = menuItems[curSelected]; - - switch (daSelected) - { - case 'Resume': - close(); - - case 'Change Difficulty': - menuItems = pauseOptionsDifficulty; - regenMenu(); - - case 'Toggle Practice Mode': - PlayState.instance.isPracticeMode = true; - practiceText.visible = PlayState.instance.isPracticeMode; - - case 'Restart Song': - PlayState.instance.needsReset = true; - close(); - - case 'Exit to Menu': - exitingToMenu = true; - PlayState.instance.deathCounter = 0; - - for (item in grpMenuShit.members) - { - item.targetY = -3; - item.alpha = 0.6; - } - - FlxTransitionableState.skipNextTransIn = true; - FlxTransitionableState.skipNextTransOut = true; - - if (PlayStatePlaylist.isStoryMode) - { - PlayStatePlaylist.reset(); - openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState(sticker))); - } - else - { - openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker))); - } - - case 'Exit to Chart Editor': - this.close(); - if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position! - PlayState.instance.close(); // This only works because PlayState is a substate! - - case 'BACK': - menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase; - regenMenu(); - - default: - if (pauseOptionsDifficulty.contains(daSelected)) - { - PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase()); - - // Reset campaign score when changing difficulty - // So if you switch difficulty on the last song of a week you get a really low overall score. - PlayStatePlaylist.campaignScore = 0; - PlayStatePlaylist.campaignDifficulty = daSelected.toLowerCase(); - PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty; - - PlayState.instance.needsReset = true; - - close(); - } - else - { - trace('[WARN] Unhandled pause menu option: ${daSelected}'); - } - } - } - - if (FlxG.keys.justPressed.J) - { - // for reference later! - // PlayerSettings.player1.controls.replaceBinding(Control.LEFT, Keys, FlxKey.J, null); - } - } - } - - override function destroy():Void - { - pauseMusic.destroy(); - - super.destroy(); } + /** + * Move the current selection up or down. + * @param change The amount to change the selection by, with sign indicating direction. + */ function changeSelection(change:Int = 0):Void { FlxG.sound.play(Paths.sound('scrollMenu'), 0.4); - curSelected += change; + currentEntry += change; - if (curSelected < 0) curSelected = menuItems.length - 1; - if (curSelected >= menuItems.length) curSelected = 0; + if (currentEntry < 0) currentEntry = currentMenuEntries.length - 1; + if (currentEntry >= currentMenuEntries.length) currentEntry = 0; - for (index => item in grpMenuShit.members) + for (entryIndex in 0...currentMenuEntries.length) { - item.targetY = index - curSelected; + var isCurrent:Bool = entryIndex == currentEntry; - item.alpha = 0.6; + var entry:PauseMenuEntry = currentMenuEntries[entryIndex]; + var text:AtlasText = entry.sprite; - if (item.targetY == 0) - { - item.alpha = 1; - } + // Set the transparency. + text.alpha = isCurrent ? 1.0 : 0.6; + + // Set the position. + var targetX = FlxMath.remapToRange((entryIndex - currentEntry), 0, 1, 0, 1.3) * 20 + 90; + var targetY = FlxMath.remapToRange((entryIndex - currentEntry), 0, 1, 0, 1.3) * 120 + (FlxG.height * 0.48); + trace(targetY); + FlxTween.globalManager.cancelTweensOf(text); + FlxTween.tween(text, {x: targetX, y: targetY}, 0.33, {ease: FlxEase.quartOut}); } } + + // =============== + // Menu Functions + // =============== + + /** + * Clear the current menu entries and regenerate them based on the current mode. + * @param targetMode Optionally specify a mode to switch to before regenerating the menu. + */ + function regenerateMenu(?targetMode:PauseMode):Void + { + // If targetMode is null, keep the current mode. + if (targetMode == null) targetMode = this.currentMode; + + var previousMode:PauseMode = this.currentMode; + this.currentMode = targetMode; + + resetSelection(); + chooseMenuEntries(); + clearAndAddMenuEntries(); + updateMetadataText(); + changeSelection(); + } + + /** + * Reset the current selection to the first entry. + */ + function resetSelection():Void + { + this.currentEntry = 0; + } + + /** + * Select which menu entries to display based on the current mode. + */ + function chooseMenuEntries():Void + { + // Choose the correct menu entries. + // NOTE: We clone the arrays to prevent modifications to the arrays from affecting the original. + switch (this.currentMode) + { + case PauseMode.Standard: + currentMenuEntries = PAUSE_MENU_ENTRIES_STANDARD.clone(); + case PauseMode.Charting: + currentMenuEntries = PAUSE_MENU_ENTRIES_CHARTING.clone(); + case PauseMode.Difficulty: + // Prepend the difficulties. + var entries:Array = []; + if (PlayState.instance.currentChart != null) + { + var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation); + trace('DIFFICULTIES: ${difficultiesInVariation}'); + for (difficulty in difficultiesInVariation) + { + entries.push({text: difficulty.toTitleCase(), callback: (state) -> changeDifficulty(state, difficulty)}); + } + } + + // Add the back button. + currentMenuEntries = entries.concat(PAUSE_MENU_ENTRIES_DIFFICULTY.clone()); + case PauseMode.Conversation: + currentMenuEntries = PAUSE_MENU_ENTRIES_CONVERSATION.clone(); + case PauseMode.Cutscene: + currentMenuEntries = PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE.clone(); + } + } + + /** + * Clear the `menuEntryText` group and render the current menu entries to it. + * We first create the `menuEntryText` group if it doesn't already exist. + */ + function clearAndAddMenuEntries():Void + { + if (menuEntryText == null) + { + menuEntryText = new FlxTypedSpriteGroup(); + menuEntryText.scrollFactor.set(0, 0); + add(menuEntryText); + } + menuEntryText.clear(); + + // Render out the entries depending on the mode. + var entryIndex:Int = 0; + var toRemove = []; + for (entry in currentMenuEntries) + { + if (entry == null || (entry.filter != null && !entry.filter())) + { + // Remove entries that should be hidden. + toRemove.push(entry); + } + else + { + // Handle visible entries. + var yPos:Float = 70 * entryIndex + 30; + var text:AtlasText = new AtlasText(0, yPos, entry.text, AtlasFont.BOLD); + text.scrollFactor.set(0, 0); + text.alpha = 0; + menuEntryText.add(text); + + entry.sprite = text; + + entryIndex++; + } + } + for (entry in toRemove) + { + currentMenuEntries.remove(entry); + } + } + + // =============== + // Metadata Functions + // =============== + + /** + * Update the values for the metadata text in the top right. + */ + function updateMetadataText():Void + { + metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false; + + switch (this.currentMode) + { + case Standard | Difficulty: + metadataDeaths.text = '${PlayState.instance?.deathCounter} Blue Balls'; + case Charting: + metadataDeaths.text = 'Chart Editor Preview'; + case Conversation: + metadataDeaths.text = 'Dialogue Paused'; + case Cutscene: + metadataDeaths.text = 'Video Paused'; + } + } + + // =============== + // Menu Callbacks + // =============== + + /** + * Close the pause menu and resume the game. + * @param state The current PauseSubState. + */ + static function resume(state:PauseSubState):Void + { + // Resume a paused video if it exists. + VideoCutscene.resumeVideo(); + + state.close(); + } + + /** + * Switch the pause menu to the indicated mode. + * Create a callback from this using `.bind(_, targetMode)`. + * @param state The current PauseSubState. + * @param targetMode The mode to switch to. + */ + static function switchMode(state:PauseSubState, targetMode:PauseMode):Void + { + state.regenerateMenu(targetMode); + } + + /** + * Switch the game's difficulty to the indicated difficulty, then resume the game. + * @param state The current PauseSubState. + * @param difficulty The difficulty to switch to. + */ + static function changeDifficulty(state:PauseSubState, difficulty:String):Void + { + PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase()); + + // Reset campaign score when changing difficulty + // So if you switch difficulty on the last song of a week you get a really low overall score. + PlayStatePlaylist.campaignScore = 0; + PlayStatePlaylist.campaignDifficulty = difficulty; + PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty; + + PlayState.instance.needsReset = true; + + state.close(); + } + + /** + * Restart the current level, then resume the game. + * @param state The current PauseSubState. + */ + static function restartPlayState(state:PauseSubState):Void + { + PlayState.instance.needsReset = true; + state.close(); + } + + /** + * Force the game into practice mode, then update the pause menu. + * @param state The current PauseSubState. + */ + static function enablePracticeMode(state:PauseSubState):Void + { + if (PlayState.instance == null) return; + + PlayState.instance.isPracticeMode = true; + state.regenerateMenu(); + } + + /** + * Restart the paused video cutscene, then resume the game. + * @param state The current PauseSubState. + */ + static function restartVideoCutscene(state:PauseSubState):Void + { + VideoCutscene.restartVideo(); + state.close(); + } + + /** + * Skip the paused video cutscene, then resume the game. + * @param state The current PauseSubState. + */ + static function skipVideoCutscene(state:PauseSubState):Void + { + VideoCutscene.finishVideo(); + state.close(); + } + + /** + * Restart the paused conversation, then resume the game. + * @param state The current PauseSubState. + */ + static function restartConversation(state:PauseSubState):Void + { + if (PlayState.instance?.currentConversation == null) return; + + PlayState.instance.currentConversation.resetConversation(); + state.close(); + } + + /** + * Skip the paused conversation, then resume the game. + * @param state The current PauseSubState. + */ + static function skipConversation(state:PauseSubState):Void + { + if (PlayState.instance?.currentConversation == null) return; + + PlayState.instance.currentConversation.skipConversation(); + state.close(); + } + + /** + * Quit the game and return to the main menu. + * @param state The current PauseSubState. + */ + static function quitToMenu(state:PauseSubState):Void + { + state.allowInput = false; + + PlayState.instance.deathCounter = 0; + + FlxTransitionableState.skipNextTransIn = true; + FlxTransitionableState.skipNextTransOut = true; + + if (PlayStatePlaylist.isStoryMode) + { + PlayStatePlaylist.reset(); + state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState(sticker))); + } + else + { + state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker))); + } + } + + /** + * Quit the game and return to the chart editor. + * @param state The current PauseSubState. + */ + static function quitToChartEditor(state:PauseSubState):Void + { + state.close(); + if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position! + PlayState.instance.close(); // This only works because PlayState is a substate! + } } + +/** + * Which set of options the pause menu should display. + */ +enum PauseMode +{ + /** + * The menu displayed when the player pauses the game during a song. + */ + Standard; + + /** + * The menu displayed when the player pauses the game during a song while in charting mode. + */ + Charting; + + /** + * The menu displayed when the player moves to change the game's difficulty. + */ + Difficulty; + + /** + * The menu displayed when the player pauses the game during a conversation. + */ + Conversation; + + /** + * The menu displayed when the player pauses the game during a video cutscene. + */ + Cutscene; +} + +/** + * Represents a single entry in the pause menu. + */ +typedef PauseMenuEntry = +{ + /** + * The text to display for this entry. + * TODO: Implement localization. + */ + var text:String; + + /** + * The callback to execute when the user selects this entry. + */ + var callback:PauseSubState->Void; + + /** + * If this returns true, the entry will be displayed. If it returns false, the entry will be hidden. + */ + var ?filter:Void->Bool; + + // Instance-specific properties + + /** + * The text object currently displaying this entry. + */ + var ?sprite:AtlasText; +}; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index a745be6cd..36e96d580 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -322,6 +322,11 @@ class PlayState extends MusicBeatSubState **/ var inputReleaseQueue:Array = []; + /** + * If we just unpaused the game, we shouldn't be able to pause again for one frame. + */ + var justUnpaused:Bool = false; + /** * PRIVATE INSTANCE VARIABLES * Private instance variables should be used for information that must be reset or dereferenced @@ -450,11 +455,6 @@ class PlayState extends MusicBeatSubState */ var comboPopUps:PopUpStuff; - /** - * The circular sprite that appears while the user is holding down the Skip Cutscene button. - */ - var skipTimer:FlxPieDial; - /** * PROPERTIES */ @@ -482,7 +482,7 @@ class PlayState extends MusicBeatSubState if (!Std.isOfType(this.subState, PauseSubState)) return false; var pauseSubState:PauseSubState = cast this.subState; - return pauseSubState.exitingToMenu; + return !pauseSubState.allowInput; } /** @@ -637,16 +637,9 @@ class PlayState extends MusicBeatSubState // Initialize the judgements and combo meter. comboPopUps = new PopUpStuff(); - comboPopUps.cameras = [camHUD]; + comboPopUps.zIndex = 900; add(comboPopUps); - - // The little dial that shows up when you hold the Skip Cutscene key. - skipTimer = new FlxPieDial(16, 16, 32, FlxColor.WHITE, 36, CIRCLE, true, 24); - skipTimer.amount = 0; - skipTimer.zIndex = 1000; - add(skipTimer); - // Renders only in video cutscene mode. - skipTimer.cameras = [camCutscene]; + comboPopUps.cameras = [camHUD]; #if discord_rpc // Initialize Discord Rich Presence. @@ -763,6 +756,8 @@ class PlayState extends MusicBeatSubState public override function update(elapsed:Float):Void { + // TOTAL: 9.42% CPU Time when profiled in VS 2019. + if (criticalFailure) return; super.update(elapsed); @@ -787,8 +782,7 @@ class PlayState extends MusicBeatSubState inputSpitter = []; // Reset music properly. - - FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instance.instrumentalOffset); + FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; FlxG.sound.music.pause(); if (!overrideMusic) @@ -864,7 +858,7 @@ class PlayState extends MusicBeatSubState #end // Attempt to pause the game. - if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame) + if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame && !justUnpaused) { var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000)); @@ -899,12 +893,12 @@ class PlayState extends MusicBeatSubState boyfriendPos = currentStage.getBoyfriend().getScreenPosition(); } - var pauseSubState:FlxSubState = new PauseSubState(isChartingMode); + var pauseSubState:FlxSubState = new PauseSubState({mode: isChartingMode ? Charting : Standard}); FlxTransitionableSubState.skipNextTransIn = true; FlxTransitionableSubState.skipNextTransOut = true; - openSubState(pauseSubState); pauseSubState.camera = camHUD; + openSubState(pauseSubState); // boyfriendPos.put(); // TODO: Why is this here? } @@ -1002,6 +996,8 @@ class PlayState extends MusicBeatSubState // Moving notes into position is now done by Strumline.update(). processNotes(elapsed); + + justUnpaused = false; } function processSongEvents():Void @@ -1076,7 +1072,10 @@ class PlayState extends MusicBeatSubState if (FlxG.sound.music != null) { musicPausedBySubState = FlxG.sound.music.playing; - FlxG.sound.music.pause(); + if (musicPausedBySubState) + { + FlxG.sound.music.pause(); + } if (vocals != null) vocals.pause(); } @@ -1104,7 +1103,12 @@ class PlayState extends MusicBeatSubState // Resume if (musicPausedBySubState) { - FlxG.sound.music.play(FlxG.sound.music.time); + FlxG.sound.music.play(); + } + + if (currentConversation != null) + { + currentConversation.resumeMusic(); } if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals(); @@ -1123,6 +1127,8 @@ class PlayState extends MusicBeatSubState DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC); } #end + + justUnpaused = true; } else if (Std.isOfType(subState, Transition)) { @@ -1283,6 +1289,7 @@ class PlayState extends MusicBeatSubState { var animShit:ComboMilestone = new ComboMilestone(-100, 300, Highscore.tallies.combo); animShit.scrollFactor.set(0.6, 0.6); + animShit.zIndex = 1100; animShit.cameras = [camHUD]; add(animShit); @@ -1304,12 +1311,6 @@ class PlayState extends MusicBeatSubState public override function destroy():Void { - if (currentConversation != null) - { - remove(currentConversation); - currentConversation.kill(); - } - performCleanup(); super.destroy(); @@ -1369,18 +1370,21 @@ class PlayState extends MusicBeatSubState healthBarBG = FunkinSprite.create(0, healthBarYPos, Paths.image('healthBar')); healthBarBG.screenCenter(X); healthBarBG.scrollFactor.set(0, 0); + healthBarBG.zIndex = 800; add(healthBarBG); healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this, 'healthLerp', 0, 2); healthBar.scrollFactor.set(); healthBar.createFilledBar(Constants.COLOR_HEALTH_BAR_RED, Constants.COLOR_HEALTH_BAR_GREEN); + healthBar.zIndex = 801; add(healthBar); // The score text below the health bar. scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, '', 20); scoreText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); scoreText.scrollFactor.set(); + scoreText.zIndex = 802; add(scoreText); // Move the health bar to the HUD camera. @@ -1494,6 +1498,7 @@ class PlayState extends MusicBeatSubState iconP2 = new HealthIcon('dad', 1); iconP2.y = healthBar.y - (iconP2.height / 2); dad.initHealthIcon(true); // Apply the character ID here + iconP2.zIndex = 850; add(iconP2); iconP2.cameras = [camHUD]; @@ -1513,6 +1518,7 @@ class PlayState extends MusicBeatSubState iconP1 = new HealthIcon('bf', 0); iconP1.y = healthBar.y - (iconP1.height / 2); boyfriend.initHealthIcon(false); // Apply the character ID here + iconP1.zIndex = 850; add(iconP1); iconP1.cameras = [camHUD]; @@ -1580,13 +1586,13 @@ class PlayState extends MusicBeatSubState playerStrumline.x = FlxG.width / 2 + Constants.STRUMLINE_X_OFFSET; // Classic style // playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; // Centered style playerStrumline.y = Preferences.downscroll ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET; - playerStrumline.zIndex = 200; + playerStrumline.zIndex = 1001; playerStrumline.cameras = [camHUD]; // Position the opponent strumline on the left half of the screen opponentStrumline.x = Constants.STRUMLINE_X_OFFSET; opponentStrumline.y = Preferences.downscroll ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET; - opponentStrumline.zIndex = 100; + opponentStrumline.zIndex = 1000; opponentStrumline.cameras = [camHUD]; if (!PlayStatePlaylist.isStoryMode) @@ -1733,6 +1739,7 @@ class PlayState extends MusicBeatSubState currentConversation = ConversationRegistry.instance.fetchEntry(conversationId); if (currentConversation == null) return; + if (!currentConversation.alive) currentConversation.revive(); currentConversation.completeCallback = onConversationComplete; currentConversation.cameras = [camCutscene]; @@ -1750,8 +1757,13 @@ class PlayState extends MusicBeatSubState function onConversationComplete():Void { isInCutscene = false; - remove(currentConversation); - currentConversation = null; + + if (currentConversation != null) + { + currentConversation.kill(); + remove(currentConversation); + currentConversation = null; + } if (startingSong && !isInCountdown) { @@ -1776,12 +1788,14 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.onComplete = endSong.bind(false); // A negative instrumental offset means the song skips the first few milliseconds of the track. // This just gets added into the startTimestamp behavior so we don't need to do anything extra. - FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; + FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset); + + // I am going insane. + FlxG.sound.music.volume = 1.0; + FlxG.sound.music.fadeTween.cancel(); trace('Playing vocals...'); add(vocals); - - FlxG.sound.music.play(FlxG.sound.music.time); vocals.play(); resyncVocals(); @@ -2459,58 +2473,43 @@ class PlayState extends MusicBeatSubState */ function handleCutsceneKeys(elapsed:Float):Void { + if (isGamePaused) return; + if (currentConversation != null) { - if (controls.CUTSCENE_ADVANCE) currentConversation?.advanceConversation(); - - if (controls.CUTSCENE_SKIP) + // Pause/unpause may conflict with advancing the conversation! + if (controls.CUTSCENE_ADVANCE && !justUnpaused) { - currentConversation?.trySkipConversation(elapsed); + currentConversation.advanceConversation(); } - else + else if (controls.PAUSE && !justUnpaused) { - currentConversation?.trySkipConversation(-1); + currentConversation.pauseMusic(); + + var pauseSubState:FlxSubState = new PauseSubState({mode: Conversation}); + + persistentUpdate = false; + FlxTransitionableSubState.skipNextTransIn = true; + FlxTransitionableSubState.skipNextTransOut = true; + pauseSubState.camera = camCutscene; + openSubState(pauseSubState); } } else if (VideoCutscene.isPlaying()) { // This is a video cutscene. - - if (controls.CUTSCENE_SKIP) + if (controls.PAUSE && !justUnpaused) { - trySkipVideoCutscene(elapsed); + VideoCutscene.pauseVideo(); + + var pauseSubState:FlxSubState = new PauseSubState({mode: Cutscene}); + + persistentUpdate = false; + FlxTransitionableSubState.skipNextTransIn = true; + FlxTransitionableSubState.skipNextTransOut = true; + pauseSubState.camera = camCutscene; + openSubState(pauseSubState); } - else - { - trySkipVideoCutscene(-1); - } - } - } - - /** - * Handle logic for the skip timer. - * If the skip button is being held, pass the amount of time elapsed since last game update. - * If the skip button has been released, pass a negative number. - */ - function trySkipVideoCutscene(elapsed:Float):Void - { - if (skipTimer == null || skipTimer.animation == null) return; - - if (elapsed < 0) - { - skipHeldTimer = 0.0; - } - else - { - skipHeldTimer += elapsed; - } - - skipTimer.visible = skipHeldTimer >= 0.05; - skipTimer.amount = Math.min(skipHeldTimer / 1.5, 1.0); - - if (skipHeldTimer >= 1.5) - { - skipVideoCutscene(); } } @@ -2724,6 +2723,12 @@ class PlayState extends MusicBeatSubState */ function performCleanup():Void { + if (currentConversation != null) + { + remove(currentConversation); + currentConversation.kill(); + } + if (currentChart != null) { // TODO: Uncache the song. @@ -2740,7 +2745,7 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.pause(); if (vocals != null) { - vocals.pause(); + vocals.destroy(); remove(vocals); } } @@ -2755,6 +2760,7 @@ class PlayState extends MusicBeatSubState } GameOverSubState.reset(); + PauseSubState.reset(); // Clear the static reference to this state. instance = null; diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index f9dc18119..418982bef 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -191,7 +191,11 @@ class AnimateAtlasCharacter extends BaseCharacter _skipTransformChildren = true; super.kill(); _skipTransformChildren = false; - this.mainSprite.kill(); + if (this.mainSprite != null) + { + this.mainSprite.kill(); + this.mainSprite = null; + } } /** diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx index 88ffa468c..faab5e4dc 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -5,6 +5,7 @@ import flixel.group.FlxGroup.FlxTypedGroup; import flixel.tweens.FlxTween; import funkin.graphics.FunkinSprite; import funkin.play.PlayState; +import flixel.util.FlxDirection; class PopUpStuff extends FlxTypedGroup { @@ -30,14 +31,7 @@ class PopUpStuff extends FlxTypedGroup rating.zIndex = 1000; rating.x = FlxG.width * 0.50; - rating.x -= FlxG.camera.scroll.x * 0.2; - // make sure rating is visible lol! - // if (rating.x < FlxG.camera.scroll.x) - // rating.x = FlxG.camera.scroll.x; - // else if (rating.x > FlxG.camera.scroll.x + FlxG.camera.width - rating.width) - // rating.x = FlxG.camera.scroll.x + FlxG.camera.width - rating.width; - - // FlxG.camera.scroll.y + + // rating.x -= FlxG.camera.scroll.x * 0.2; rating.y = FlxG.camera.height * 0.4 - 60; rating.acceleration.y = 550; rating.velocity.y -= FlxG.random.int(140, 175); @@ -91,13 +85,7 @@ class PopUpStuff extends FlxTypedGroup var comboSpr:FunkinSprite = FunkinSprite.create(Paths.image(pixelShitPart1 + 'combo' + pixelShitPart2)); comboSpr.y = FlxG.camera.height * 0.4 + 80; comboSpr.x = FlxG.width * 0.50; - comboSpr.x -= FlxG.camera.scroll.x * 0.2; - // make sure combo is visible lol! - // 194 fits 4 combo digits - // if (comboSpr.x < FlxG.camera.scroll.x + 194) - // comboSpr.x = FlxG.camera.scroll.x + 194; - // else if (comboSpr.x > FlxG.camera.scroll.x + FlxG.camera.width - comboSpr.width) - // comboSpr.x = FlxG.camera.scroll.x + FlxG.camera.width - comboSpr.width; + // comboSpr.x -= FlxG.camera.scroll.x * 0.2; comboSpr.acceleration.y = 600; comboSpr.velocity.y -= 150; diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx index 75e69bf04..2d31b0a28 100644 --- a/source/funkin/play/cutscene/VideoCutscene.hx +++ b/source/funkin/play/cutscene/VideoCutscene.hx @@ -112,6 +112,7 @@ class VideoCutscene { vid.zIndex = 0; vid.bitmap.onEndReached.add(finishVideo.bind(0.5)); + vid.autoPause = false; vid.cameras = [PlayState.instance.camCutscene]; @@ -136,6 +137,63 @@ class VideoCutscene } #end + public static function restartVideo(resume:Bool = true):Void + { + #if html5 + if (vid != null) + { + vid.restartVideo(); + } + #end + + #if hxCodec + if (vid != null) + { + // Seek to the start of the video. + vid.bitmap.time = 0; + if (resume) + { + // Resume the video if it was paused. + vid.resume(); + } + } + #end + } + + public static function pauseVideo():Void + { + #if html5 + if (vid != null) + { + vid.pauseVideo(); + } + #end + + #if hxCodec + if (vid != null) + { + vid.pause(); + } + #end + } + + public static function resumeVideo():Void + { + #if html5 + if (vid != null) + { + vid.resumeVideo(); + } + #end + + #if hxCodec + if (vid != null) + { + vid.resume(); + } + #end + } + /** * Finish the active video cutscene. Done when the video is finished or when the player skips the cutscene. * @param transitionTime The duration of the transition to the next state. Defaults to 0.5 seconds (this time is always used when cancelling the video). diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx index dc3fd8c8a..f865f3b7b 100644 --- a/source/funkin/play/cutscene/dialogue/Conversation.hx +++ b/source/funkin/play/cutscene/dialogue/Conversation.hx @@ -31,10 +31,6 @@ import funkin.data.dialogue.DialogueBoxRegistry; */ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry { - static final CONVERSATION_SKIP_TIMER:Float = 1.5; - - var skipHeldTimer:Float = 0.0; - /** * The ID of the conversation. */ @@ -105,8 +101,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl var currentDialogueBox:DialogueBox; - var skipTimer:FlxPieDial; - public function new(id:String) { super(); @@ -124,8 +118,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl { // Reset the progress in the dialogue. currentDialogueEntry = 0; + currentDialogueLine = 0; this.state = ConversationState.Start; - this.alpha = 1.0; // Start the dialogue. dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, false)); @@ -151,8 +145,31 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl music.play(); } + public function pauseMusic():Void + { + if (music != null) + { + music.pause(); + } + } + + public function resumeMusic():Void + { + if (music != null) + { + music.resume(); + } + } + function setupBackdrop():Void { + if (backdrop != null) + { + backdrop.destroy(); + remove(backdrop); + backdrop = null; + } + backdrop = new FunkinSprite(0, 0); if (_data.backdrop == null) return; @@ -181,12 +198,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl refresh(); } - function setupSkipTimer():Void - { - add(skipTimer = new FlxPieDial(16, 16, 32, FlxColor.WHITE, 36, CIRCLE, true, 24)); - skipTimer.amount = 0; - } - public override function update(elapsed:Float):Void { super.update(elapsed); @@ -199,9 +210,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl var nextSpeakerId:String = currentDialogueEntryData.speaker; // Skip the next steps if the current speaker is already displayed. - if (currentSpeaker != null && nextSpeakerId == currentSpeaker.id) return; - - var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId); + if ((currentSpeaker != null && currentSpeaker.alive) && nextSpeakerId == currentSpeaker.id) return; if (currentSpeaker != null) { @@ -210,6 +219,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl currentSpeaker = null; } + var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId); + if (nextSpeaker == null) { if (nextSpeakerId == null) @@ -222,6 +233,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl } return; } + if (!nextSpeaker.alive) nextSpeaker.revive(); ScriptEventDispatcher.callEvent(nextSpeaker, new ScriptEvent(CREATE, true)); @@ -249,8 +261,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl { var nextDialogueBoxId:String = currentDialogueEntryData?.box; - // Skip the next steps if the current speaker is already displayed. - if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.id) return; + // Skip the next steps if the current dialogue box is already displayed. + if ((currentDialogueBox != null && currentDialogueBox.alive) && nextDialogueBoxId == currentDialogueBox.id) return; if (currentDialogueBox != null) { @@ -266,6 +278,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl trace('Dialogue box could not be retrieved.'); return; } + if (!nextDialogueBox.alive) nextDialogueBox.revive(); ScriptEventDispatcher.callEvent(nextDialogueBox, new ScriptEvent(CREATE, true)); @@ -347,29 +360,28 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl currentDialogueEntry = 0; this.state = ConversationState.Start; - advanceConversation(); - } - - public function trySkipConversation(elapsed:Float):Void - { - if (skipTimer == null || skipTimer.animation == null) return; - - if (elapsed < 0) + if (outroTween != null) { - skipHeldTimer = 0.0; - } - else - { - skipHeldTimer += elapsed; + outroTween.cancel(); } + outroTween = null; - skipTimer.visible = skipHeldTimer >= 0.05; - skipTimer.amount = Math.min(skipHeldTimer / CONVERSATION_SKIP_TIMER, 1.0); + if (this.music != null) this.music.stop(); + this.music = null; - if (skipHeldTimer >= CONVERSATION_SKIP_TIMER) - { - skipConversation(); - } + if (currentSpeaker != null) currentSpeaker.kill(); + remove(currentSpeaker); + currentSpeaker = null; + + if (currentDialogueBox != null) currentDialogueBox.kill(); + remove(currentDialogueBox); + currentDialogueBox = null; + + if (backdrop != null) backdrop.destroy(); + remove(backdrop); + backdrop = null; + + startConversation(); } /** @@ -383,7 +395,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl dispatchEvent(new DialogueScriptEvent(DIALOGUE_SKIP, this, true)); } - static var outroTween:FlxTween; + var outroTween:FlxTween; public function startOutro():Void { @@ -411,7 +423,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl public function endOutro():Void { - outroTween = null; ScriptEventDispatcher.callEvent(this, new ScriptEvent(DESTROY, false)); } @@ -425,7 +436,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl // Fade in the music and backdrop. setupMusic(); setupBackdrop(); - setupSkipTimer(); // Advance the conversation. state = ConversationState.Opening; @@ -547,19 +557,25 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl { propagateEvent(event); - if (outroTween != null) outroTween.cancel(); // Canc + if (outroTween != null) + { + outroTween.cancel(); + } outroTween = null; - this.alpha = 0.0; if (this.music != null) this.music.stop(); this.music = null; - this.skipTimer = null; if (currentSpeaker != null) currentSpeaker.kill(); + remove(currentSpeaker); currentSpeaker = null; + if (currentDialogueBox != null) currentDialogueBox.kill(); + remove(currentDialogueBox); currentDialogueBox = null; - if (backdrop != null) backdrop.kill(); + + if (backdrop != null) backdrop.destroy(); + remove(backdrop); backdrop = null; this.clear(); @@ -578,16 +594,27 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl */ function propagateEvent(event:ScriptEvent):Void { - if (this.currentDialogueBox != null) + if (this.currentDialogueBox != null && this.currentDialogueBox.exists) { ScriptEventDispatcher.callEvent(this.currentDialogueBox, event); } - if (this.currentSpeaker != null) + if (this.currentSpeaker != null && this.currentSpeaker.exists) { ScriptEventDispatcher.callEvent(this.currentSpeaker, event); } } + /** + * Calls `kill()` on the group's members and then on the group itself. + * You can revive this group later via `revive()` after this. + */ + public override function revive():Void + { + super.revive(); + this.alpha = 1; + this.visible = true; + } + /** * Calls `kill()` on the group's members and then on the group itself. * You can revive this group later via `revive()` after this. @@ -599,6 +626,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl exists = false; _skipTransformChildren = false; if (group != null) group.kill(); + + if (outroTween != null) + { + outroTween.cancel(); + outroTween = null; + } } public override function toString():String diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx index 6f8a0086a..fe4f13be0 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx @@ -4,10 +4,11 @@ import flixel.FlxSprite; import funkin.data.IRegistryEntry; import flixel.group.FlxSpriteGroup; import flixel.graphics.frames.FlxFramesCollection; -import flixel.text.FlxText; +import funkin.graphics.FunkinSprite; import flixel.addons.text.FlxTypeText; import funkin.util.assets.FlxAnimationUtil; import funkin.modding.events.ScriptEvent; +import funkin.audio.FunkinSound; import funkin.modding.IScriptedClass.IDialogueScriptedClass; import flixel.util.FlxColor; import funkin.data.dialogue.DialogueBoxData; @@ -111,9 +112,6 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple this.y = 0; this.alpha = 1; - this.boxSprite = new FlxSprite(0, 0); - add(this.boxSprite); - loadSpritesheet(); loadAnimations(); @@ -122,6 +120,14 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple function loadSpritesheet():Void { + if (this.boxSprite != null) + { + remove(this.boxSprite); + this.boxSprite = null; + } + + this.boxSprite = new FunkinSprite(0, 0); + trace('[DIALOGUE BOX] Loading spritesheet ${_data.assetPath} for ${id}'); var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath); @@ -146,6 +152,8 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple this.flipY = _data.flipY; this.globalOffsets = _data.offsets; this.setScale(_data.scale); + + add(this.boxSprite); } public function setText(newText:String):Void @@ -190,6 +198,34 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple this.boxSprite.updateHitbox(); } + /** + * Calls `kill()` on the group's members and then on the group itself. + * You can revive this group later via `revive()` after this. + */ + public override function kill():Void + { + super.kill(); + if (this.boxSprite != null) + { + this.boxSprite.kill(); + this.boxSprite = null; + } + if (this.textDisplay != null) + { + this.textDisplay.kill(); + this.textDisplay = null; + } + this.clear(); + } + + public override function revive():Void + { + super.revive(); + + this.visible = true; + this.alpha = 1.0; + } + function loadAnimations():Void { trace('[DIALOGUE BOX] Loading ${_data.animations.length} animations for ${id}'); @@ -246,7 +282,8 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW, FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false); textDisplay.borderSize = _data.text.shadowWidth ?? 2; - textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)]; + // TODO: Add an option to configure this. + textDisplay.sounds = [FunkinSound.load(Paths.sound('pixelText'), 0.6)]; textDisplay.completeCallback = onTypingComplete; diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx index d5bffd7b0..f848d79c8 100644 --- a/source/funkin/play/cutscene/dialogue/Speaker.hx +++ b/source/funkin/play/cutscene/dialogue/Speaker.hx @@ -106,6 +106,26 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe loadAnimations(); } + /** + * Calls `kill()` on the group's members and then on the group itself. + * You can revive this group later via `revive()` after this. + */ + public override function kill():Void + { + super.kill(); + } + + public override function revive():Void + { + super.revive(); + + this.visible = true; + this.alpha = 1.0; + + loadSpritesheet(); + loadAnimations(); + } + function loadSpritesheet():Void { trace('[SPEAKER] Loading spritesheet ${_data.assetPath} for ${id}'); diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 970aebc57..61f83d1ed 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -502,11 +502,11 @@ class SongDifficulty } } - public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void + public function playInst(volume:Float = 1.0, looped:Bool = false):Void { var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; - FlxG.sound.music = FunkinSound.load(Paths.inst(this.song.id, suffix), volume, looped); + FlxG.sound.music = FunkinSound.load(Paths.inst(this.song.id, suffix), volume, looped, false, true); // Workaround for a bug where FlxG.sound.music.update() was being called twice. FlxG.sound.list.remove(FlxG.sound.music); @@ -614,9 +614,9 @@ class SongDifficulty } // Add player vocals. - if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(Assets.getSound(voiceList[0]))); + if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(voiceList[0])); // Add opponent vocals. - if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(Assets.getSound(voiceList[1]))); + if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(voiceList[1])); // Add additional vocals. if (voiceList.length > 2) diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index ff5e5bfb8..6246dcb58 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -893,12 +893,6 @@ typedef SaveControlsData = */ var ?CUTSCENE_ADVANCE:Array; - /** - * Keybind for skipping a cutscene. - * @default `Escape` - */ - var ?CUTSCENE_SKIP:Array; - /** * Keybind for increasing volume. * @default `Plus` diff --git a/source/funkin/save/migrator/RawSaveData_v1_0_0.hx b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx index b71102cce..a516f944a 100644 --- a/source/funkin/save/migrator/RawSaveData_v1_0_0.hx +++ b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx @@ -35,7 +35,6 @@ typedef SaveControlsData_v1_0_0 = var ?ACCEPT:Array; var ?BACK:Array; var ?CUTSCENE_ADVANCE:Array; - var ?CUTSCENE_SKIP:Array; var ?NOTE_DOWN:Array; var ?NOTE_LEFT:Array; var ?NOTE_RIGHT:Array; diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index 92bee4ceb..f995660f7 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -272,7 +272,6 @@ class SaveDataMigrator ACCEPT: controlsData?.keys?.ACCEPT ?? null, BACK: controlsData?.keys?.BACK ?? null, CUTSCENE_ADVANCE: controlsData?.keys?.CUTSCENE_ADVANCE ?? null, - CUTSCENE_SKIP: controlsData?.keys?.CUTSCENE_SKIP ?? null, NOTE_DOWN: controlsData?.keys?.NOTE_DOWN ?? null, NOTE_LEFT: controlsData?.keys?.NOTE_LEFT ?? null, NOTE_RIGHT: controlsData?.keys?.NOTE_RIGHT ?? null, @@ -293,7 +292,6 @@ class SaveDataMigrator ACCEPT: controlsData?.pad?.ACCEPT ?? null, BACK: controlsData?.pad?.BACK ?? null, CUTSCENE_ADVANCE: controlsData?.pad?.CUTSCENE_ADVANCE ?? null, - CUTSCENE_SKIP: controlsData?.pad?.CUTSCENE_SKIP ?? null, NOTE_DOWN: controlsData?.pad?.NOTE_DOWN ?? null, NOTE_LEFT: controlsData?.pad?.NOTE_LEFT ?? null, NOTE_RIGHT: controlsData?.pad?.NOTE_RIGHT ?? null, diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx index 2c8970357..dc742874f 100644 --- a/source/funkin/ui/MusicBeatSubState.hx +++ b/source/funkin/ui/MusicBeatSubState.hx @@ -51,6 +51,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler override function update(elapsed:Float):Void { + // 3.59% CPU Usage (100% is FlxTypedGroup#update() and most of that is updating each member.) super.update(elapsed); // Emergency exit button. @@ -61,8 +62,11 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler // Display Conductor info in the watch window. FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); + + // 0.09% CPU Usage? Conductor.watchQuick(); + // 4.31% CPU Usage dispatchEvent(new UpdateScriptEvent(elapsed)); } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 768ea9e43..e46779483 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -2838,12 +2838,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menuBarItemInputStyleNone.onClick = function(event:UIEvent) { currentLiveInputStyle = None; }; + menuBarItemInputStyleNone.selected = currentLiveInputStyle == None; menuBarItemInputStyleNumberKeys.onClick = function(event:UIEvent) { currentLiveInputStyle = NumberKeys; }; + menuBarItemInputStyleNumberKeys.selected = currentLiveInputStyle == NumberKeys; menuBarItemInputStyleWASD.onClick = function(event:UIEvent) { - currentLiveInputStyle = WASD; + currentLiveInputStyle = WASDKeys; }; + menuBarItemInputStyleWASD.selected = currentLiveInputStyle == WASDKeys; menubarItemAbout.onClick = _ -> this.openAboutDialog(); menubarItemWelcomeDialog.onClick = _ -> this.openWelcomeDialog(true); @@ -2942,7 +2945,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemPlaybackSpeed.onChange = event -> { var pitch:Float = (event.value.toFloat() * 2.0) / 100.0; - pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25. + pitch = Math.floor(pitch / 0.05) * 0.05; // Round to nearest 5% + pitch = Math.max(0.05, Math.min(2.0, pitch)); // Clamp to 5% to 200% #if FLX_PITCH if (audioInstTrack != null) audioInstTrack.pitch = pitch; audioVocalTrackGroup.pitch = pitch; @@ -4933,7 +4937,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Place notes at the playhead. switch (currentLiveInputStyle) { - case ChartEditorLiveInputStyle.WASD: + case ChartEditorLiveInputStyle.WASDKeys: if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5); if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6); @@ -5236,6 +5240,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } // Would bind Ctrl+A and Ctrl+D here, but they are already bound to Select All and Select None. } + else + { + trace('Ignoring keybinds for View menu items because we are in live input mode (${currentLiveInputStyle}).'); + } } /** @@ -6125,7 +6133,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } /** - * Available input modes for the chart editor state. + * Available input modes for the chart editor state. Numbers/arrows/WASD available for other keybinds. */ enum ChartEditorLiveInputStyle { @@ -6140,9 +6148,9 @@ enum ChartEditorLiveInputStyle NumberKeys; /** - * WASD to place notes on opponent's side, arrow keys to place notes on player's side. + * WASD to place notes on opponent's side, Arrow keys to place notes on player's side. */ - WASD; + WASDKeys; } typedef ChartEditorParams = diff --git a/source/funkin/ui/debug/dialogue/ConversationDebugState.hx b/source/funkin/ui/debug/dialogue/ConversationDebugState.hx index 33a6f365a..f165865c4 100644 --- a/source/funkin/ui/debug/dialogue/ConversationDebugState.hx +++ b/source/funkin/ui/debug/dialogue/ConversationDebugState.hx @@ -36,11 +36,24 @@ class ConversationDebugState extends MusicBeatState public override function create():Void { - conversation = ConversationRegistry.instance.fetchEntry(conversationId); - conversation.completeCallback = onConversationComplete; - add(conversation); + super.create(); + startConversation(); + } - ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false)); + function startConversation():Void + { + if (conversation != null) return; + + conversation = ConversationRegistry.instance.fetchEntry(conversationId); + if (conversation == null) return; + if (!conversation.alive) conversation.revive(); + + conversation.zIndex = 1000; + add(conversation); + refresh(); + + var event:ScriptEvent = new ScriptEvent(CREATE, false); + ScriptEventDispatcher.callEvent(conversation, event); } function onConversationComplete():Void @@ -61,15 +74,17 @@ class ConversationDebugState extends MusicBeatState if (conversation != null) { - if (controls.CUTSCENE_ADVANCE) conversation.advanceConversation(); - - if (controls.CUTSCENE_SKIP) + if (controls.CUTSCENE_ADVANCE) { - conversation.trySkipConversation(elapsed); + conversation.advanceConversation(); } - else + else if (controls.PAUSE) { - conversation.trySkipConversation(-1); + conversation.kill(); + remove(conversation); + conversation = null; + + FlxG.switchState(() -> new ConversationDebugState()); } } } diff --git a/source/funkin/ui/options/ControlsMenu.hx b/source/funkin/ui/options/ControlsMenu.hx index bda071846..62ae4b1a9 100644 --- a/source/funkin/ui/options/ControlsMenu.hx +++ b/source/funkin/ui/options/ControlsMenu.hx @@ -27,7 +27,7 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page static var controlGroups:Array> = [ [NOTE_UP, NOTE_DOWN, NOTE_LEFT, NOTE_RIGHT], [UI_UP, UI_DOWN, UI_LEFT, UI_RIGHT, ACCEPT, BACK], - [CUTSCENE_ADVANCE, CUTSCENE_SKIP], + [CUTSCENE_ADVANCE], [VOLUME_UP, VOLUME_DOWN, VOLUME_MUTE], [DEBUG_MENU, DEBUG_CHART] ]; diff --git a/source/funkin/util/plugins/MemoryGCPlugin.hx b/source/funkin/util/plugins/MemoryGCPlugin.hx new file mode 100644 index 000000000..3df861eb5 --- /dev/null +++ b/source/funkin/util/plugins/MemoryGCPlugin.hx @@ -0,0 +1,37 @@ +package funkin.util.plugins; + +import flixel.FlxBasic; + +/** + * A plugin which adds functionality to press `Ins` to immediately perform memory garbage collection. + */ +class MemoryGCPlugin extends FlxBasic +{ + public function new() + { + super(); + } + + public static function initialize():Void + { + FlxG.plugins.addPlugin(new MemoryGCPlugin()); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (FlxG.keys.justPressed.INSERT) + { + var perfStart:Float = Sys.time(); + funkin.util.MemoryUtil.collect(true); + var perfEnd:Float = Sys.time(); + trace("Memory GC took " + (perfEnd - perfStart) + " seconds"); + } + } + + public override function destroy():Void + { + super.destroy(); + } +} diff --git a/source/funkin/util/plugins/WatchPlugin.hx b/source/funkin/util/plugins/WatchPlugin.hx index defd9797d..a03115820 100644 --- a/source/funkin/util/plugins/WatchPlugin.hx +++ b/source/funkin/util/plugins/WatchPlugin.hx @@ -35,7 +35,10 @@ class WatchPlugin extends FlxBasic FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition); FlxG.watch.addQuick("songPositionNoOffset", Conductor.instance.songPosition + Conductor.instance.instrumentalOffset); + + FlxG.watch.addQuick("musicLength", FlxG.sound?.music?.length ?? 0.0); FlxG.watch.addQuick("musicTime", FlxG.sound?.music?.time ?? 0.0); + FlxG.watch.addQuick("bpm", Conductor.instance.bpm); FlxG.watch.addQuick("currentMeasureTime", Conductor.instance.currentMeasureTime); FlxG.watch.addQuick("currentBeatTime", Conductor.instance.currentBeatTime);