diff --git a/assets b/assets index 69ebdb6a7..51b02f0d4 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 69ebdb6a7aa57b6762ce509243679ab959615120 +Subproject commit 51b02f0d47e5b34bf8589065c092953c10c5040d diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index e7ad326d2..46c09090d 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -111,6 +111,11 @@ typedef PlayStateParams = * @default `false` */ ?practiceMode:Bool, + /** + * Whether the song should start in Bot Play Mode. + * @default `false` + */ + ?botPlayMode:Bool, /** * Whether the song should be in minimal mode. * @default `false` @@ -282,6 +287,12 @@ class PlayState extends MusicBeatSubState */ public var isPracticeMode:Bool = false; + /** + * Whether the game is currently in Bot Play Mode. + * If true, player will not lose gain or lose score from notes. + */ + public var isBotPlayMode:Bool = false; + /** * Whether the player has dropped below zero health, * and we are just waiting for an animation to play out before transitioning. @@ -566,6 +577,7 @@ class PlayState extends MusicBeatSubState if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty; if (params.targetVariation != null) currentVariation = params.targetVariation; isPracticeMode = params.practiceMode ?? false; + isBotPlayMode = params.botPlayMode ?? false; isMinimalMode = params.minimalMode ?? false; startTimestamp = params.startTimestamp ?? 0.0; playbackRate = params.playbackRate ?? 1.0; @@ -1614,7 +1626,7 @@ class PlayState extends MusicBeatSubState var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); - playerStrumline = new Strumline(noteStyle, true); + playerStrumline = new Strumline(noteStyle, !isBotPlayMode); opponentStrumline = new Strumline(noteStyle, false); add(playerStrumline); add(opponentStrumline); @@ -1876,7 +1888,14 @@ class PlayState extends MusicBeatSubState function updateScoreText():Void { // TODO: Add functionality for modules to update the score text. - scoreText.text = 'Score:' + songScore; + if (isBotPlayMode) + { + scoreText.text = 'Bot Play Enabled'; + } + else + { + scoreText.text = 'Score:' + songScore; + } } /** @@ -1884,7 +1903,14 @@ class PlayState extends MusicBeatSubState */ function updateHealthBar():Void { - healthLerp = FlxMath.lerp(healthLerp, health, 0.15); + if (isBotPlayMode) + { + healthLerp = Constants.HEALTH_MAX; + } + else + { + healthLerp = FlxMath.lerp(healthLerp, health, 0.15); + } } /** @@ -1928,13 +1954,16 @@ class PlayState extends MusicBeatSubState if (Conductor.instance.songPosition > hitWindowEnd) { - if (note.hasMissed) continue; + if (note.hasMissed || note.hasBeenHit) continue; note.tooEarly = false; note.mayHit = false; note.hasMissed = true; - if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; + if (note.holdNoteSprite != null) + { + note.holdNoteSprite.missedNote = true; + } } else if (Conductor.instance.songPosition > hitWindowCenter) { @@ -2021,10 +2050,38 @@ class PlayState extends MusicBeatSubState if (Conductor.instance.songPosition > hitWindowEnd) { + if (note.hasMissed || note.hasBeenHit) continue; note.tooEarly = false; note.mayHit = false; note.hasMissed = true; - if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; + if (note.holdNoteSprite != null) + { + note.holdNoteSprite.missedNote = true; + } + } + else if (isBotPlayMode && Conductor.instance.songPosition > hitWindowCenter) + { + if (note.hasBeenHit) continue; + + // We call onHitNote to play the proper animations, + // but not goodNoteHit! This means zero score and zero notes hit for the results screen! + + // Call an event to allow canceling the note hit. + // NOTE: This is what handles the character animations! + var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0); + dispatchEvent(event); + + // Calling event.cancelEvent() skips all the other logic! Neat! + if (event.eventCanceled) continue; + + // Command the bot to hit the note on time. + // NOTE: This is what handles the strumline and cleaning up the note itself! + playerStrumline.hitNote(note); + + if (note.holdNoteSprite != null) + { + playerStrumline.playNoteHoldCover(note.holdNoteSprite); + } } else if (Conductor.instance.songPosition > hitWindowStart) { @@ -2069,7 +2126,7 @@ class PlayState extends MusicBeatSubState if (holdNote == null || !holdNote.alive) continue; // While the hold note is being hit, and there is length on the hold note... - if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0) + if (!isBotPlayMode && holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0) { // Grant the player health. health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed; diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 190aa3ee0..efe1c707a 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -38,6 +38,10 @@ class Strumline extends FlxSpriteGroup return FlxG.height / 0.45; } + /** + * Whether this strumline is controlled by the player's inputs. + * False means it's controlled by the opponent or Bot Play. + */ public var isPlayer:Bool; /** diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 78e73bf27..85a2396b9 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -592,6 +592,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var playtestPracticeMode:Bool = false; + /** + * If true, playtesting a chart will make the computer do it for you! + */ + var playtestBotPlayMode:Bool = false; + /** * Enables or disables the "debugger" popup that appears when you run into a flixel error. */ @@ -5359,6 +5364,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState targetDifficulty: selectedDifficulty, targetVariation: selectedVariation, practiceMode: playtestPracticeMode, + botPlayMode: playtestBotPlayMode, minimalMode: minimal, startTimestamp: startTimestamp, playbackRate: playbackRate, diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index f32cc2bfb..3b32edf5d 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -299,6 +299,15 @@ class ChartEditorToolboxHandler state.playtestStartTime = checkboxStartTime.selected; }; + var checkboxBotPlay:Null = toolbox.findComponent('playtestBotPlayCheckbox', CheckBox); + if (checkboxBotPlay == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestBotPlayCheckbox component.'; + + checkboxBotPlay.selected = state.playtestBotPlayMode; + + checkboxBotPlay.onClick = _ -> { + state.playtestBotPlayMode = checkboxBotPlay.selected; + }; + var checkboxDebugger:Null = toolbox.findComponent('playtestDebuggerCheckbox', CheckBox); if (checkboxDebugger == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestDebuggerCheckbox component.'; diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 50f85571b..45f9a4d27 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1143,12 +1143,12 @@ class FreeplayState extends MusicBeatSubState targetSong: targetSong, targetDifficulty: targetDifficulty, targetVariation: targetVariation, - // TODO: Make this an option! - // startTimestamp: 0.0, - // TODO: Make this an option! - // playbackRate: 0.5, practiceMode: false, minimalMode: false, + // TODO: Make these an option! It's currently only accessible via chart editor. + // startTimestamp: 0.0, + // playbackRate: 0.5, + // botPlayMode: true, }, true); }); }