diff --git a/assets b/assets index 5479e17b1..49970e24e 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 5479e17b1085f72e05f1c1f9e0a668ef832d3341 +Subproject commit 49970e24e919de25f4dcef5bd47116f1877ee360 diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 52b9c19d6..0a430f196 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -915,6 +915,28 @@ class SongNoteDataRaw implements ICloneable return SongNoteData.buildDirectionName(this.data, strumlineSize); } + /** + * The strumline index of the note, if applicable. + * Strips the direction from the data. + * + * 0 = player, 1 = opponent, etc. + */ + public function getStrumlineIndex(strumlineSize:Int = 4):Int + { + return Math.floor(this.data / strumlineSize); + } + + /** + * Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side). + * TODO: The name of this function is a little misleading; what about mines? + * @param strumlineSize Defaults to 4. + * @return True if it's Boyfriend's note. + */ + public function getMustHitNote(strumlineSize:Int = 4):Bool + { + return getStrumlineIndex(strumlineSize) == 0; + } + @:jignored var _stepTime:Null = null; @@ -1003,28 +1025,6 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw } } - /** - * The strumline index of the note, if applicable. - * Strips the direction from the data. - * - * 0 = player, 1 = opponent, etc. - */ - public inline function getStrumlineIndex(strumlineSize:Int = 4):Int - { - return Math.floor(this.data / strumlineSize); - } - - /** - * Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side). - * TODO: The name of this function is a little misleading; what about mines? - * @param strumlineSize Defaults to 4. - * @return True if it's Boyfriend's note. - */ - public inline function getMustHitNote(strumlineSize:Int = 4):Bool - { - return getStrumlineIndex(strumlineSize) == 0; - } - @:jignored public var isHoldNote(get, never):Bool; diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index f293919f3..d38e3ac87 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -234,11 +234,11 @@ class PauseSubState extends MusicBeatSubState if (PlayStatePlaylist.isStoryMode) { PlayStatePlaylist.reset(); - openSubState(new funkin.ui.transition.StickerSubState(null, () -> new funkin.ui.story.StoryMenuState())); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState())); } else { - openSubState(new funkin.ui.transition.StickerSubState(null, () -> new funkin.ui.freeplay.FreeplayState())); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker))); } case 'Exit to Chart Editor': diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index c72ac1ed9..ad2ea5a45 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -83,10 +83,10 @@ typedef PlayStateParams = */ ?targetDifficulty:String, /** - * The character to play as. - * @default `bf`, or the first character in the song's character list. + * The variation to play on. + * @default `Constants.DEFAULT_VARIATION` . */ - ?targetCharacter:String, + ?targetVariation:String, /** * The instrumental to play with. * Significant if the `targetSong` supports alternate instrumentals. @@ -153,9 +153,9 @@ class PlayState extends MusicBeatSubState public var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY; /** - * The player character being used for this level, as a character ID. + * The currently selected variation. */ - public var currentPlayerId:String = 'bf'; + public var currentVariation:String = Constants.DEFAULT_VARIATION; /** * The currently active Stage. This is the object containing all the props. @@ -454,7 +454,7 @@ class PlayState extends MusicBeatSubState function get_currentChart():SongDifficulty { if (currentSong == null || currentDifficulty == null) return null; - return currentSong.getDifficulty(currentDifficulty, currentPlayerId); + return currentSong.getDifficulty(currentDifficulty, currentVariation); } /** @@ -512,7 +512,7 @@ class PlayState extends MusicBeatSubState // Apply parameters. currentSong = params.targetSong; if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty; - if (params.targetCharacter != null) currentPlayerId = params.targetCharacter; + if (params.targetVariation != null) currentVariation = params.targetVariation; isPracticeMode = params.practiceMode ?? false; isMinimalMode = params.minimalMode ?? false; startTimestamp = params.startTimestamp ?? 0.0; @@ -692,7 +692,7 @@ class PlayState extends MusicBeatSubState } else if (currentChart == null) { - message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty playing as "$currentPlayerId". Click OK to return to the main menu.'; + 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.'; } // Display a popup. This blocks the application until the user clicks OK. @@ -838,7 +838,7 @@ class PlayState extends MusicBeatSubState { targetSong: currentSong, targetDifficulty: currentDifficulty, - targetCharacter: currentPlayerId, + targetVariation: currentVariation, })); } else @@ -1395,7 +1395,7 @@ class PlayState extends MusicBeatSubState trace('Song difficulty could not be loaded.'); } - var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the character we are playing as by manipulating currentPlayerId. + var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the variation we are playing on by manipulating targetVariation. // // GIRLFRIEND @@ -2600,7 +2600,7 @@ class PlayState extends MusicBeatSubState { targetSong: targetSong, targetDifficulty: PlayStatePlaylist.campaignDifficulty, - targetCharacter: currentPlayerId, + targetVariation: currentVariation, }); nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y); return nextPlayState; @@ -2618,7 +2618,7 @@ class PlayState extends MusicBeatSubState { targetSong: targetSong, targetDifficulty: PlayStatePlaylist.campaignDifficulty, - targetCharacter: currentPlayerId, + targetVariation: currentVariation, }); nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y); return nextPlayState; diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx index 0d90df5a0..420a4fdc4 100644 --- a/source/funkin/play/components/HealthIcon.hx +++ b/source/funkin/play/components/HealthIcon.hx @@ -148,11 +148,12 @@ class HealthIcon extends FlxSprite { if (characterId == 'bf-old') { - characterId = PlayState.instance.currentPlayerId; + PlayState.instance.currentStage.getBoyfriend().initHealthIcon(false); } else { characterId = 'bf-old'; + loadCharacter(characterId); } } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 52a1ba6f8..5100e1888 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -184,9 +184,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = difficulties.get(diffId); + var variationSuffix = (variation != Constants.DEFAULT_VARIATION) ? '-$variation' : ''; + var difficulty:Null = difficulties.get('$diffId$variationSuffix'); if (difficulty == null) { trace('Fabricated new difficulty for $diffId.'); difficulty = new SongDifficulty(this, diffId, variation); var metadata = _metadata.get(variation); - var variationSuffix = (variation != null && variation != '' && variation != Constants.DEFAULT_VARIATION) ? '-$variation' : ''; difficulties.set('$diffId$variationSuffix', difficulty); if (metadata != null) @@ -258,26 +256,52 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + public function getDifficulty(?diffId:String, ?variation:String, ?variations:Array):Null { - if (diffId == null) diffId = listDifficulties()[0]; - + if (diffId == null) diffId = listDifficulties(variation)[0]; if (variation == null) variation = Constants.DEFAULT_VARIATION; - var variationSuffix = (variation != null && variation != '' && variation != Constants.DEFAULT_VARIATION) ? '-$variation' : ''; + if (variations == null) variations = [variation]; - return difficulties.get('$diffId$variationSuffix'); + for (currentVariation in variations) + { + var variationSuffix = (currentVariation != Constants.DEFAULT_VARIATION) ? '-$currentVariation' : ''; + + if (difficulties.exists('$diffId$variationSuffix')) + { + return difficulties.get('$diffId$variationSuffix'); + } + } + + return null; + } + + public function getFirstValidVariation(?diffId:String, ?possibleVariations:Array):Null + { + if (variations == null) possibleVariations = variations; + if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0]; + + for (variation in variations) + { + if (difficulties.exists('$diffId-$variation')) return variation; + } + + return null; } /** * List all the difficulties in this song. - * @param variationId Optionally filter by variation. + * @param variationId Optionally filter by a single variation. + * @param variationIds Optionally filter by multiple variations. * @return The list of difficulties. */ - public function listDifficulties(?variationId:String):Array + public function listDifficulties(?variationId:String, ?variationIds:Array):Array { - if (variationId == '') variationId = null; + if (variationIds == null) variationIds = []; + if (variationId != null) variationIds.push(variationId); // The difficulties array contains entries like 'normal', 'nightmare-erect', and 'normal-pico', // so we have to map it to the actual difficulty names. @@ -286,7 +310,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = difficulties.keys().array().map(function(diffId:String):Null { var difficulty:Null = difficulties.get(diffId); if (difficulty == null) return null; - if (variationId != null && difficulty.variation != variationId) return null; + if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null; return difficulty.difficulty; }).nonNull().unique(); @@ -504,7 +528,8 @@ class SongDifficulty var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; // Automatically resolve voices by removing suffixes. - // For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`. + // For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`. + // Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`. var playerId:String = characters.player; var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix'); @@ -516,6 +541,19 @@ class SongDifficulty // Try again. voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix'); } + if (voicePlayer == null) + { + // Try again without $suffix. + playerId = characters.player; + voicePlayer = Paths.voices(this.song.id, '-${playerId}'); + while (voicePlayer != null && !Assets.exists(voicePlayer)) + { + // Remove the last suffix. + playerId = playerId.split('-').slice(0, -1).join('-'); + // Try again. + voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix'); + } + } var opponentId:String = characters.opponent; var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix'); @@ -526,6 +564,19 @@ class SongDifficulty // Try again. voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix'); } + if (voiceOpponent == null) + { + // Try again without $suffix. + opponentId = characters.opponent; + voiceOpponent = Paths.voices(this.song.id, '-${opponentId}'); + while (voiceOpponent != null && !Assets.exists(voiceOpponent)) + { + // Remove the last suffix. + opponentId = opponentId.split('-').slice(0, -1).join('-'); + // Try again. + voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix'); + } + } var result:Array = []; if (voicePlayer != null) result.push(voicePlayer); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 686edb135..af7eed129 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -5037,7 +5037,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState stopWelcomeMusic(); // TODO: PR Flixel to make onComplete nullable. if (audioInstTrack != null) audioInstTrack.onComplete = null; - FlxG.switchState(() -> new MainMenuState()); + FlxG.switchState(() -> new MainMenuState()); resetWindowTitle(); @@ -5303,8 +5303,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { targetSong: targetSong, targetDifficulty: selectedDifficulty, - // TODO: Add this. - // targetCharacter: targetCharacter, + targetVariation: selectedVariation, practiceMode: playtestPracticeMode, minimalMode: minimal, startTimestamp: startTimestamp, diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index 9c86269e8..2d8e6a71e 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -70,28 +70,28 @@ class ChartEditorImportExportHandler { state.loadInstFromAsset(Paths.inst(songId, '-$variation'), variation); } - } - for (difficultyId in song.listDifficulties()) - { - var diff:Null = song.getDifficulty(difficultyId); - if (diff == null) continue; + for (difficultyId in song.listDifficulties(variation)) + { + var diff:Null = song.getDifficulty(difficultyId, variation); + if (diff == null) continue; - var instId:String = diff.variation == Constants.DEFAULT_VARIATION ? '' : diff.variation; - var voiceList:Array = diff.buildVoiceList(); // SongDifficulty accounts for variation already. + var instId:String = diff.variation == Constants.DEFAULT_VARIATION ? '' : diff.variation; + var voiceList:Array = diff.buildVoiceList(); // SongDifficulty accounts for variation already. - if (voiceList.length == 2) - { - state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId); - state.loadVocalsFromAsset(voiceList[1], diff.characters.opponent, instId); - } - else if (voiceList.length == 1) - { - state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId); - } - else - { - trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}'); + if (voiceList.length == 2) + { + state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId); + state.loadVocalsFromAsset(voiceList[1], diff.characters.opponent, instId); + } + else if (voiceList.length == 1) + { + state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId); + } + else + { + trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}'); + } } } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index f981357cf..e2f01fb13 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -59,11 +59,14 @@ import lime.utils.Assets; */ typedef FreeplayStateParams = { - ?character:String; + ?character:String, }; class FreeplayState extends MusicBeatSubState { + // Params, you can't change these without transitioning to a new FreeplayState. + final currentCharacter:String; + var songs:Array> = []; var diffIdsCurrent:Array = []; @@ -72,9 +75,6 @@ class FreeplayState extends MusicBeatSubState var curSelected:Int = 0; var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY; - // Params - var currentCharacter:String; - var fp:FreeplayScore; var txtCompletion:AtlasText; var lerpCompletion:Float = 0; @@ -102,6 +102,8 @@ class FreeplayState extends MusicBeatSubState var ostName:FlxText; var difficultyStars:DifficultyStars; + var displayedVariations:Array; + var dj:DJBoyfriend; var letterSort:LetterSort; @@ -113,7 +115,7 @@ class FreeplayState extends MusicBeatSubState static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; static var rememberedSongId:Null = null; - public function new(?params:FreeplayParams, ?stickers:StickerSubState) + public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER; @@ -159,6 +161,10 @@ class FreeplayState extends MusicBeatSubState // Add a null entry that represents the RANDOM option songs.push(null); + // TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later. + // Default character (BF) shows default and Erect variations. Pico shows only Pico variations. + displayedVariations = (currentCharacter == "bf") ? [Constants.DEFAULT_VARIATION, "erect"] : [currentCharacter]; + // programmatically adds the songs via LevelRegistry and SongRegistry for (levelId in LevelRegistry.instance.listBaseGameLevelIds()) { @@ -166,9 +172,12 @@ class FreeplayState extends MusicBeatSubState { var song:Song = SongRegistry.instance.fetchEntry(songId); - songs.push(new FreeplaySongData(levelId, songId, song)); + // Only display songs which actually have available charts for the current character. + var availableDifficultiesForSong = song.listDifficulties(displayedVariations); + if (availableDifficultiesForSong.length == 0) continue; - for (difficulty in song.listDifficulties()) + songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); + for (difficulty in availableDifficultiesForSong) { diffIdsTotal.pushUnique(difficulty); } @@ -287,6 +296,8 @@ class FreeplayState extends MusicBeatSubState x: -dj.width * 1.6, speed: 0.5 }); + // TODO: Replace this. + if (currentCharacter == "pico") dj.visible = false; add(dj); var bgDad:FlxSprite = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); @@ -859,8 +870,11 @@ class FreeplayState extends MusicBeatSubState // TODO: DEBUG REMOVE THIS if (FlxG.keys.justPressed.P) { - currentCharacter = (currentCharacter == "bf") ? "pico" : "bf"; - changeSelection(0); + var newParams:FreeplayStateParams = + { + character: currentCharacter == "bf" ? "pico" : "bf", + }; + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(newParams, sticker))); } if (controls.BACK && !typing.hasFocus) @@ -914,7 +928,7 @@ class FreeplayState extends MusicBeatSubState } else { - FlxG.switchState(new MainMenuState()); + FlxG.switchState(() -> new MainMenuState()); } }); } @@ -1080,13 +1094,7 @@ class FreeplayState extends MusicBeatSubState return; } var targetDifficulty:String = currentDifficulty; - - // TODO: Implement Pico into the interface properly. - var targetCharacter:String = 'bf'; - if (FlxG.keys.pressed.P) - { - targetCharacter = 'pico'; - } + var targetVariation:String = targetSong.getFirstValidVariation(targetDifficulty); PlayStatePlaylist.campaignId = cap.songData.levelId; @@ -1100,11 +1108,11 @@ class FreeplayState extends MusicBeatSubState new FlxTimer().start(1, function(tmr:FlxTimer) { Paths.setCurrentLevel(cap.songData.levelId); - LoadingState.loadAndSwitchState(new PlayState( + LoadingState.loadAndSwitchState(() -> new PlayState( { targetSong: targetSong, targetDifficulty: targetDifficulty, - targetCharacter: targetCharacter, + targetVariation: targetVariation, }), true); }); } @@ -1269,31 +1277,33 @@ class FreeplaySongData public var songRating(default, null):Int = 0; public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; + public var displayedVariations(default, null):Array = [Constants.DEFAULT_VARIATION]; function set_currentDifficulty(value:String):String { if (currentDifficulty == value) return value; currentDifficulty = value; - updateValues(); + updateValues(displayedVariations); return value; } - public function new(levelId:String, songId:String, song:Song) + public function new(levelId:String, songId:String, song:Song, ?displayedVariations:Array) { this.levelId = levelId; this.songId = songId; this.song = song; + if (displayedVariations != null) this.displayedVariations = displayedVariations; - updateValues(); + updateValues(displayedVariations); } - function updateValues():Void + function updateValues(displayedVariations:Array):Void { - this.songDifficulties = song.listDifficulties(); + this.songDifficulties = song.listDifficulties(displayedVariations); if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY; - var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty); + var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, displayedVariations); if (songDifficulty == null) return; this.songName = songDifficulty.songName; this.songCharacter = songDifficulty.characters.opponent; diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index 1b9252fde..f0c39c13f 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -150,7 +150,8 @@ class Level implements IRegistryEntry if (firstSong != null) { - for (difficulty in firstSong.listDifficulties()) + // Don't display alternate characters in Story Mode. + for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, "erect"])) { difficulties.push(difficulty); } diff --git a/source/funkin/ui/transition/StickerSubState.hx b/source/funkin/ui/transition/StickerSubState.hx index 43ced1d7c..fa36cfd50 100644 --- a/source/funkin/ui/transition/StickerSubState.hx +++ b/source/funkin/ui/transition/StickerSubState.hx @@ -36,7 +36,7 @@ class StickerSubState extends MusicBeatSubState * This is a FUNCTION so we can pass it directly to `FlxG.switchState()`, * and we can add constructor parameters in the caller. */ - var targetState:Void->FlxState; + var targetState:StickerSubState->FlxState; // what "folders" to potentially load from (as of writing only "keys" exist) var soundSelections:Array = []; @@ -44,14 +44,11 @@ class StickerSubState extends MusicBeatSubState var soundSelection:String = ""; var sounds:Array = []; - public function new(?oldStickers:Array, ?targetState:Void->FlxState):Void + public function new(?oldStickers:Array, ?targetState:StickerSubState->FlxState):Void { super(); - if (targetState != null) - { - this.targetState = () -> new MainMenuState(); - } + this.targetState = (targetState == null) ? ((sticker) -> new MainMenuState()) : targetState; // todo still // make sure that ONLY plays mp3/ogg files @@ -248,7 +245,7 @@ class StickerSubState extends MusicBeatSubState dipshit.addChild(bitmap); FlxG.addChildBelowMouse(dipshit); - FlxG.switchState(targetState); + FlxG.switchState(() -> targetState(this)); } }); });