From 9fa603363c9ecf50d4576f18f3d2692372e12ca5 Mon Sep 17 00:00:00 2001 From: AppleHair <95587502+AppleHair@users.noreply.github.com> Date: Mon, 10 Jun 2024 19:42:27 +0300 Subject: [PATCH 01/20] [BUGFIX] Made freeplay use the metadata to get the instrumental suffix Song previews in freeplay will now use the instrumental suffix from the current difficulty's corresponding song variation metadata instead of using the variation id as an instrumental suffix and checking only the "erect" variation. --- source/funkin/ui/freeplay/FreeplayState.hx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 5e07fb396..5a7fa54d2 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1890,14 +1890,17 @@ class FreeplayState extends MusicBeatSubState } else { - var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : ""; + var previewSong:Null = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId); + var instSuffix:String = previewSong?.getDifficulty(currentDifficulty, + previewSong?.variations ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? ''; + instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; FunkinSound.playMusic(daSongCapsule.songData.songId, { startingVolume: 0.0, overrideExisting: true, restartTrack: false, pathsFunction: INST, - suffix: potentiallyErect, + suffix: instSuffix, partialParams: { loadPartial: true, From c4d6e18885bc02fb723e1f73a87ad2ca67a6ede3 Mon Sep 17 00:00:00 2001 From: AppleHair <95587502+AppleHair@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:23:45 +0300 Subject: [PATCH 02/20] now using getVariationsByCharId instead --- source/funkin/ui/freeplay/FreeplayState.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 5a7fa54d2..1caad3ee5 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1892,7 +1892,7 @@ class FreeplayState extends MusicBeatSubState { var previewSong:Null = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId); var instSuffix:String = previewSong?.getDifficulty(currentDifficulty, - previewSong?.variations ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? ''; + previewSong?.getVariationsByCharId(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? ''; instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; FunkinSound.playMusic(daSongCapsule.songData.songId, { From 1f1fe62a06827f2487fb99d45b5a551dc07ed454 Mon Sep 17 00:00:00 2001 From: AppleHair <95587502+AppleHair@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:53:33 +0300 Subject: [PATCH 03/20] [BUGFIX] Fixed Ranks not appearing in freeplay for custom variations Freeplay tries to access a song's rank using the current difficulty name alone, but custom variation ranks are being saved with a variation prefix. This PR makes freeplay look for the variation prefix when necessary. --- source/funkin/ui/freeplay/FreeplayState.hx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 0caaf4591..23c0f6afb 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -2094,8 +2094,13 @@ class FreeplaySongData { this.albumId = songDifficulty.album; } + + // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. + // `easy`, `erect`, `normal-pico`, etc. + var suffixedDifficulty = (songDifficulty.variation != Constants.DEFAULT_VARIATION + && songDifficulty.variation != 'erect') ? '$currentDifficulty-${songDifficulty.variation}' : currentDifficulty; - this.scoringRank = Save.instance.getSongRank(songId, currentDifficulty); + this.scoringRank = Save.instance.getSongRank(songId, suffixedDifficulty); this.isNew = song.isSongNew(currentDifficulty); } From 60e741434c6ecd1f15c4e62f4ea364b8648c3538 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 18 Jun 2024 17:56:24 -0400 Subject: [PATCH 04/20] Implemented playable character registry, added Freeplay character filtering, added alt instrumental support --- assets | 2 +- source/funkin/InitState.hx | 2 + .../funkin/data/freeplay/player/CHANGELOG.md | 9 ++ .../funkin/data/freeplay/player/PlayerData.hx | 63 ++++++++ .../data/freeplay/player/PlayerRegistry.hx | 151 ++++++++++++++++++ source/funkin/modding/PolymodHandler.hx | 6 +- source/funkin/play/song/Song.hx | 34 ++-- .../handlers/ChartEditorDialogHandler.hx | 7 +- source/funkin/ui/freeplay/FreeplayState.hx | 80 +++++++--- .../freeplay/charselect/PlayableCharacter.hx | 108 +++++++++++++ .../charselect/ScriptedPlayableCharacter.hx | 8 + source/funkin/util/VersionUtil.hx | 1 - 12 files changed, 433 insertions(+), 38 deletions(-) create mode 100644 source/funkin/data/freeplay/player/CHANGELOG.md create mode 100644 source/funkin/data/freeplay/player/PlayerData.hx create mode 100644 source/funkin/data/freeplay/player/PlayerRegistry.hx create mode 100644 source/funkin/ui/freeplay/charselect/PlayableCharacter.hx create mode 100644 source/funkin/ui/freeplay/charselect/ScriptedPlayableCharacter.hx diff --git a/assets b/assets index 2e1594ee4..fece99b3b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 2e1594ee4c04c7148628bae471bdd061c9deb6b7 +Subproject commit fece99b3b121045fb2f6f02dba485201b32f1c87 diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 49b15ddf6..c2a56bdc2 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.data.freeplay.player.PlayerRegistry; import funkin.ui.debug.charting.ChartEditorState; import funkin.ui.transition.LoadingState; import flixel.FlxState; @@ -164,6 +165,7 @@ class InitState extends FlxState SongRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries(); + PlayerRegistry.instance.loadEntries(); ConversationRegistry.instance.loadEntries(); DialogueBoxRegistry.instance.loadEntries(); SpeakerRegistry.instance.loadEntries(); diff --git a/source/funkin/data/freeplay/player/CHANGELOG.md b/source/funkin/data/freeplay/player/CHANGELOG.md new file mode 100644 index 000000000..7a31e11ca --- /dev/null +++ b/source/funkin/data/freeplay/player/CHANGELOG.md @@ -0,0 +1,9 @@ +# Freeplay Playable Character Data Schema Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] +Initial release. diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx new file mode 100644 index 000000000..d7b814584 --- /dev/null +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -0,0 +1,63 @@ +package funkin.data.freeplay.player; + +import funkin.data.animation.AnimationData; + +@:nullSafety +class PlayerData +{ + /** + * The sematic version number of the player data JSON format. + * Supports fancy comparisons like NPM does it's neat. + */ + @:default(funkin.data.freeplay.player.PlayerRegistry.PLAYER_DATA_VERSION) + public var version:String; + + /** + * A readable name for this playable character. + */ + public var name:String = 'Unknown'; + + /** + * The character IDs this character is associated with. + * Only songs that use these characters will show up in Freeplay. + */ + @:default([]) + public var ownedChars:Array = []; + + /** + * Whether to show songs with character IDs that aren't associated with any specific character. + */ + @:optional + @:default(false) + public var showUnownedChars:Bool = false; + + /** + * Whether this character is unlocked by default. + * Use a ScriptedPlayableCharacter to add custom logic. + */ + @:optional + @:default(true) + public var unlocked:Bool = true; + + public function new() + { + this.version = PlayerRegistry.PLAYER_DATA_VERSION; + } + + /** + * Convert this StageData into a JSON string. + */ + public function serialize(pretty:Bool = true):String + { + // Update generatedBy and version before writing. + updateVersionToLatest(); + + var writer = new json2object.JsonWriter(); + return writer.write(this, pretty ? ' ' : null); + } + + public function updateVersionToLatest():Void + { + this.version = PlayerRegistry.PLAYER_DATA_VERSION; + } +} diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx new file mode 100644 index 000000000..3de9efd41 --- /dev/null +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -0,0 +1,151 @@ +package funkin.data.freeplay.player; + +import funkin.data.freeplay.player.PlayerData; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter; + +class PlayerRegistry extends BaseRegistry +{ + /** + * The current version string for the stage data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migratePlayerData()` function. + */ + public static final PLAYER_DATA_VERSION:thx.semver.Version = "1.0.0"; + + public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; + + public static var instance(get, never):PlayerRegistry; + static var _instance:Null = null; + + static function get_instance():PlayerRegistry + { + if (_instance == null) _instance = new PlayerRegistry(); + return _instance; + } + + /** + * A mapping between stage character IDs and Freeplay playable character IDs. + */ + var ownedCharacterIds:Map = []; + + public function new() + { + super('PLAYER', 'players', PLAYER_DATA_VERSION_RULE); + } + + public override function loadEntries():Void + { + super.loadEntries(); + + for (playerId in listEntryIds()) + { + var player = fetchEntry(playerId); + if (player == null) continue; + + var currentPlayerCharIds = player.getOwnedCharacterIds(); + for (characterId in currentPlayerCharIds) + { + ownedCharacterIds.set(characterId, playerId); + } + } + + log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.'); + } + + /** + * Get the playable character associated with a given stage character. + * @param characterId The stage character ID. + * @return The playable character. + */ + public function getCharacterOwnerId(characterId:String):String + { + return ownedCharacterIds[characterId]; + } + + /** + * Return true if the given stage character is associated with a specific playable character. + * If so, the level should only appear if that character is selected in Freeplay. + * @param characterId The stage character ID. + * @return Whether the character is owned by any one character. + */ + public function isCharacterOwned(characterId:String):Bool + { + return ownedCharacterIds.exists(characterId); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + */ + public function parseEntryData(id:String):Null + { + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + + switch (loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):PlayableCharacter + { + return ScriptedPlayableCharacter.init(clsName, "unknown"); + } + + function getScriptedClassNames():Array + { + return ScriptedPlayableCharacter.listScriptClasses(); + } + + /** + * A list of all the playable characters from the base game, in order. + */ + public function listBaseGamePlayerIds():Array + { + return ["bf", "pico"]; + } + + /** + * A list of all installed playable characters that are not from the base game. + */ + public function listModdedPlayerIds():Array + { + return listEntryIds().filter(function(id:String):Bool { + return listBaseGamePlayerIds().indexOf(id) == -1; + }); + } +} diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index ae754b780..c352aa606 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -8,6 +8,7 @@ import funkin.data.event.SongEventRegistry; import funkin.data.story.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.song.SongRegistry; +import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.stage.StageRegistry; import funkin.data.freeplay.album.AlbumRegistry; import funkin.modding.module.ModuleHandler; @@ -369,15 +370,18 @@ class PolymodHandler // These MUST be imported at the top of the file and not referred to by fully qualified name, // to ensure build macros work properly. + SongEventRegistry.loadEventCache(); + SongRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries(); - SongEventRegistry.loadEventCache(); + PlayerRegistry.instance.loadEntries(); ConversationRegistry.instance.loadEntries(); DialogueBoxRegistry.instance.loadEntries(); SpeakerRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries(); + CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry. ModuleHandler.loadModuleCache(); } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index dde5ee7b8..91d35d8fa 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -14,6 +14,7 @@ import funkin.data.song.SongData.SongTimeFormat; import funkin.data.song.SongRegistry; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import funkin.modding.events.ScriptEvent; +import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.util.SortUtil; import openfl.utils.Assets; @@ -401,11 +402,11 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry):Null + public function getFirstValidVariation(?diffId:String, ?currentCharacter:PlayableCharacter, ?possibleVariations:Array):Null { if (possibleVariations == null) { - possibleVariations = variations; + possibleVariations = getVariationsByCharacter(currentCharacter); possibleVariations.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_VARIATION_LIST)); } if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0]; @@ -422,22 +423,29 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + public function getVariationsByCharacter(?char:PlayableCharacter):Array { - if (charId == null) charId = Constants.DEFAULT_CHARACTER; + if (char == null) return variations; - if (variations.contains(charId)) + var result = []; + trace('Evaluating variations for ${this.id} ${char.id}: ${this.variations}'); + for (variation in variations) { - return [charId]; - } - else - { - // TODO: How to exclude character variations while keeping other custom variations? - return variations; + var metadata = _metadata.get(variation); + + var playerCharId = metadata?.playData?.characters?.player; + if (playerCharId == null) continue; + + if (char.shouldShowCharacter(playerCharId)) + { + result.push(variation); + } } + + return result; } /** @@ -455,6 +463,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = song.listDifficulties(displayedVariations, false); - trace(availableDifficultiesForSong); + trace('Available Difficulties: $availableDifficultiesForSong'); if (availableDifficultiesForSong.length == 0) continue; songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); @@ -454,7 +458,7 @@ class FreeplayState extends MusicBeatSubState }); // TODO: Replace this. - if (currentCharacter == 'pico') dj.visible = false; + if (currentCharacterId == 'pico') dj.visible = false; add(dj); @@ -1195,6 +1199,16 @@ class FreeplayState extends MusicBeatSubState rankAnimStart(fromResultsParams); } + if (FlxG.keys.justPressed.P) + { + FlxG.switchState(FreeplayState.build( + { + { + character: currentCharacterId == "pico" ? "bf" : "pico", + } + })); + } + // if (FlxG.keys.justPressed.H) // { // rankDisplayNew(fromResultsParams); @@ -1302,9 +1316,9 @@ class FreeplayState extends MusicBeatSubState { if (busy) return; - var upP:Bool = controls.UI_UP_P && !FlxG.keys.pressed.CONTROL; - var downP:Bool = controls.UI_DOWN_P && !FlxG.keys.pressed.CONTROL; - var accepted:Bool = controls.ACCEPT && !FlxG.keys.pressed.CONTROL; + var upP:Bool = controls.UI_UP_P; + var downP:Bool = controls.UI_DOWN_P; + var accepted:Bool = controls.ACCEPT; if (FlxG.onMobile) { @@ -1378,7 +1392,7 @@ class FreeplayState extends MusicBeatSubState } #end - if (!FlxG.keys.pressed.CONTROL && (controls.UI_UP || controls.UI_DOWN)) + if ((controls.UI_UP || controls.UI_DOWN)) { if (spamming) { @@ -1440,13 +1454,13 @@ class FreeplayState extends MusicBeatSubState } #end - if (controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) + if (controls.UI_LEFT_P) { dj.resetAFKTimer(); changeDiff(-1); generateSongList(currentFilter, true); } - if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) + if (controls.UI_RIGHT_P) { dj.resetAFKTimer(); changeDiff(1); @@ -1720,7 +1734,7 @@ class FreeplayState extends MusicBeatSubState return; } var targetDifficultyId:String = currentDifficulty; - var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId); + var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter); PlayStatePlaylist.campaignId = cap.songData.levelId; var targetDifficulty:SongDifficulty = targetSong.getDifficulty(targetDifficultyId, targetVariation); @@ -1730,8 +1744,18 @@ class FreeplayState extends MusicBeatSubState return; } - // TODO: Change this with alternate instrumentals - var targetInstId:String = targetDifficulty.characters.instrumental; + var baseInstrumentalId:String = targetDifficulty?.characters?.instrumental ?? ''; + var altInstrumentalIds:Array = targetDifficulty?.characters?.altInstrumentals ?? []; + + var targetInstId:String = baseInstrumentalId; + + // TODO: Make this a UI element. + #if (debug || FORCE_DEBUG_VERSION) + if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL) + { + targetInstId = altInstrumentalIds[0]; + } + #end // Visual and audio effects. FunkinSound.playOnce(Paths.sound('confirmMenu')); @@ -1883,9 +1907,23 @@ class FreeplayState extends MusicBeatSubState else { var previewSong:Null = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId); - var instSuffix:String = previewSong?.getDifficulty(currentDifficulty, - previewSong?.getVariationsByCharId(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? ''; + var songDifficulty = previewSong?.getDifficulty(currentDifficulty, + previewSong?.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST); + var baseInstrumentalId:String = songDifficulty?.characters?.instrumental ?? ''; + var altInstrumentalIds:Array = songDifficulty?.characters?.altInstrumentals ?? []; + + var instSuffix:String = baseInstrumentalId; + + // TODO: Make this a UI element. + #if (debug || FORCE_DEBUG_VERSION) + if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL) + { + instSuffix = altInstrumentalIds[0]; + } + #end + instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; + FunkinSound.playMusic(daSongCapsule.songData.songId, { startingVolume: 0.0, @@ -1913,7 +1951,7 @@ class FreeplayState extends MusicBeatSubState public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState { var result:MainMenuState; - if (params?.fromResults.playRankAnim) result = new MainMenuState(true); + if (params?.fromResults?.playRankAnim) result = new MainMenuState(true); else result = new MainMenuState(false); @@ -1951,8 +1989,8 @@ class DifficultySelector extends FlxSprite override function update(elapsed:Float):Void { - if (flipX && controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) moveShitDown(); - if (!flipX && controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) moveShitDown(); + if (flipX && controls.UI_RIGHT_P) moveShitDown(); + if (!flipX && controls.UI_LEFT_P) moveShitDown(); super.update(elapsed); } diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx new file mode 100644 index 000000000..743345004 --- /dev/null +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -0,0 +1,108 @@ +package funkin.ui.freeplay.charselect; + +import funkin.data.IRegistryEntry; +import funkin.data.freeplay.player.PlayerData; +import funkin.data.freeplay.player.PlayerRegistry; + +/** + * An object used to retrieve data about a playable character (also known as "weeks"). + * Can be scripted to override each function, for custom behavior. + */ +class PlayableCharacter implements IRegistryEntry +{ + /** + * The ID of the playable character. + */ + public final id:String; + + /** + * Playable character data as parsed from the JSON file. + */ + public final _data:PlayerData; + + /** + * @param id The ID of the JSON file to parse. + */ + public function new(id:String) + { + this.id = id; + _data = _fetchData(id); + + if (_data == null) + { + throw 'Could not parse playable character data for id: $id'; + } + } + + /** + * Retrieve the readable name of the playable character. + */ + public function getName():String + { + // TODO: Maybe add localization support? + return _data.name; + } + + /** + * Retrieve the list of stage character IDs associated with this playable character. + * @return The list of associated character IDs + */ + public function getOwnedCharacterIds():Array + { + return _data.ownedChars; + } + + /** + * Return `true` if, when this character is selected in Freeplay, + * songs unassociated with a specific character should appear. + */ + public function shouldShowUnownedChars():Bool + { + return _data.showUnownedChars; + } + + public function shouldShowCharacter(id:String):Bool + { + if (_data.ownedChars.contains(id)) + { + return true; + } + + if (_data.showUnownedChars) + { + var result = !PlayerRegistry.instance.isCharacterOwned(id); + return result; + } + + return false; + } + + /** + * Returns whether this character is unlocked. + */ + public function isUnlocked():Bool + { + return _data.unlocked; + } + + /** + * Called when the character is destroyed. + * TODO: Document when this gets called + */ + public function destroy():Void {} + + public function toString():String + { + return 'PlayableCharacter($id)'; + } + + /** + * Retrieve and parse the JSON data for a playable character by ID. + * @param id The ID of the character + * @return The parsed player data, or null if not found or invalid + */ + static function _fetchData(id:String):Null + { + return PlayerRegistry.instance.parseEntryDataWithMigration(id, PlayerRegistry.instance.fetchEntryVersion(id)); + } +} diff --git a/source/funkin/ui/freeplay/charselect/ScriptedPlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/ScriptedPlayableCharacter.hx new file mode 100644 index 000000000..f75a58092 --- /dev/null +++ b/source/funkin/ui/freeplay/charselect/ScriptedPlayableCharacter.hx @@ -0,0 +1,8 @@ +package funkin.ui.freeplay.charselect; + +/** + * A script that can be tied to a PlayableCharacter. + * Create a scripted class that extends PlayableCharacter to use this. + */ +@:hscriptClass +class ScriptedPlayableCharacter extends funkin.ui.freeplay.charselect.PlayableCharacter implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx index 832ce008a..9bf46a188 100644 --- a/source/funkin/util/VersionUtil.hx +++ b/source/funkin/util/VersionUtil.hx @@ -24,7 +24,6 @@ class VersionUtil try { var versionRaw:thx.semver.Version.SemVer = version; - trace('${versionRaw} satisfies (${versionRule})? ${version.satisfies(versionRule)}'); return version.satisfies(versionRule); } catch (e) From 263039f52ce5096abc5929c9af3b95ef9440e229 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 18 Jun 2024 20:07:27 -0400 Subject: [PATCH 05/20] Make Boyfriend DJ animations data driven --- assets | 2 +- .../funkin/data/freeplay/player/PlayerData.hx | 93 +++++ source/funkin/ui/freeplay/DJBoyfriend.hx | 371 ------------------ source/funkin/ui/freeplay/FreeplayDJ.hx | 369 +++++++++++++++++ source/funkin/ui/freeplay/FreeplayState.hx | 24 +- .../freeplay/charselect/PlayableCharacter.hx | 5 + source/funkin/util/tools/MapTools.hx | 4 + 7 files changed, 484 insertions(+), 384 deletions(-) delete mode 100644 source/funkin/ui/freeplay/DJBoyfriend.hx create mode 100644 source/funkin/ui/freeplay/FreeplayDJ.hx diff --git a/assets b/assets index fece99b3b..6ec72f8ae 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit fece99b3b121045fb2f6f02dba485201b32f1c87 +Subproject commit 6ec72f8aeb5ab77997dee4e2e98ae03f0ec347b8 diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index d7b814584..10fc54b78 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -31,6 +31,12 @@ class PlayerData @:default(false) public var showUnownedChars:Bool = false; + /** + * Data for displaying this character in the Freeplay menu. + * If null, display no DJ. + */ + public var freeplayDJ:Null = null; + /** * Whether this character is unlocked by default. * Use a ScriptedPlayableCharacter to add custom logic. @@ -61,3 +67,90 @@ class PlayerData this.version = PlayerRegistry.PLAYER_DATA_VERSION; } } + +class PlayerFreeplayDJData +{ + var assetPath:String; + var animations:Array; + + @:jignored + var animationMap:Map; + + @:optional + var cartoon:Null; + + public function new() + { + animationMap = new Map(); + } + + function mapAnimations() + { + if (animationMap == null) animationMap = new Map(); + + animationMap.clear(); + for (anim in animations) + { + animationMap.set(anim.name, anim); + } + } + + public function getAtlasPath():String + { + return Paths.animateAtlas(assetPath); + } + + public function getAnimationPrefix(name:String):Null + { + if (animationMap.size() == 0) mapAnimations(); + + var anim = animationMap.get(name); + if (anim == null) return null; + return anim.prefix; + } + + public function getAnimationOffsets(name:String):Null> + { + if (animationMap.size() == 0) mapAnimations(); + + var anim = animationMap.get(name); + if (anim == null) return null; + return anim.offsets; + } + + // TODO: These should really be frame labels, ehe. + + public function getCartoonSoundClickFrame():Int + { + return cartoon?.soundClickFrame ?? 80; + } + + public function getCartoonSoundCartoonFrame():Int + { + return cartoon?.soundCartoonFrame ?? 85; + } + + public function getCartoonLoopBlinkFrame():Int + { + return cartoon?.loopBlinkFrame ?? 112; + } + + public function getCartoonLoopFrame():Int + { + return cartoon?.loopFrame ?? 166; + } + + public function getCartoonChannelChangeFrame():Int + { + return cartoon?.channelChangeFrame ?? 60; + } +} + +typedef PlayerFreeplayDJCartoonData = +{ + var soundClickFrame:Int; + var soundCartoonFrame:Int; + var loopBlinkFrame:Int; + var loopFrame:Int; + var channelChangeFrame:Int; +} diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx deleted file mode 100644 index bbf043dd4..000000000 --- a/source/funkin/ui/freeplay/DJBoyfriend.hx +++ /dev/null @@ -1,371 +0,0 @@ -package funkin.ui.freeplay; - -import flixel.FlxSprite; -import flixel.util.FlxSignal; -import funkin.util.assets.FlxAnimationUtil; -import funkin.graphics.adobeanimate.FlxAtlasSprite; -import funkin.audio.FunkinSound; -import flixel.util.FlxTimer; -import funkin.audio.FunkinSound; -import funkin.audio.FlxStreamSound; - -class DJBoyfriend extends FlxAtlasSprite -{ - // Represents the sprite's current status. - // Without state machines I would have driven myself crazy years ago. - public var currentState:DJBoyfriendState = Intro; - - // A callback activated when the intro animation finishes. - public var onIntroDone:FlxSignal = new FlxSignal(); - - // A callback activated when Boyfriend gets spooked. - public var onSpook:FlxSignal = new FlxSignal(); - - // playAnim stolen from Character.hx, cuz im lazy lol! - // TODO: Switch this class to use SwagSprite instead. - public var animOffsets:Map>; - - var gotSpooked:Bool = false; - - static final SPOOK_PERIOD:Float = 60.0; - static final TV_PERIOD:Float = 120.0; - - // Time since dad last SPOOKED you. - var timeSinceSpook:Float = 0; - - public function new(x:Float, y:Float) - { - super(x, y, Paths.animateAtlas("freeplay/freeplay-boyfriend", "preload")); - - animOffsets = new Map>(); - - anim.callback = function(name, number) { - switch (name) - { - case "Boyfriend DJ watchin tv OG": - if (number == 80) - { - FunkinSound.playOnce(Paths.sound('remote_click')); - } - if (number == 85) - { - runTvLogic(); - } - default: - } - }; - - setupAnimations(); - - FlxG.debugger.track(this); - FlxG.console.registerObject("dj", this); - - anim.onComplete = onFinishAnim; - - FlxG.console.registerFunction("tv", function() { - currentState = TV; - }); - } - - /* - [remote hand under,boyfriend top head,brim piece,arm cringe l,red lazer,dj arm in,bf fist pump arm,hand raised right,forearm left,fist shaking,bf smile eyes closed face,arm cringe r,bf clenched face,face shrug,boyfriend falling,blue tint 1,shirt sleeve,bf clenched fist,head BF relaxed,blue tint 2,hand down left,blue tint 3,blue tint 4,head less smooshed,blue tint 5,boyfriend freeplay,BF head slight turn,blue tint 6,arm shrug l,blue tint 7,shoulder raised w sleeve,blue tint 8,fist pump face,blue tint 9,foot rested light,hand turnaround,arm chill right,Boyfriend DJ,arm shrug r,head back bf,hat top piece,dad bod,face surprise snap,Boyfriend DJ fist pump,office chair,foot rested right,chest down,office chair upright,body chill,bf dj afk,head mouth open dad,BF Head defalt HAIR BLOWING,hand shrug l,face piece,foot wag,turn table,shoulder up left,turntable lights,boyfriend dj body shirt blowing,body chunk turned,hand down right,dj arm out,hand shrug r,body chest out,rave hand,palm,chill face default,head back semi bf,boyfriend bottom head,DJ arm,shoulder right dad,bf surprise,boyfriend dj body,hs1,Boyfriend DJ watchin tv OG,spinning disk,hs2,arm chill left,boyfriend dj intro,hs3,hs4,chill face extra,hs5,remote hand upright,hs6,pant over table,face surprise,bf arm peace,arm turnaround,bf eyes 1,arm slammed table,eye squit,leg BF,head mid piece,arm backing,arm swoopin in,shoe right lowering,forearm right,hand out,blue tint 10,body falling back,remote thumb press,shoulder,hair spike single,bf bent - arm,crt,foot raised right,dad hand,chill face 1,chill face 2,clenched fist,head SMOOSHED,shoulder left dad,df1,body chunk upright,df2,df3,df4,hat front piece,df5,foot rested right 2,hand in,arm spun,shoe raised left,bf 1 finger hand,bf mouth 1,Boyfriend DJ confirm,forearm down ,hand raised left,remote thumb up] - */ - override public function listAnimations():Array - { - var anims:Array = []; - @:privateAccess - for (animKey in anim.symbolDictionary) - { - anims.push(animKey.name); - } - return anims; - } - - var lowPumpLoopPoint:Int = 4; - - public override function update(elapsed:Float):Void - { - super.update(elapsed); - - switch (currentState) - { - case Intro: - // Play the intro animation then leave this state immediately. - if (getCurrentAnimation() != 'boyfriend dj intro') playFlashAnimation('boyfriend dj intro', true); - timeSinceSpook = 0; - case Idle: - // We are in this state the majority of the time. - if (getCurrentAnimation() != 'Boyfriend DJ') - { - playFlashAnimation('Boyfriend DJ', true); - } - - if (getCurrentAnimation() == 'Boyfriend DJ' && this.isLoopFinished()) - { - if (timeSinceSpook >= SPOOK_PERIOD && !gotSpooked) - { - currentState = Spook; - } - else if (timeSinceSpook >= TV_PERIOD) - { - currentState = TV; - } - } - timeSinceSpook += elapsed; - case Confirm: - if (getCurrentAnimation() != 'Boyfriend DJ confirm') playFlashAnimation('Boyfriend DJ confirm', false); - timeSinceSpook = 0; - case PumpIntro: - if (getCurrentAnimation() != 'Boyfriend DJ fist pump') playFlashAnimation('Boyfriend DJ fist pump', false); - if (getCurrentAnimation() == 'Boyfriend DJ fist pump' && anim.curFrame >= 4) - { - anim.play("Boyfriend DJ fist pump", true, false, 0); - } - case FistPump: - - case Spook: - if (getCurrentAnimation() != 'bf dj afk') - { - onSpook.dispatch(); - playFlashAnimation('bf dj afk', false); - gotSpooked = true; - } - timeSinceSpook = 0; - case TV: - if (getCurrentAnimation() != 'Boyfriend DJ watchin tv OG') playFlashAnimation('Boyfriend DJ watchin tv OG', true); - timeSinceSpook = 0; - default: - // I shit myself. - } - - if (FlxG.keys.pressed.CONTROL) - { - if (FlxG.keys.justPressed.LEFT) - { - this.offsetX -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); - } - - if (FlxG.keys.justPressed.RIGHT) - { - this.offsetX += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); - } - - if (FlxG.keys.justPressed.UP) - { - this.offsetY -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); - } - - if (FlxG.keys.justPressed.DOWN) - { - this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); - } - - if (FlxG.keys.justPressed.SPACE) - { - currentState = (currentState == Idle ? TV : Idle); - } - } - } - - function onFinishAnim():Void - { - var name = anim.curSymbol.name; - switch (name) - { - case "boyfriend dj intro": - // trace('Finished intro'); - currentState = Idle; - onIntroDone.dispatch(); - case "Boyfriend DJ": - // trace('Finished idle'); - case "bf dj afk": - // trace('Finished spook'); - currentState = Idle; - case "Boyfriend DJ confirm": - - case "Boyfriend DJ fist pump": - currentState = Idle; - - case "Boyfriend DJ loss reaction 1": - currentState = Idle; - - case "Boyfriend DJ watchin tv OG": - var frame:Int = FlxG.random.bool(33) ? 112 : 166; - - // BF switches channels when the video ends, or at a 10% chance each time his idle loops. - if (FlxG.random.bool(5)) - { - frame = 60; - // boyfriend switches channel code? - // runTvLogic(); - } - trace('Replay idle: ${frame}'); - anim.play("Boyfriend DJ watchin tv OG", true, false, frame); - // trace('Finished confirm'); - } - } - - public function resetAFKTimer():Void - { - timeSinceSpook = 0; - gotSpooked = false; - } - - var offsetX:Float = 0.0; - var offsetY:Float = 0.0; - - function setupAnimations():Void - { - // Intro - addOffset('boyfriend dj intro', 8.0 - 1.3, 3.0 - 0.4); - - // Idle - addOffset('Boyfriend DJ', 0, 0); - - // Confirm - addOffset('Boyfriend DJ confirm', 0, 0); - - // AFK: Spook - addOffset('bf dj afk', 649.5, 58.5); - - // AFK: TV - addOffset('Boyfriend DJ watchin tv OG', 0, 0); - } - - var cartoonSnd:Null = null; - - public var playingCartoon:Bool = false; - - public function runTvLogic() - { - if (cartoonSnd == null) - { - // tv is OFF, but getting turned on - FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { - loadCartoon(); - }); - } - else - { - // plays it smidge after the click - FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() { - cartoonSnd.destroy(); - loadCartoon(); - }); - } - - // loadCartoon(); - } - - function loadCartoon() - { - cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() { - anim.play("Boyfriend DJ watchin tv OG", true, false, 60); - }); - - // Fade out music to 40% volume over 1 second. - // This helps make the TV a bit more audible. - FlxG.sound.music.fadeOut(1.0, 0.1); - - // Play the cartoon at a random time between the start and 5 seconds from the end. - cartoonSnd.time = FlxG.random.float(0, Math.max(cartoonSnd.length - (5 * Constants.MS_PER_SEC), 0.0)); - } - - final cartoonList:Array = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/")); - - function getRandomFlashToon():String - { - var randomFile = FlxG.random.getObject(cartoonList); - - // Strip folder prefix - randomFile = randomFile.replace("assets/sounds/", ""); - // Strip file extension - randomFile = randomFile.substring(0, randomFile.length - 4); - - return randomFile; - } - - public function confirm():Void - { - currentState = Confirm; - } - - public function fistPump():Void - { - currentState = PumpIntro; - } - - public function pumpFist():Void - { - currentState = FistPump; - anim.play("Boyfriend DJ fist pump", true, false, 4); - } - - public function pumpFistBad():Void - { - currentState = FistPump; - anim.play("Boyfriend DJ loss reaction 1", true, false, 4); - } - - public inline function addOffset(name:String, x:Float = 0, y:Float = 0) - { - animOffsets[name] = [x, y]; - } - - override public function getCurrentAnimation():String - { - if (this.anim == null || this.anim.curSymbol == null) return ""; - return this.anim.curSymbol.name; - } - - public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void - { - anim.play(id, Force, Reverse, Frame); - applyAnimOffset(); - } - - function applyAnimOffset() - { - var AnimName = getCurrentAnimation(); - var daOffset = animOffsets.get(AnimName); - if (animOffsets.exists(AnimName)) - { - var xValue = daOffset[0]; - var yValue = daOffset[1]; - if (AnimName == "Boyfriend DJ watchin tv OG") - { - xValue += offsetX; - yValue += offsetY; - } - - offset.set(xValue, yValue); - } - else - { - offset.set(0, 0); - } - } - - public override function destroy():Void - { - super.destroy(); - - if (cartoonSnd != null) - { - cartoonSnd.destroy(); - cartoonSnd = null; - } - } -} - -enum DJBoyfriendState -{ - Intro; - Idle; - Confirm; - PumpIntro; - FistPump; - Spook; - TV; -} diff --git a/source/funkin/ui/freeplay/FreeplayDJ.hx b/source/funkin/ui/freeplay/FreeplayDJ.hx new file mode 100644 index 000000000..f9effe793 --- /dev/null +++ b/source/funkin/ui/freeplay/FreeplayDJ.hx @@ -0,0 +1,369 @@ +package funkin.ui.freeplay; + +import flixel.FlxSprite; +import flixel.util.FlxSignal; +import funkin.util.assets.FlxAnimationUtil; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.audio.FunkinSound; +import flixel.util.FlxTimer; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.data.freeplay.player.PlayerData.PlayerFreeplayDJData; +import funkin.audio.FunkinSound; +import funkin.audio.FlxStreamSound; + +class FreeplayDJ extends FlxAtlasSprite +{ + // Represents the sprite's current status. + // Without state machines I would have driven myself crazy years ago. + public var currentState:DJBoyfriendState = Intro; + + // A callback activated when the intro animation finishes. + public var onIntroDone:FlxSignal = new FlxSignal(); + + // A callback activated when the idle easter egg plays. + public var onIdleEasterEgg:FlxSignal = new FlxSignal(); + + var seenIdleEasterEgg:Bool = false; + + static final IDLE_EGG_PERIOD:Float = 60.0; + static final IDLE_CARTOON_PERIOD:Float = 120.0; + + // Time since last special idle animation you. + var timeIdling:Float = 0; + + final characterId:String = Constants.DEFAULT_CHARACTER; + final playableCharData:PlayerFreeplayDJData; + + public function new(x:Float, y:Float, characterId:String) + { + this.characterId = characterId; + + var playableChar = PlayerRegistry.instance.fetchEntry(characterId); + playableCharData = playableChar.getFreeplayDJData(); + + super(x, y, playableCharData.getAtlasPath()); + + anim.callback = function(name, number) { + if (name == playableCharData.getAnimationPrefix('cartoon')) + { + if (number == playableCharData.getCartoonSoundClickFrame()) + { + FunkinSound.playOnce(Paths.sound('remote_click')); + } + if (number == playableCharData.getCartoonSoundCartoonFrame()) + { + runTvLogic(); + } + } + }; + + FlxG.debugger.track(this); + FlxG.console.registerObject("dj", this); + + anim.onComplete = onFinishAnim; + + FlxG.console.registerFunction("freeplayCartoon", function() { + currentState = Cartoon; + }); + } + + override public function listAnimations():Array + { + var anims:Array = []; + @:privateAccess + for (animKey in anim.symbolDictionary) + { + anims.push(animKey.name); + } + return anims; + } + + var lowPumpLoopPoint:Int = 4; + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + switch (currentState) + { + case Intro: + // Play the intro animation then leave this state immediately. + var animPrefix = playableCharData.getAnimationPrefix('intro'); + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + case Idle: + // We are in this state the majority of the time. + var animPrefix = playableCharData.getAnimationPrefix('idle'); + if (getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true); + } + + if (getCurrentAnimation() == animPrefix && this.isLoopFinished()) + { + if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg) + { + currentState = IdleEasterEgg; + } + else if (timeIdling >= IDLE_CARTOON_PERIOD) + { + currentState = Cartoon; + } + } + timeIdling += elapsed; + case Confirm: + var animPrefix = playableCharData.getAnimationPrefix('confirm'); + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false); + timeIdling = 0; + case FistPumpIntro: + var animPrefix = playableCharData.getAnimationPrefix('fistPump'); + if (getCurrentAnimation() != animPrefix) playFlashAnimation('Boyfriend DJ fist pump', false); + if (getCurrentAnimation() == animPrefix && anim.curFrame >= 4) + { + anim.play("Boyfriend DJ fist pump", true, false, 0); + } + case FistPump: + + case IdleEasterEgg: + var animPrefix = playableCharData.getAnimationPrefix('idleEasterEgg'); + if (getCurrentAnimation() != animPrefix) + { + onIdleEasterEgg.dispatch(); + playFlashAnimation(animPrefix, false); + seenIdleEasterEgg = true; + } + timeIdling = 0; + case Cartoon: + var animPrefix = playableCharData.getAnimationPrefix('cartoon'); + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + default: + // I shit myself. + } + + if (FlxG.keys.pressed.CONTROL) + { + if (FlxG.keys.justPressed.LEFT) + { + this.offsetX -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); + } + + if (FlxG.keys.justPressed.RIGHT) + { + this.offsetX += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); + } + + if (FlxG.keys.justPressed.UP) + { + this.offsetY -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); + } + + if (FlxG.keys.justPressed.DOWN) + { + this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); + } + + if (FlxG.keys.justPressed.SPACE) + { + currentState = (currentState == Idle ? Cartoon : Idle); + } + } + } + + function onFinishAnim():Void + { + var name = anim.curSymbol.name; + + if (name == playableCharData.getAnimationPrefix('intro')) + { + currentState = Idle; + onIntroDone.dispatch(); + } + else if (name == playableCharData.getAnimationPrefix('idle')) + { + // trace('Finished idle'); + } + else if (name == playableCharData.getAnimationPrefix('confirm')) + { + // trace('Finished confirm'); + } + else if (name == playableCharData.getAnimationPrefix('fistPump')) + { + // trace('Finished fist pump'); + currentState = Idle; + } + else if (name == playableCharData.getAnimationPrefix('idleEasterEgg')) + { + // trace('Finished spook'); + currentState = Idle; + } + else if (name == playableCharData.getAnimationPrefix('loss')) + { + // trace('Finished loss reaction'); + currentState = Idle; + } + else if (name == playableCharData.getAnimationPrefix('cartoon')) + { + // trace('Finished cartoon'); + + var frame:Int = FlxG.random.bool(33) ? playableCharData.getCartoonLoopBlinkFrame() : playableCharData.getCartoonLoopFrame(); + + // Character switches channels when the video ends, or at a 10% chance each time his idle loops. + if (FlxG.random.bool(5)) + { + frame = playableCharData.getCartoonChannelChangeFrame(); + // boyfriend switches channel code? + // runTvLogic(); + } + trace('Replay idle: ${frame}'); + anim.play(playableCharData.getAnimationPrefix('cartoon'), true, false, frame); + // trace('Finished confirm'); + } + else + { + trace('Finished ${name}'); + } + } + + public function resetAFKTimer():Void + { + timeIdling = 0; + seenIdleEasterEgg = false; + } + + var offsetX:Float = 0.0; + var offsetY:Float = 0.0; + + var cartoonSnd:Null = null; + + public var playingCartoon:Bool = false; + + public function runTvLogic() + { + if (cartoonSnd == null) + { + // tv is OFF, but getting turned on + FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { + loadCartoon(); + }); + } + else + { + // plays it smidge after the click + FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() { + cartoonSnd.destroy(); + loadCartoon(); + }); + } + + // loadCartoon(); + } + + function loadCartoon() + { + cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() { + anim.play("Boyfriend DJ watchin tv OG", true, false, 60); + }); + + // Fade out music to 40% volume over 1 second. + // This helps make the TV a bit more audible. + FlxG.sound.music.fadeOut(1.0, 0.1); + + // Play the cartoon at a random time between the start and 5 seconds from the end. + cartoonSnd.time = FlxG.random.float(0, Math.max(cartoonSnd.length - (5 * Constants.MS_PER_SEC), 0.0)); + } + + final cartoonList:Array = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/")); + + function getRandomFlashToon():String + { + var randomFile = FlxG.random.getObject(cartoonList); + + // Strip folder prefix + randomFile = randomFile.replace("assets/sounds/", ""); + // Strip file extension + randomFile = randomFile.substring(0, randomFile.length - 4); + + return randomFile; + } + + public function confirm():Void + { + currentState = Confirm; + } + + public function fistPump():Void + { + currentState = FistPumpIntro; + } + + public function pumpFist():Void + { + currentState = FistPump; + anim.play("Boyfriend DJ fist pump", true, false, 4); + } + + public function pumpFistBad():Void + { + currentState = FistPump; + anim.play("Boyfriend DJ loss reaction 1", true, false, 4); + } + + override public function getCurrentAnimation():String + { + if (this.anim == null || this.anim.curSymbol == null) return ""; + return this.anim.curSymbol.name; + } + + public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void + { + anim.play(id, Force, Reverse, Frame); + applyAnimOffset(); + } + + function applyAnimOffset() + { + var AnimName = getCurrentAnimation(); + var daOffset = playableCharData.getAnimationOffsets(AnimName); + if (daOffset != null) + { + var xValue = daOffset[0]; + var yValue = daOffset[1]; + if (AnimName == "Boyfriend DJ watchin tv OG") + { + xValue += offsetX; + yValue += offsetY; + } + + trace('Successfully applied offset: ' + xValue + ', ' + yValue); + offset.set(xValue, yValue); + } + else + { + trace('No offset found, defaulting to: 0, 0'); + offset.set(0, 0); + } + } + + public override function destroy():Void + { + super.destroy(); + + if (cartoonSnd != null) + { + cartoonSnd.destroy(); + cartoonSnd = null; + } + } +} + +enum DJBoyfriendState +{ + Intro; + Idle; + Confirm; + FistPumpIntro; + FistPump; + IdleEasterEgg; + Cartoon; +} diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 30863f2a9..6b342fb40 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -167,7 +167,7 @@ class FreeplayState extends MusicBeatSubState var curCapsule:SongMenuItem; var curPlaying:Bool = false; - var dj:DJBoyfriend; + var dj:FreeplayDJ; var ostName:FlxText; var albumRoll:AlbumRoll; @@ -211,6 +211,7 @@ class FreeplayState extends MusicBeatSubState { currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER; currentCharacter = PlayerRegistry.instance.fetchEntry(currentCharacterId); + if (currentCharacter == null) throw 'Could not build Freeplay state for character: $currentCharacterId'; fromResultsParams = params?.fromResults; @@ -450,17 +451,16 @@ class FreeplayState extends MusicBeatSubState add(cardGlow); - dj = new DJBoyfriend(640, 366); - exitMovers.set([dj], - { - x: -dj.width * 1.6, - speed: 0.5 - }); - - // TODO: Replace this. - if (currentCharacterId == 'pico') dj.visible = false; - - add(dj); + if (currentCharacter?.getFreeplayDJData() != null) + { + dj = new FreeplayDJ(640, 366, currentCharacterId); + exitMovers.set([dj], + { + x: -dj.width * 1.6, + speed: 0.5 + }); + add(dj); + } bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); bgDad.shader = new AngleMask(); diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index 743345004..282e35d7a 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -77,6 +77,11 @@ class PlayableCharacter implements IRegistryEntry return false; } + public function getFreeplayDJData():PlayerFreeplayDJData + { + return _data.freeplayDJ; + } + /** * Returns whether this character is unlocked. */ diff --git a/source/funkin/util/tools/MapTools.hx b/source/funkin/util/tools/MapTools.hx index b98cb0adf..807f0aebd 100644 --- a/source/funkin/util/tools/MapTools.hx +++ b/source/funkin/util/tools/MapTools.hx @@ -14,6 +14,7 @@ class MapTools */ public static function size(map:Map):Int { + if (map == null) return 0; return map.keys().array().length; } @@ -22,6 +23,7 @@ class MapTools */ public static function values(map:Map):Array { + if (map == null) return []; return [for (i in map.iterator()) i]; } @@ -30,6 +32,7 @@ class MapTools */ public static function clone(map:Map):Map { + if (map == null) return null; return map.copy(); } @@ -76,6 +79,7 @@ class MapTools */ public static function keyValues(map:Map):Array { + if (map == null) return []; return map.keys().array(); } } From 9b3a748f3768f2d742b922aeedb7e62ee9766e40 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 20 Jun 2024 16:17:53 -0400 Subject: [PATCH 06/20] Working Pico DJ --- assets | 2 +- source/funkin/Paths.hx | 11 +- .../funkin/data/freeplay/player/PlayerData.hx | 42 ++- source/funkin/play/scoring/Scoring.hx | 4 +- source/funkin/ui/freeplay/FreeplayDJ.hx | 14 +- source/funkin/ui/freeplay/FreeplayState.hx | 278 ++++++++++-------- .../freeplay/charselect/PlayableCharacter.hx | 5 + source/funkin/ui/mainmenu/MainMenuState.hx | 5 +- source/funkin/util/SortUtil.hx | 2 +- 9 files changed, 216 insertions(+), 147 deletions(-) diff --git a/assets b/assets index 6ec72f8ae..8dd51cde0 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 6ec72f8aeb5ab77997dee4e2e98ae03f0ec347b8 +Subproject commit 8dd51cde0b9a3730abe9f97d0f50365c396ca784 diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index b0a97c4fa..285af7ca2 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -11,9 +11,16 @@ class Paths { static var currentLevel:Null = null; - public static function setCurrentLevel(name:String):Void + public static function setCurrentLevel(name:Null):Void { - currentLevel = name.toLowerCase(); + if (name == null) + { + currentLevel = null; + } + else + { + currentLevel = name.toLowerCase(); + } } public static function stripLibrary(path:String):String diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index 10fc54b78..c461c9555 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -35,6 +35,7 @@ class PlayerData * Data for displaying this character in the Freeplay menu. * If null, display no DJ. */ + @:optional public var freeplayDJ:Null = null; /** @@ -73,9 +74,25 @@ class PlayerFreeplayDJData var assetPath:String; var animations:Array; + @:optional + @:default("BOYFRIEND") + var text1:String; + + @:optional + @:default("HOT BLOODED IN MORE WAYS THAN ONE") + var text2:String; + + @:optional + @:default("PROTECT YO NUTS") + var text3:String; + + @:jignored var animationMap:Map; + @:jignored + var prefixToOffsetsMap:Map>; + @:optional var cartoon:Null; @@ -87,11 +104,14 @@ class PlayerFreeplayDJData function mapAnimations() { if (animationMap == null) animationMap = new Map(); + if (prefixToOffsetsMap == null) prefixToOffsetsMap = new Map(); animationMap.clear(); + prefixToOffsetsMap.clear(); for (anim in animations) { animationMap.set(anim.name, anim); + prefixToOffsetsMap.set(anim.prefix, anim.offsets); } } @@ -100,6 +120,15 @@ class PlayerFreeplayDJData return Paths.animateAtlas(assetPath); } + public function getFreeplayDJText(index:Int):String { + switch (index) { + case 1: return text1; + case 2: return text2; + case 3: return text3; + default: return ''; + } + } + public function getAnimationPrefix(name:String):Null { if (animationMap.size() == 0) mapAnimations(); @@ -109,13 +138,16 @@ class PlayerFreeplayDJData return anim.prefix; } - public function getAnimationOffsets(name:String):Null> + public function getAnimationOffsetsByPrefix(?prefix:String):Array { - if (animationMap.size() == 0) mapAnimations(); + if (prefixToOffsetsMap.size() == 0) mapAnimations(); + if (prefix == null) return [0, 0]; + return prefixToOffsetsMap.get(prefix); + } - var anim = animationMap.get(name); - if (anim == null) return null; - return anim.offsets; + public function getAnimationOffsets(name:String):Array + { + return getAnimationOffsetsByPrefix(getAnimationPrefix(name)); } // TODO: These should really be frame labels, ehe. diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index dc2c40647..02e5750bc 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -590,7 +590,7 @@ enum abstract ScoringRank(String) } } - public function getFreeplayRankIconAsset():Null + public function getFreeplayRankIconAsset():String { switch (abstract) { @@ -607,7 +607,7 @@ enum abstract ScoringRank(String) case SHIT: return 'LOSS'; default: - return null; + return 'LOSS'; } } diff --git a/source/funkin/ui/freeplay/FreeplayDJ.hx b/source/funkin/ui/freeplay/FreeplayDJ.hx index f9effe793..72eddd0ca 100644 --- a/source/funkin/ui/freeplay/FreeplayDJ.hx +++ b/source/funkin/ui/freeplay/FreeplayDJ.hx @@ -135,8 +135,12 @@ class FreeplayDJ extends FlxAtlasSprite timeIdling = 0; case Cartoon: var animPrefix = playableCharData.getAnimationPrefix('cartoon'); - if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); - timeIdling = 0; + if (animPrefix == null) { + currentState = IdleEasterEgg; + } else { + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + } default: // I shit myself. } @@ -324,7 +328,7 @@ class FreeplayDJ extends FlxAtlasSprite function applyAnimOffset() { var AnimName = getCurrentAnimation(); - var daOffset = playableCharData.getAnimationOffsets(AnimName); + var daOffset = playableCharData.getAnimationOffsetsByPrefix(AnimName); if (daOffset != null) { var xValue = daOffset[0]; @@ -335,12 +339,12 @@ class FreeplayDJ extends FlxAtlasSprite yValue += offsetY; } - trace('Successfully applied offset: ' + xValue + ', ' + yValue); + trace('Successfully applied offset ($AnimName): ' + xValue + ', ' + yValue); offset.set(xValue, yValue); } else { - trace('No offset found, defaulting to: 0, 0'); + trace('No offset found ($AnimName), defaulting to: 0, 0'); offset.set(0, 0); } } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 6b342fb40..06a090769 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1,54 +1,55 @@ package funkin.ui.freeplay; -import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.addons.transition.FlxTransitionableState; import flixel.addons.ui.FlxInputText; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.group.FlxGroup; -import funkin.graphics.shaders.GaussianBlurShader; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.input.touch.FlxTouch; import flixel.math.FlxAngle; import flixel.math.FlxPoint; -import openfl.display.BlendMode; import flixel.system.debug.watch.Tracker.TrackerProfile; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; +import flixel.tweens.misc.ShakeTween; import flixel.util.FlxColor; import flixel.util.FlxSpriteUtil; import flixel.util.FlxTimer; import funkin.audio.FunkinSound; -import funkin.data.story.level.LevelRegistry; -import funkin.data.song.SongRegistry; import funkin.data.freeplay.player.PlayerRegistry; +import funkin.data.song.SongRegistry; +import funkin.data.story.level.LevelRegistry; +import funkin.effects.IntervalShake; +import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinSprite; import funkin.graphics.shaders.AngleMask; +import funkin.graphics.shaders.GaussianBlurShader; import funkin.graphics.shaders.HSVShader; import funkin.graphics.shaders.PureColor; import funkin.graphics.shaders.StrokeShader; import funkin.input.Controls; import funkin.play.PlayStatePlaylist; +import funkin.play.scoring.Scoring; +import funkin.play.scoring.Scoring.ScoringRank; import funkin.play.song.Song; -import funkin.ui.story.Level; import funkin.save.Save; import funkin.save.Save.SaveScoreData; import funkin.ui.AtlasText; -import funkin.play.scoring.Scoring; -import funkin.play.scoring.Scoring.ScoringRank; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.freeplay.SongMenuItem.FreeplayRank; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatSubState; +import funkin.ui.story.Level; import funkin.ui.transition.LoadingState; import funkin.ui.transition.StickerSubState; import funkin.util.MathUtil; +import funkin.util.SortUtil; import lime.utils.Assets; -import flixel.tweens.misc.ShakeTween; -import funkin.effects.IntervalShake; -import funkin.ui.freeplay.SongMenuItem.FreeplayRank; -import funkin.ui.freeplay.charselect.PlayableCharacter; +import openfl.display.BlendMode; /** * Parameters used to initialize the FreeplayState. @@ -94,6 +95,7 @@ typedef FromResultsParams = /** * The state for the freeplay menu, allowing the player to select any song to play. */ +@:nullSafety class FreeplayState extends MusicBeatSubState { // @@ -164,10 +166,9 @@ class FreeplayState extends MusicBeatSubState var grpSongs:FlxTypedGroup; var grpCapsules:FlxTypedGroup; - var curCapsule:SongMenuItem; var curPlaying:Bool = false; - var dj:FreeplayDJ; + var dj:Null = null; var ostName:FlxText; var albumRoll:AlbumRoll; @@ -175,7 +176,7 @@ class FreeplayState extends MusicBeatSubState var letterSort:LetterSort; var exitMovers:ExitMoverData = new Map(); - var stickerSubState:StickerSubState; + var stickerSubState:Null = null; public static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; public static var rememberedSongId:Null = 'tutorial'; @@ -210,8 +211,12 @@ class FreeplayState extends MusicBeatSubState public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER; - currentCharacter = PlayerRegistry.instance.fetchEntry(currentCharacterId); - if (currentCharacter == null) throw 'Could not build Freeplay state for character: $currentCharacterId'; + var fetchPlayableCharacter = function():PlayableCharacter { + var result = PlayerRegistry.instance.fetchEntry(params?.character ?? Constants.DEFAULT_CHARACTER); + if (result == null) throw 'No valid playable character with id ${params?.character}'; + return result; + }; + currentCharacter = fetchPlayableCharacter(); fromResultsParams = params?.fromResults; @@ -220,12 +225,54 @@ class FreeplayState extends MusicBeatSubState prepForNewRank = true; } + super(FlxColor.TRANSPARENT); + if (stickers != null) { stickerSubState = stickers; } - super(FlxColor.TRANSPARENT); + // We build a bunch of sprites BEFORE create() so we can guarantee they aren't null later on. + albumRoll = new AlbumRoll(); + fp = new FreeplayScore(460, 60, 7, 100); + cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow')); + confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow')); + confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText')); + rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height); + funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); + funnyScroll = new BGScrollingText(0, 220, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60); + funnyScroll2 = new BGScrollingText(0, 335, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60); + grpCapsules = new FlxTypedGroup(); + grpDifficulties = new FlxTypedSpriteGroup(-300, 80); + letterSort = new LetterSort(400, 75); + grpSongs = new FlxTypedGroup(); + moreWays = new BGScrollingText(0, 160, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43); + moreWays2 = new BGScrollingText(0, 397, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43); + pinkBack = FunkinSprite.create('freeplay/pinkBack'); + rankBg = new FunkinSprite(0, 0); + rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette')); + sparks = new FlxSprite(0, 0); + sparksADD = new FlxSprite(0, 0); + txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR); + txtNuts = new BGScrollingText(0, 285, currentCharacter.getFreeplayDJText(3), FlxG.width / 2, true, 43); + + ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48); + + orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); + + bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); + alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); + confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2')); + funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, 60); + backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"), + { + FrameRate: 24.0, + Reversed: false, + // ?OnComplete:Void -> Void, + ShowPivot: false, + Antialiasing: true, + ScrollFactor: new FlxPoint(1, 1), + }); } override function create():Void @@ -236,12 +283,6 @@ class FreeplayState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; - // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere - funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); - funnyCam.bgColor = FlxColor.TRANSPARENT; - FlxG.cameras.add(funnyCam, false); - this.cameras = [funnyCam]; - if (stickerSubState != null) { this.persistentUpdate = true; @@ -277,7 +318,7 @@ class FreeplayState extends MusicBeatSubState // programmatically adds the songs via LevelRegistry and SongRegistry for (levelId in LevelRegistry.instance.listSortedLevelIds()) { - var level:Level = LevelRegistry.instance.fetchEntry(levelId); + var level:Null = LevelRegistry.instance.fetchEntry(levelId); if (level == null) { @@ -287,7 +328,7 @@ class FreeplayState extends MusicBeatSubState for (songId in level.getSongs()) { - var song:Song = SongRegistry.instance.fetchEntry(songId); + var song:Null = SongRegistry.instance.fetchEntry(songId); if (song == null) { @@ -319,17 +360,14 @@ class FreeplayState extends MusicBeatSubState trace(FlxG.camera.initialZoom); trace(FlxCamera.defaultZoom); - pinkBack = FunkinSprite.create('freeplay/pinkBack'); pinkBack.color = 0xFFFFD4E9; // sets it to pink! pinkBack.x -= pinkBack.width; FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); add(pinkBack); - orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); add(orangeBackShit); - alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); add(alsoOrangeLOL); exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL], @@ -344,15 +382,11 @@ class FreeplayState extends MusicBeatSubState orangeBackShit.visible = false; alsoOrangeLOL.visible = false; - confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText')); confirmTextGlow.blend = BlendMode.ADD; confirmTextGlow.visible = false; - confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow')); confirmGlow.blend = BlendMode.ADD; - confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2')); - confirmGlow.visible = false; confirmGlow2.visible = false; @@ -367,7 +401,6 @@ class FreeplayState extends MusicBeatSubState FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ['x', 'y', 'speed', 'size'])); - moreWays = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43); moreWays.funnyColor = 0xFFFFF383; moreWays.speed = 6.8; grpTxtScrolls.add(moreWays); @@ -378,7 +411,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.4, }); - funnyScroll = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60); funnyScroll.funnyColor = 0xFFFF9963; funnyScroll.speed = -3.8; grpTxtScrolls.add(funnyScroll); @@ -391,7 +423,6 @@ class FreeplayState extends MusicBeatSubState wait: 0 }); - txtNuts = new BGScrollingText(0, 285, 'PROTECT YO NUTS', FlxG.width / 2, true, 43); txtNuts.speed = 3.5; grpTxtScrolls.add(txtNuts); exitMovers.set([txtNuts], @@ -400,7 +431,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.4, }); - funnyScroll2 = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60); funnyScroll2.funnyColor = 0xFFFF9963; funnyScroll2.speed = -3.8; grpTxtScrolls.add(funnyScroll2); @@ -411,7 +441,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.5, }); - moreWays2 = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43); moreWays2.funnyColor = 0xFFFFF383; moreWays2.speed = 6.8; grpTxtScrolls.add(moreWays2); @@ -422,7 +451,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.4 }); - funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60); funnyScroll3.funnyColor = 0xFFFEA400; funnyScroll3.speed = -3.8; grpTxtScrolls.add(funnyScroll3); @@ -433,19 +461,8 @@ class FreeplayState extends MusicBeatSubState speed: 0.3 }); - backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"), - { - FrameRate: 24.0, - Reversed: false, - // ?OnComplete:Void -> Void, - ShowPivot: false, - Antialiasing: true, - ScrollFactor: new FlxPoint(1, 1), - }); - add(backingTextYeah); - cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow')); cardGlow.blend = BlendMode.ADD; cardGlow.visible = false; @@ -462,7 +479,6 @@ class FreeplayState extends MusicBeatSubState add(dj); } - bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); bgDad.shader = new AngleMask(); bgDad.visible = false; @@ -488,17 +504,13 @@ class FreeplayState extends MusicBeatSubState blackOverlayBullshitLOLXD.shader = bgDad.shader; - rankBg = new FunkinSprite(0, 0); rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xD3000000); add(rankBg); - grpSongs = new FlxTypedGroup(); add(grpSongs); - grpCapsules = new FlxTypedGroup(); add(grpCapsules); - grpDifficulties = new FlxTypedSpriteGroup(-300, 80); add(grpDifficulties); exitMovers.set([grpDifficulties], @@ -525,7 +537,6 @@ class FreeplayState extends MusicBeatSubState if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true; } - albumRoll = new AlbumRoll(); albumRoll.albumId = null; add(albumRoll); @@ -540,7 +551,6 @@ class FreeplayState extends MusicBeatSubState fnfFreeplay.font = 'VCR OSD Mono'; fnfFreeplay.visible = false; - ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48); ostName.font = 'VCR OSD Mono'; ostName.alignment = RIGHT; ostName.visible = false; @@ -572,7 +582,6 @@ class FreeplayState extends MusicBeatSubState tmr.time = FlxG.random.float(20, 60); }, 0); - fp = new FreeplayScore(460, 60, 7, 100); fp.visible = false; add(fp); @@ -580,11 +589,9 @@ class FreeplayState extends MusicBeatSubState clearBoxSprite.visible = false; add(clearBoxSprite); - txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR); txtCompletion.visible = false; add(txtCompletion); - letterSort = new LetterSort(400, 75); add(letterSort); letterSort.visible = false; @@ -632,7 +639,8 @@ class FreeplayState extends MusicBeatSubState // be careful not to "add()" things in here unless it's to a group that's already added to the state // otherwise it won't be properly attatched to funnyCamera (relavent code should be at the bottom of create()) - dj.onIntroDone.add(function() { + var onDJIntroDone = function() { + // when boyfriend hits dat shiii albumRoll.playIntro(); @@ -679,20 +687,24 @@ class FreeplayState extends MusicBeatSubState cardGlow.visible = true; FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut}); - if (prepForNewRank) + if (prepForNewRank && fromResultsParams != null) { rankAnimStart(fromResultsParams); } - }); + }; + + if (dj != null) { + dj.onIntroDone.add(onDJIntroDone); + } else { + onDJIntroDone(); + } generateSongList(null, false); // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere - funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); funnyCam.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(funnyCam, false); - rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette')); rankVignette.scale.set(2, 2); rankVignette.updateHitbox(); rankVignette.blend = BlendMode.ADD; @@ -704,7 +716,6 @@ class FreeplayState extends MusicBeatSubState bs.cameras = [funnyCam]; }); - rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height); rankCamera.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(rankCamera, false); rankBg.cameras = [rankCamera]; @@ -716,8 +727,8 @@ class FreeplayState extends MusicBeatSubState } } - var currentFilter:SongFilter = null; - var currentFilteredSongs:Array = []; + var currentFilter:Null = null; + var currentFilteredSongs:Array> = []; /** * Given the current filter, rebuild the current song list. @@ -728,7 +739,7 @@ class FreeplayState extends MusicBeatSubState */ public function generateSongList(filterStuff:Null, force:Bool = false, onlyIfChanged:Bool = true):Void { - var tempSongs:Array = songs; + var tempSongs:Array> = songs; // Remember just the difficulty because it's important for song sorting. if (rememberedDifficulty != null) @@ -790,11 +801,12 @@ class FreeplayState extends MusicBeatSubState for (i in 0...tempSongs.length) { - if (tempSongs[i] == null) continue; + var tempSong = tempSongs[i]; + if (tempSong == null) continue; var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem); - funnyMenu.init(FlxG.width, 0, tempSongs[i]); + funnyMenu.init(FlxG.width, 0, tempSong); funnyMenu.onConfirm = function() { capsuleOnConfirmDefault(funnyMenu); }; @@ -803,8 +815,8 @@ class FreeplayState extends MusicBeatSubState funnyMenu.ID = i; funnyMenu.capsule.alpha = 0.5; funnyMenu.songText.visible = false; - funnyMenu.favIcon.visible = tempSongs[i].isFav; - funnyMenu.favIconBlurred.visible = tempSongs[i].isFav; + funnyMenu.favIcon.visible = tempSong.isFav; + funnyMenu.favIconBlurred.visible = tempSong.isFav; funnyMenu.hsvShader = hsvShader; funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45); @@ -828,13 +840,10 @@ class FreeplayState extends MusicBeatSubState * @param songFilter The filter to apply * @return Array */ - public function sortSongs(songsToFilter:Array, songFilter:SongFilter):Array + public function sortSongs(songsToFilter:Array>, songFilter:SongFilter):Array> { - var filterAlphabetically = function(a:FreeplaySongData, b:FreeplaySongData):Int { - if (a?.songName.toLowerCase() < b?.songName.toLowerCase()) return -1; - else if (a?.songName.toLowerCase() > b?.songName.toLowerCase()) return 1; - else - return 0; + var filterAlphabetically = function(a:Null, b:Null):Int { + return SortUtil.alphabetically(a?.songName ?? '', b?.songName ?? ''); }; switch (songFilter.filterType) @@ -858,7 +867,7 @@ class FreeplayState extends MusicBeatSubState songsToFilter = songsToFilter.filter(str -> { if (str == null) return true; // Random - return str.songName.toLowerCase().startsWith(songFilter.filterData); + return str.songName.toLowerCase().startsWith(songFilter.filterData ?? ''); }); case ALL: // no filter! @@ -880,32 +889,28 @@ class FreeplayState extends MusicBeatSubState var sparks:FlxSprite; var sparksADD:FlxSprite; - function rankAnimStart(fromResults:Null):Void + function rankAnimStart(fromResults:FromResultsParams):Void { busy = true; grpCapsules.members[curSelected].sparkle.alpha = 0; // grpCapsules.members[curSelected].forcePosition(); - if (fromResults != null) - { - rememberedSongId = fromResults.songId; - rememberedDifficulty = fromResults.difficultyId; - changeSelection(); - changeDiff(); - } + rememberedSongId = fromResults.songId; + rememberedDifficulty = fromResults.difficultyId; + changeSelection(); + changeDiff(); - dj.fistPump(); + if (dj != null) dj.fistPump(); // rankCamera.fade(FlxColor.BLACK, 0.5, true); rankCamera.fade(0xFF000000, 0.5, true, null, true); if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; rankBg.alpha = 1; - if (fromResults?.oldRank != null) + if (fromResults.oldRank != null) { grpCapsules.members[curSelected].fakeRanking.rank = fromResults.oldRank; grpCapsules.members[curSelected].fakeBlurredRanking.rank = fromResults.oldRank; - sparks = new FlxSprite(0, 0); sparks.frames = Paths.getSparrowAtlas('freeplay/sparks'); sparks.animation.addByPrefix('sparks', 'sparks', 24, false); sparks.visible = false; @@ -915,7 +920,6 @@ class FreeplayState extends MusicBeatSubState add(sparks); sparks.cameras = [rankCamera]; - sparksADD = new FlxSprite(0, 0); sparksADD.visible = false; sparksADD.frames = Paths.getSparrowAtlas('freeplay/sparksadd'); sparksADD.animation.addByPrefix('sparks add', 'sparks add', 24, false); @@ -980,14 +984,14 @@ class FreeplayState extends MusicBeatSubState grpCapsules.members[curSelected].ranking.scale.set(20, 20); grpCapsules.members[curSelected].blurredRanking.scale.set(20, 20); - if (fromResults?.newRank != null) + if (fromResults != null && fromResults.newRank != null) { grpCapsules.members[curSelected].ranking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true); } FlxTween.tween(grpCapsules.members[curSelected].ranking, {"scale.x": 1, "scale.y": 1}, 0.1); - if (fromResults?.newRank != null) + if (fromResults != null && fromResults.newRank != null) { grpCapsules.members[curSelected].blurredRanking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true); } @@ -1078,11 +1082,11 @@ class FreeplayState extends MusicBeatSubState if (fromResultsParams?.newRank == SHIT) { - dj.pumpFistBad(); + if (dj != null) dj.pumpFistBad(); } else { - dj.pumpFist(); + if (dj != null) dj.pumpFist(); } rankCamera.zoom = 0.8; @@ -1196,7 +1200,13 @@ class FreeplayState extends MusicBeatSubState #if debug if (FlxG.keys.justPressed.T) { - rankAnimStart(fromResultsParams); + rankAnimStart(fromResultsParams ?? + { + playRankAnim: true, + newRank: PERFECT_GOLD, + songId: "tutorial", + difficultyId: "hard" + }); } if (FlxG.keys.justPressed.P) @@ -1427,7 +1437,7 @@ class FreeplayState extends MusicBeatSubState } spamTimer += elapsed; - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); } else { @@ -1438,31 +1448,31 @@ class FreeplayState extends MusicBeatSubState #if !html5 if (FlxG.mouse.wheel != 0) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeSelection(-Math.round(FlxG.mouse.wheel)); } #else if (FlxG.mouse.wheel < 0) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeSelection(-Math.round(FlxG.mouse.wheel / 8)); } else if (FlxG.mouse.wheel > 0) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeSelection(-Math.round(FlxG.mouse.wheel / 8)); } #end if (controls.UI_LEFT_P) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeDiff(-1); generateSongList(currentFilter, true); } if (controls.UI_RIGHT_P) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeDiff(1); generateSongList(currentFilter, true); } @@ -1472,7 +1482,7 @@ class FreeplayState extends MusicBeatSubState busy = true; FlxTween.globalManager.clear(); FlxTimer.globalManager.clear(); - dj.onIntroDone.removeAll(); + if (dj != null) dj.onIntroDone.removeAll(); FunkinSound.playOnce(Paths.sound('cancelMenu')); @@ -1498,7 +1508,8 @@ class FreeplayState extends MusicBeatSubState for (grpSpr in exitMovers.keys()) { - var moveData:MoveData = exitMovers.get(grpSpr); + var moveData:Null = exitMovers.get(grpSpr); + if (moveData == null) continue; for (spr in grpSpr) { @@ -1506,14 +1517,14 @@ class FreeplayState extends MusicBeatSubState var funnyMoveShit:MoveData = moveData; - if (moveData.x == null) funnyMoveShit.x = spr.x; - if (moveData.y == null) funnyMoveShit.y = spr.y; - if (moveData.speed == null) funnyMoveShit.speed = 0.2; - if (moveData.wait == null) funnyMoveShit.wait = 0; + var moveDataX = funnyMoveShit.x ?? spr.x; + var moveDataY = funnyMoveShit.y ?? spr.y; + var moveDataSpeed = funnyMoveShit.speed ?? 0.2; + var moveDataWait = funnyMoveShit.wait ?? 0; - FlxTween.tween(spr, {x: funnyMoveShit.x, y: funnyMoveShit.y}, funnyMoveShit.speed, {ease: FlxEase.expoIn}); + FlxTween.tween(spr, {x: moveDataX, y: moveDataY}, moveDataSpeed, {ease: FlxEase.expoIn}); - longestTimer = Math.max(longestTimer, funnyMoveShit.speed + funnyMoveShit.wait); + longestTimer = Math.max(longestTimer, moveDataSpeed + moveDataWait); } } @@ -1586,19 +1597,18 @@ class FreeplayState extends MusicBeatSubState var daSong:Null = grpCapsules.members[curSelected].songData; if (daSong != null) { - // TODO: Make this actually be the variation you're focused on. We don't need to fetch the song metadata just to calculate it. - var targetSong:Song = SongRegistry.instance.fetchEntry(grpCapsules.members[curSelected].songData.songId); + var targetSong:Null = SongRegistry.instance.fetchEntry(daSong.songId); if (targetSong == null) { - FlxG.log.warn('WARN: could not find song with id (${grpCapsules.members[curSelected].songData.songId})'); + FlxG.log.warn('WARN: could not find song with id (${daSong.songId})'); return; } - var targetVariation:String = targetSong.getFirstValidVariation(currentDifficulty); + var targetVariation:String = targetSong.getFirstValidVariation(currentDifficulty) ?? ''; // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. var suffixedDifficulty = (targetVariation != Constants.DEFAULT_VARIATION && targetVariation != 'erect') ? '$currentDifficulty-${targetVariation}' : currentDifficulty; - var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, suffixedDifficulty); + var songScore:Null = Save.instance.getSongScore(daSong.songId, suffixedDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); rememberedDifficulty = currentDifficulty; @@ -1660,7 +1670,7 @@ class FreeplayState extends MusicBeatSubState } // Set the album graphic and play the animation if relevant. - var newAlbumId:String = daSong?.albumId; + var newAlbumId:Null = daSong?.albumId; if (albumRoll.albumId != newAlbumId) { albumRoll.albumId = newAlbumId; @@ -1698,7 +1708,7 @@ class FreeplayState extends MusicBeatSubState }); trace('Available songs: ${availableSongCapsules.map(function(cap) { - return cap.songData.songName; + return cap?.songData?.songName; })}'); if (availableSongCapsules.length == 0) @@ -1727,17 +1737,20 @@ class FreeplayState extends MusicBeatSubState PlayStatePlaylist.isStoryMode = false; - var targetSong:Song = SongRegistry.instance.fetchEntry(cap.songData.songId); - if (targetSong == null) + var targetSongId:String = cap?.songData?.songId ?? 'unknown'; + var targetSongNullable:Null = SongRegistry.instance.fetchEntry(targetSongId); + if (targetSongNullable == null) { - FlxG.log.warn('WARN: could not find song with id (${cap.songData.songId})'); + FlxG.log.warn('WARN: could not find song with id (${targetSongId})'); return; } + var targetSong:Song = targetSongNullable; var targetDifficultyId:String = currentDifficulty; - var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter); - PlayStatePlaylist.campaignId = cap.songData.levelId; + var targetVariation:Null = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter); + var targetLevelId:Null = cap?.songData?.levelId; + PlayStatePlaylist.campaignId = targetLevelId ?? null; - var targetDifficulty:SongDifficulty = targetSong.getDifficulty(targetDifficultyId, targetVariation); + var targetDifficulty:Null = targetSong.getDifficulty(targetDifficultyId, targetVariation); if (targetDifficulty == null) { FlxG.log.warn('WARN: could not find difficulty with id (${targetDifficultyId})'); @@ -1759,7 +1772,7 @@ class FreeplayState extends MusicBeatSubState // Visual and audio effects. FunkinSound.playOnce(Paths.sound('confirmMenu')); - dj.confirm(); + if (dj != null) dj.confirm(); grpCapsules.members[curSelected].forcePosition(); grpCapsules.members[curSelected].songText.flickerText(); @@ -1801,7 +1814,7 @@ class FreeplayState extends MusicBeatSubState new FlxTimer().start(1, function(tmr:FlxTimer) { FunkinSound.emptyPartialQueue(); - Paths.setCurrentLevel(cap.songData.levelId); + Paths.setCurrentLevel(cap?.songData?.levelId); LoadingState.loadPlayState( { targetSong: targetSong, @@ -1856,7 +1869,7 @@ class FreeplayState extends MusicBeatSubState var daSongCapsule:SongMenuItem = grpCapsules.members[curSelected]; if (daSongCapsule.songData != null) { - var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty); + var songScore:Null = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); diffIdsCurrent = daSongCapsule.songData.songDifficulties; @@ -1906,7 +1919,10 @@ class FreeplayState extends MusicBeatSubState } else { - var previewSong:Null = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId); + var previewSongId:Null = daSongCapsule?.songData?.songId; + if (previewSongId == null) return; + + var previewSong:Null = SongRegistry.instance.fetchEntry(previewSongId); var songDifficulty = previewSong?.getDifficulty(currentDifficulty, previewSong?.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST); var baseInstrumentalId:String = songDifficulty?.characters?.instrumental ?? ''; @@ -1924,7 +1940,9 @@ class FreeplayState extends MusicBeatSubState instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; - FunkinSound.playMusic(daSongCapsule.songData.songId, + trace('Attempting to play partial preview: ${previewSongId}:${instSuffix}'); + + FunkinSound.playMusic(previewSongId, { startingVolume: 0.0, overrideExisting: true, @@ -1951,7 +1969,7 @@ class FreeplayState extends MusicBeatSubState public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState { var result:MainMenuState; - if (params?.fromResults?.playRankAnim) result = new MainMenuState(true); + if (params?.fromResults?.playRankAnim ?? false) result = new MainMenuState(true); else result = new MainMenuState(false); diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index 282e35d7a..6d7b96c58 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -82,6 +82,11 @@ class PlayableCharacter implements IRegistryEntry return _data.freeplayDJ; } + public function getFreeplayDJText(index:Int):String + { + return _data.freeplayDJ.getFreeplayDJText(index); + } + /** * Returns whether this character is unlocked. */ diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index d09536eea..56ffc9a27 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -117,7 +117,10 @@ class MainMenuState extends MusicBeatState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - openSubState(new FreeplayState()); + openSubState(new FreeplayState( + { + character: FlxG.keys.pressed.SHIFT ? 'pico' : 'bf', + })); }); #if CAN_OPEN_LINKS diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx index c5ac175be..f6d3721f0 100644 --- a/source/funkin/util/SortUtil.hx +++ b/source/funkin/util/SortUtil.hx @@ -97,7 +97,7 @@ class SortUtil * @param b The second string to compare. * @return 1 if `a` comes before `b`, -1 if `b` comes before `a`, 0 if they are equal */ - public static function alphabetically(a:String, b:String):Int + public static function alphabetically(?a:String, ?b:String):Int { a = a.toUpperCase(); b = b.toUpperCase(); From 84dd1cd3b98d58d230196036db61556ad40eae43 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 28 Jun 2024 22:29:59 -0400 Subject: [PATCH 07/20] Fix several character animation issues --- assets | 2 +- source/funkin/modding/events/ScriptEvent.hx | 3 +- source/funkin/play/character/BaseCharacter.hx | 109 ++++++++++++++---- source/funkin/util/Constants.hx | 15 +++ 4 files changed, 102 insertions(+), 27 deletions(-) diff --git a/assets b/assets index 225e248f1..9050732ec 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 225e248f148a92500a6fe90e4f10e4cd2acee782 +Subproject commit 9050732ec1cc69cbca9a9a73ee817459f84bdc53 diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index dd55de23b..70055b262 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -151,7 +151,8 @@ class HitNoteScriptEvent extends NoteScriptEvent public var hitDiff:Float = 0; /** - * If the hit causes a notesplash + * Whether this note hit causes a note splash to display. + * Defaults to true only on "sick" notes. */ public var doesNotesplash:Bool = false; diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 4ef86c6a9..980cf2106 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -308,13 +308,26 @@ class BaseCharacter extends Bopper // so we can query which ones are available. this.comboNoteCounts = findCountAnimations('combo'); // example: combo50 this.dropNoteCounts = findCountAnimations('drop'); // example: drop50 - // trace('${this.animation.getNameList()}'); - // trace('Combo note counts: ' + this.comboNoteCounts); - // trace('Drop note counts: ' + this.dropNoteCounts); + if (comboNoteCounts.length > 0) trace('Combo note counts: ' + this.comboNoteCounts); + if (dropNoteCounts.length > 0) trace('Drop note counts: ' + this.dropNoteCounts); super.onCreate(event); } + override function onAnimationFinished(animationName:String):Void + { + super.onAnimationFinished(animationName); + + trace('${characterId} has finished animation: ${animationName}'); + if ((animationName.endsWith(Constants.ANIMATION_END_SUFFIX) && !animationName.startsWith('idle') && !animationName.startsWith('dance')) + || animationName.startsWith('combo') + || animationName.startsWith('drop')) + { + // Force the character to play the idle after the animation ends. + this.dance(true); + } + } + function resetCameraFocusPoint():Void { // Calculate the camera focus point @@ -368,9 +381,11 @@ class BaseCharacter extends Bopper // and Darnell (this keeps the flame on his lighter flickering). // Works for idle, singLEFT/RIGHT/UP/DOWN, alt singing animations, and anything else really. - if (!getCurrentAnimation().endsWith('-hold') && hasAnimation(getCurrentAnimation() + '-hold') && isAnimationFinished()) + if (!getCurrentAnimation().endsWith(Constants.ANIMATION_HOLD_SUFFIX) + && hasAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX) + && isAnimationFinished()) { - playAnimation(getCurrentAnimation() + '-hold'); + playAnimation(getCurrentAnimation() + Constants.ANIMATION_HOLD_SUFFIX); } // Handle character note hold time. @@ -395,7 +410,25 @@ class BaseCharacter extends Bopper { trace('holdTimer reached ${holdTimer}sec (> ${singTimeSec}), stopping sing animation'); holdTimer = 0; - dance(true); + + var currentAnimation:String = getCurrentAnimation(); + // Strip "-hold" from the end. + if (currentAnimation.endsWith(Constants.ANIMATION_HOLD_SUFFIX)) currentAnimation = currentAnimation.substring(0, + currentAnimation.length - Constants.ANIMATION_HOLD_SUFFIX.length); + + var endAnimation:String = currentAnimation + Constants.ANIMATION_END_SUFFIX; + if (hasAnimation(endAnimation)) + { + // Play the '-end' animation, if one exists. + trace('${characterId}: playing ${endAnimation}'); + playAnimation(endAnimation); + } + else + { + // Play the idle animation. + trace('${characterId}: attempting dance'); + dance(true); + } } } else @@ -408,7 +441,8 @@ class BaseCharacter extends Bopper public function isSinging():Bool { - return getCurrentAnimation().startsWith('sing'); + var currentAnimation:String = getCurrentAnimation(); + return currentAnimation.startsWith('sing') && !currentAnimation.endsWith(Constants.ANIMATION_END_SUFFIX); } override function dance(force:Bool = false):Void @@ -418,15 +452,15 @@ class BaseCharacter extends Bopper if (!force) { + // Prevent dancing while a singing animation is playing. if (isSinging()) return; + // Prevent dancing while a non-idle special animation is playing. var currentAnimation:String = getCurrentAnimation(); - if ((currentAnimation == 'hey' || currentAnimation == 'cheer') && !isAnimationFinished()) return; + if (!currentAnimation.startsWith('dance') && !currentAnimation.startsWith('idle') && !isAnimationFinished()) return; } - // Prevent dancing while another animation is playing. - if (!force && isSinging()) return; - + trace('${characterId}: Actually dancing'); // Otherwise, fallback to the super dance() method, which handles playing the idle animation. super.dance(); } @@ -499,6 +533,16 @@ class BaseCharacter extends Bopper this.playSingAnimation(event.note.noteData.getDirection(), false); holdTimer = 0; } + else if (characterType == GF && event.note.noteData.getMustHitNote()) + { + switch (event.judgement) + { + case 'sick' | 'good': + playComboAnimation(event.comboCount); + default: + playComboDropAnimation(event.comboCount); + } + } } /** @@ -521,25 +565,40 @@ class BaseCharacter extends Bopper } else if (event.note.noteData.getMustHitNote() && characterType == GF) { - var dropAnim = ''; + playComboDropAnimation(Highscore.tallies.combo); + } + } - // Choose the combo drop anim to play. - // If there are several (for example, drop10 and drop50) the highest one will be used. - // If the combo count is too low, no animation will be played. - for (count in dropNoteCounts) - { - if (event.comboCount >= count) - { - dropAnim = 'drop${count}'; - } - } + function playComboAnimation(comboCount:Int):Void + { + var comboAnim = 'combo${comboCount}'; + if (hasAnimation(comboAnim)) + { + trace('Playing GF combo animation: ${comboAnim}'); + this.playAnimation(comboAnim, true, true); + } + } - if (dropAnim != '') + function playComboDropAnimation(comboCount:Int):Void + { + var dropAnim:Null = null; + + // Choose the combo drop anim to play. + // If there are several (for example, drop10 and drop50) the highest one will be used. + // If the combo count is too low, no animation will be played. + for (count in dropNoteCounts) + { + if (comboCount >= count) { - trace('Playing GF combo drop animation: ${dropAnim}'); - this.playAnimation(dropAnim, true, true); + dropAnim = 'drop${count}'; } } + + if (dropAnim != null) + { + trace('Playing GF combo drop animation: ${dropAnim}'); + this.playAnimation(dropAnim, true, true); + } } /** diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 1e0978839..2d4fef1f4 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -283,6 +283,21 @@ class Constants */ public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4; + /** + * ANIMATIONS + */ + // ============================== + + /** + * A suffix used for animations played when an animation would loop. + */ + public static final ANIMATION_HOLD_SUFFIX:String = '-hold'; + + /** + * A suffix used for animations played when an animation would end before transitioning to another. + */ + public static final ANIMATION_END_SUFFIX:String = '-end'; + /** * TIMING */ From 9d9d7a853cee3e162fb4ddb041e2ed67fddb3701 Mon Sep 17 00:00:00 2001 From: AbnormalPoof Date: Mon, 24 Jun 2024 03:55:59 -0500 Subject: [PATCH 08/20] video cutscene autopause magic --- source/funkin/play/PlayState.hx | 16 ++++++++++++++-- source/funkin/play/cutscene/VideoCutscene.hx | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index f55cef388..de8492882 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1301,12 +1301,18 @@ class PlayState extends MusicBeatSubState super.closeSubState(); } - #if discord_rpc /** * Function called when the game window gains focus. */ public override function onFocus():Void { + if (VideoCutscene.isPlaying() && FlxG.autoPause && isGamePaused) VideoCutscene.pauseVideo(); + #if html5 + else + VideoCutscene.resumeVideo(); + #end + + #if discord_rpc if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) { if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song @@ -1318,6 +1324,7 @@ class PlayState extends MusicBeatSubState else DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); } + #end super.onFocus(); } @@ -1327,12 +1334,17 @@ class PlayState extends MusicBeatSubState */ public override function onFocusLost():Void { + #if html5 + if (FlxG.autoPause) VideoCutscene.pauseVideo(); + #end + + #if discord_rpc if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); + #end super.onFocusLost(); } - #end /** * Removes any references to the current stage, then clears the stage cache, diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx index 01a492a77..abbcd4f54 100644 --- a/source/funkin/play/cutscene/VideoCutscene.hx +++ b/source/funkin/play/cutscene/VideoCutscene.hx @@ -145,7 +145,7 @@ class VideoCutscene { vid.zIndex = 0; vid.bitmap.onEndReached.add(finishVideo.bind(0.5)); - vid.autoPause = false; + vid.autoPause = FlxG.autoPause; vid.cameras = [PlayState.instance.camCutscene]; From 6bf28a54165a57cd15beb945b7ac1ac6c7691696 Mon Sep 17 00:00:00 2001 From: gamerbross <55158797+gamerbross@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:05:01 +0200 Subject: [PATCH 09/20] Implement danceEvery for Characters --- source/funkin/play/character/BaseCharacter.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 4ef86c6a9..f9e1f00f2 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -180,6 +180,7 @@ class BaseCharacter extends Bopper { this.characterName = _data.name; this.name = _data.name; + this.danceEvery = _data.danceEvery; this.singTimeSteps = _data.singTime; this.globalOffsets = _data.offsets; this.flipX = _data.flipX; From b022581e2321eb0b214f0745ae39e35bf485192a Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 28 Jun 2024 23:25:59 -0400 Subject: [PATCH 10/20] Make danceEvery a float precise up to 0.25 beats (1 step) --- source/funkin/data/stage/StageData.hx | 8 ++--- source/funkin/data/story/level/LevelData.hx | 8 +++-- source/funkin/play/character/BaseCharacter.hx | 2 +- source/funkin/play/character/CharacterData.hx | 36 ++++++++++--------- source/funkin/play/stage/Bopper.hx | 18 ++++++---- 5 files changed, 40 insertions(+), 32 deletions(-) diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx index bebd86d02..eda8e3148 100644 --- a/source/funkin/data/stage/StageData.hx +++ b/source/funkin/data/stage/StageData.hx @@ -140,12 +140,12 @@ typedef StageDataProp = * If not zero, this prop will play an animation every X beats of the song. * This requires animations to be defined. If `danceLeft` and `danceRight` are defined, * they will alternated between, otherwise the `idle` animation will be used. - * - * @default 0 + * Supports up to 0.25 precision. + * @default 0.0 */ - @:default(0) + @:default(0.0) @:optional - var danceEvery:Int; + var danceEvery:Float; /** * How much the prop scrolls relative to the camera. Used to create a parallax effect. diff --git a/source/funkin/data/story/level/LevelData.hx b/source/funkin/data/story/level/LevelData.hx index ceb2cc054..d01689a82 100644 --- a/source/funkin/data/story/level/LevelData.hx +++ b/source/funkin/data/story/level/LevelData.hx @@ -91,11 +91,13 @@ typedef LevelPropData = /** * The frequency to bop at, in beats. - * @default 1 = every beat, 2 = every other beat, etc. + * 1 = every beat, 2 = every other beat, etc. + * Supports up to 0.25 precision. + * @default 0.0 */ - @:default(1) + @:default(0.0) @:optional - var danceEvery:Int; + var danceEvery:Float; /** * The offset on the position to render the prop at. diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index f9e1f00f2..863fccafa 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -164,7 +164,7 @@ class BaseCharacter extends Bopper public function new(id:String, renderType:CharacterRenderType) { - super(); + super(CharacterDataParser.DEFAULT_DANCEEVERY); this.characterId = id; _data = CharacterDataParser.fetchCharacterData(this.characterId); diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index 8b1649e26..d447eb97f 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -383,21 +383,21 @@ class CharacterDataParser * Values that are too high will cause the character to hold their singing pose for too long after they're done. * @default `8 steps` */ - static final DEFAULT_SINGTIME:Float = 8.0; + public static final DEFAULT_SINGTIME:Float = 8.0; - static final DEFAULT_DANCEEVERY:Int = 1; - static final DEFAULT_FLIPX:Bool = false; - static final DEFAULT_FLIPY:Bool = false; - static final DEFAULT_FRAMERATE:Int = 24; - static final DEFAULT_ISPIXEL:Bool = false; - static final DEFAULT_LOOP:Bool = false; - static final DEFAULT_NAME:String = 'Untitled Character'; - static final DEFAULT_OFFSETS:Array = [0, 0]; - static final DEFAULT_HEALTHICON_OFFSETS:Array = [0, 25]; - static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.Sparrow; - static final DEFAULT_SCALE:Float = 1; - static final DEFAULT_SCROLL:Array = [0, 0]; - static final DEFAULT_STARTINGANIM:String = 'idle'; + public static final DEFAULT_DANCEEVERY:Float = 1.0; + public static final DEFAULT_FLIPX:Bool = false; + public static final DEFAULT_FLIPY:Bool = false; + public static final DEFAULT_FRAMERATE:Int = 24; + public static final DEFAULT_ISPIXEL:Bool = false; + public static final DEFAULT_LOOP:Bool = false; + public static final DEFAULT_NAME:String = 'Untitled Character'; + public static final DEFAULT_OFFSETS:Array = [0, 0]; + public static final DEFAULT_HEALTHICON_OFFSETS:Array = [0, 25]; + public static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.Sparrow; + public static final DEFAULT_SCALE:Float = 1; + public static final DEFAULT_SCROLL:Array = [0, 0]; + public static final DEFAULT_STARTINGANIM:String = 'idle'; /** * Set unspecified parameters to their defaults. @@ -665,10 +665,12 @@ typedef CharacterData = /** * The frequency at which the character will play its idle animation, in beats. * Increasing this number will make the character dance less often. - * - * @default 1 + * Supports up to `0.25` precision. + * @default `1.0` on characters */ - var danceEvery:Null; + @:optional + @:default(1.0) + var danceEvery:Null; /** * The minimum duration that a character will play a note animation for, in beats. diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 11fb9e499..0061e85fb 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -19,8 +19,10 @@ class Bopper extends StageProp implements IPlayStateScriptedClass /** * The bopper plays the dance animation once every `danceEvery` beats. * Set to 0 to disable idle animation. + * Supports up to 0.25 precision. + * @default 0.0 on props, 1.0 on characters */ - public var danceEvery:Int = 1; + public var danceEvery:Float = 0.0; /** * Whether the bopper should dance left and right. @@ -110,7 +112,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass */ var hasDanced:Bool = false; - public function new(danceEvery:Int = 1) + public function new(danceEvery:Float = 0.0) { super(); this.danceEvery = danceEvery; @@ -171,16 +173,20 @@ class Bopper extends StageProp implements IPlayStateScriptedClass } /** - * Called once every beat of the song. + * Called once every step of the song. */ - public function onBeatHit(event:SongTimeScriptEvent):Void + public function onStepHit(event:SongTimeScriptEvent) { - if (danceEvery > 0 && event.beat % danceEvery == 0) + if (danceEvery > 0) trace('step hit(${danceEvery}): ${event.step % (danceEvery * Constants.STEPS_PER_BEAT)} == 0?'); + if (danceEvery > 0 && (event.step % (danceEvery * Constants.STEPS_PER_BEAT)) == 0) { + trace('dance onStepHit!'); dance(shouldBop); } } + public function onBeatHit(event:SongTimeScriptEvent):Void {} + /** * Called every `danceEvery` beats of the song. */ @@ -367,8 +373,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {} - public function onStepHit(event:SongTimeScriptEvent) {} - public function onCountdownStart(event:CountdownScriptEvent) {} public function onCountdownStep(event:CountdownScriptEvent) {} From 03eab140f15ca68610892f0720b5c139217522b4 Mon Sep 17 00:00:00 2001 From: gamerbross <55158797+gamerbross@users.noreply.github.com> Date: Thu, 27 Jun 2024 21:00:25 +0200 Subject: [PATCH 11/20] FunkinSound.playOnce return sound --- source/funkin/audio/FunkinSound.hx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 11b713f4d..c6a9650dd 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -535,11 +535,12 @@ class FunkinSound extends FlxSound implements ICloneable * Play a sound effect once, then destroy it. * @param key * @param volume - * @return static function construct():FunkinSound + * @return A `FunkinSound` object, or `null` if the sound could not be loaded. */ - public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Void + public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound { var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad); + return result; } /** From 62501a1dedd444426ed7cd946b70df880940e371 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 29 Jun 2024 00:03:28 -0400 Subject: [PATCH 12/20] Fix null safety error. --- source/funkin/audio/FunkinSound.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index c6a9650dd..9cb131c91 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -537,7 +537,7 @@ class FunkinSound extends FlxSound implements ICloneable * @param volume * @return A `FunkinSound` object, or `null` if the sound could not be loaded. */ - public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound + public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Null { var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad); return result; From 837efcea369467bf69c98e9d7cc5d0e57ad43c64 Mon Sep 17 00:00:00 2001 From: AppleHair <95587502+AppleHair@users.noreply.github.com> Date: Mon, 10 Jun 2024 19:42:27 +0300 Subject: [PATCH 13/20] [BUGFIX] Made freeplay use the metadata to get the instrumental suffix Song previews in freeplay will now use the instrumental suffix from the current difficulty's corresponding song variation metadata instead of using the variation id as an instrumental suffix and checking only the "erect" variation. --- source/funkin/ui/freeplay/FreeplayState.hx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 3b003cf89..92410622f 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1882,7 +1882,10 @@ class FreeplayState extends MusicBeatSubState } else { - var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : ""; + var previewSong:Null = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId); + var instSuffix:String = previewSong?.getDifficulty(currentDifficulty, + previewSong?.variations ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? ''; + instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; FunkinSound.playMusic(daSongCapsule.songData.songId, { startingVolume: 0.0, @@ -1890,7 +1893,7 @@ class FreeplayState extends MusicBeatSubState restartTrack: false, mapTimeChanges: false, // The music metadata is not alongside the audio file so this won't work. pathsFunction: INST, - suffix: potentiallyErect, + suffix: instSuffix, partialParams: { loadPartial: true, From b1021530d8bb97e259c9443a62d306c96f54147d Mon Sep 17 00:00:00 2001 From: AppleHair <95587502+AppleHair@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:23:45 +0300 Subject: [PATCH 14/20] now using getVariationsByCharId instead --- source/funkin/ui/freeplay/FreeplayState.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 92410622f..69b499e80 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1884,7 +1884,7 @@ class FreeplayState extends MusicBeatSubState { var previewSong:Null = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId); var instSuffix:String = previewSong?.getDifficulty(currentDifficulty, - previewSong?.variations ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? ''; + previewSong?.getVariationsByCharId(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? ''; instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; FunkinSound.playMusic(daSongCapsule.songData.songId, { From 72a00b9ae20ff2f2ee20fc58bd7c6fb7f0687696 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 18 Jun 2024 17:56:24 -0400 Subject: [PATCH 15/20] Implemented playable character registry, added Freeplay character filtering, added alt instrumental support --- assets | 2 +- source/funkin/InitState.hx | 2 + .../funkin/data/freeplay/player/CHANGELOG.md | 9 ++ .../funkin/data/freeplay/player/PlayerData.hx | 63 ++++++++ .../data/freeplay/player/PlayerRegistry.hx | 151 ++++++++++++++++++ source/funkin/modding/PolymodHandler.hx | 6 +- source/funkin/play/song/Song.hx | 34 ++-- .../handlers/ChartEditorDialogHandler.hx | 7 +- source/funkin/ui/freeplay/FreeplayState.hx | 80 +++++++--- .../freeplay/charselect/PlayableCharacter.hx | 108 +++++++++++++ .../charselect/ScriptedPlayableCharacter.hx | 8 + source/funkin/util/VersionUtil.hx | 1 - 12 files changed, 433 insertions(+), 38 deletions(-) create mode 100644 source/funkin/data/freeplay/player/CHANGELOG.md create mode 100644 source/funkin/data/freeplay/player/PlayerData.hx create mode 100644 source/funkin/data/freeplay/player/PlayerRegistry.hx create mode 100644 source/funkin/ui/freeplay/charselect/PlayableCharacter.hx create mode 100644 source/funkin/ui/freeplay/charselect/ScriptedPlayableCharacter.hx diff --git a/assets b/assets index 0dbd2e96c..bc1650ba7 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 0dbd2e96ca25ab5966cef05db6c76fe7fb145abf +Subproject commit bc1650ba789d675683a8c0cc27b1e2a42cb686cf diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 49b15ddf6..c2a56bdc2 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.data.freeplay.player.PlayerRegistry; import funkin.ui.debug.charting.ChartEditorState; import funkin.ui.transition.LoadingState; import flixel.FlxState; @@ -164,6 +165,7 @@ class InitState extends FlxState SongRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries(); + PlayerRegistry.instance.loadEntries(); ConversationRegistry.instance.loadEntries(); DialogueBoxRegistry.instance.loadEntries(); SpeakerRegistry.instance.loadEntries(); diff --git a/source/funkin/data/freeplay/player/CHANGELOG.md b/source/funkin/data/freeplay/player/CHANGELOG.md new file mode 100644 index 000000000..7a31e11ca --- /dev/null +++ b/source/funkin/data/freeplay/player/CHANGELOG.md @@ -0,0 +1,9 @@ +# Freeplay Playable Character Data Schema Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] +Initial release. diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx new file mode 100644 index 000000000..d7b814584 --- /dev/null +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -0,0 +1,63 @@ +package funkin.data.freeplay.player; + +import funkin.data.animation.AnimationData; + +@:nullSafety +class PlayerData +{ + /** + * The sematic version number of the player data JSON format. + * Supports fancy comparisons like NPM does it's neat. + */ + @:default(funkin.data.freeplay.player.PlayerRegistry.PLAYER_DATA_VERSION) + public var version:String; + + /** + * A readable name for this playable character. + */ + public var name:String = 'Unknown'; + + /** + * The character IDs this character is associated with. + * Only songs that use these characters will show up in Freeplay. + */ + @:default([]) + public var ownedChars:Array = []; + + /** + * Whether to show songs with character IDs that aren't associated with any specific character. + */ + @:optional + @:default(false) + public var showUnownedChars:Bool = false; + + /** + * Whether this character is unlocked by default. + * Use a ScriptedPlayableCharacter to add custom logic. + */ + @:optional + @:default(true) + public var unlocked:Bool = true; + + public function new() + { + this.version = PlayerRegistry.PLAYER_DATA_VERSION; + } + + /** + * Convert this StageData into a JSON string. + */ + public function serialize(pretty:Bool = true):String + { + // Update generatedBy and version before writing. + updateVersionToLatest(); + + var writer = new json2object.JsonWriter(); + return writer.write(this, pretty ? ' ' : null); + } + + public function updateVersionToLatest():Void + { + this.version = PlayerRegistry.PLAYER_DATA_VERSION; + } +} diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx new file mode 100644 index 000000000..3de9efd41 --- /dev/null +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -0,0 +1,151 @@ +package funkin.data.freeplay.player; + +import funkin.data.freeplay.player.PlayerData; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter; + +class PlayerRegistry extends BaseRegistry +{ + /** + * The current version string for the stage data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migratePlayerData()` function. + */ + public static final PLAYER_DATA_VERSION:thx.semver.Version = "1.0.0"; + + public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; + + public static var instance(get, never):PlayerRegistry; + static var _instance:Null = null; + + static function get_instance():PlayerRegistry + { + if (_instance == null) _instance = new PlayerRegistry(); + return _instance; + } + + /** + * A mapping between stage character IDs and Freeplay playable character IDs. + */ + var ownedCharacterIds:Map = []; + + public function new() + { + super('PLAYER', 'players', PLAYER_DATA_VERSION_RULE); + } + + public override function loadEntries():Void + { + super.loadEntries(); + + for (playerId in listEntryIds()) + { + var player = fetchEntry(playerId); + if (player == null) continue; + + var currentPlayerCharIds = player.getOwnedCharacterIds(); + for (characterId in currentPlayerCharIds) + { + ownedCharacterIds.set(characterId, playerId); + } + } + + log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.'); + } + + /** + * Get the playable character associated with a given stage character. + * @param characterId The stage character ID. + * @return The playable character. + */ + public function getCharacterOwnerId(characterId:String):String + { + return ownedCharacterIds[characterId]; + } + + /** + * Return true if the given stage character is associated with a specific playable character. + * If so, the level should only appear if that character is selected in Freeplay. + * @param characterId The stage character ID. + * @return Whether the character is owned by any one character. + */ + public function isCharacterOwned(characterId:String):Bool + { + return ownedCharacterIds.exists(characterId); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + */ + public function parseEntryData(id:String):Null + { + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + + switch (loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):PlayableCharacter + { + return ScriptedPlayableCharacter.init(clsName, "unknown"); + } + + function getScriptedClassNames():Array + { + return ScriptedPlayableCharacter.listScriptClasses(); + } + + /** + * A list of all the playable characters from the base game, in order. + */ + public function listBaseGamePlayerIds():Array + { + return ["bf", "pico"]; + } + + /** + * A list of all installed playable characters that are not from the base game. + */ + public function listModdedPlayerIds():Array + { + return listEntryIds().filter(function(id:String):Bool { + return listBaseGamePlayerIds().indexOf(id) == -1; + }); + } +} diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index ae754b780..c352aa606 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -8,6 +8,7 @@ import funkin.data.event.SongEventRegistry; import funkin.data.story.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.song.SongRegistry; +import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.stage.StageRegistry; import funkin.data.freeplay.album.AlbumRegistry; import funkin.modding.module.ModuleHandler; @@ -369,15 +370,18 @@ class PolymodHandler // These MUST be imported at the top of the file and not referred to by fully qualified name, // to ensure build macros work properly. + SongEventRegistry.loadEventCache(); + SongRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries(); - SongEventRegistry.loadEventCache(); + PlayerRegistry.instance.loadEntries(); ConversationRegistry.instance.loadEntries(); DialogueBoxRegistry.instance.loadEntries(); SpeakerRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries(); + CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry. ModuleHandler.loadModuleCache(); } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index dde5ee7b8..91d35d8fa 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -14,6 +14,7 @@ import funkin.data.song.SongData.SongTimeFormat; import funkin.data.song.SongRegistry; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import funkin.modding.events.ScriptEvent; +import funkin.ui.freeplay.charselect.PlayableCharacter; import funkin.util.SortUtil; import openfl.utils.Assets; @@ -401,11 +402,11 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry):Null + public function getFirstValidVariation(?diffId:String, ?currentCharacter:PlayableCharacter, ?possibleVariations:Array):Null { if (possibleVariations == null) { - possibleVariations = variations; + possibleVariations = getVariationsByCharacter(currentCharacter); possibleVariations.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_VARIATION_LIST)); } if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0]; @@ -422,22 +423,29 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + public function getVariationsByCharacter(?char:PlayableCharacter):Array { - if (charId == null) charId = Constants.DEFAULT_CHARACTER; + if (char == null) return variations; - if (variations.contains(charId)) + var result = []; + trace('Evaluating variations for ${this.id} ${char.id}: ${this.variations}'); + for (variation in variations) { - return [charId]; - } - else - { - // TODO: How to exclude character variations while keeping other custom variations? - return variations; + var metadata = _metadata.get(variation); + + var playerCharId = metadata?.playData?.characters?.player; + if (playerCharId == null) continue; + + if (char.shouldShowCharacter(playerCharId)) + { + result.push(variation); + } } + + return result; } /** @@ -455,6 +463,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = song.listDifficulties(displayedVariations, false); - trace(availableDifficultiesForSong); + trace('Available Difficulties: $availableDifficultiesForSong'); if (availableDifficultiesForSong.length == 0) continue; songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); @@ -454,7 +458,7 @@ class FreeplayState extends MusicBeatSubState }); // TODO: Replace this. - if (currentCharacter == 'pico') dj.visible = false; + if (currentCharacterId == 'pico') dj.visible = false; add(dj); @@ -1195,6 +1199,16 @@ class FreeplayState extends MusicBeatSubState rankAnimStart(fromResultsParams); } + if (FlxG.keys.justPressed.P) + { + FlxG.switchState(FreeplayState.build( + { + { + character: currentCharacterId == "pico" ? "bf" : "pico", + } + })); + } + // if (FlxG.keys.justPressed.H) // { // rankDisplayNew(fromResultsParams); @@ -1302,9 +1316,9 @@ class FreeplayState extends MusicBeatSubState { if (busy) return; - var upP:Bool = controls.UI_UP_P && !FlxG.keys.pressed.CONTROL; - var downP:Bool = controls.UI_DOWN_P && !FlxG.keys.pressed.CONTROL; - var accepted:Bool = controls.ACCEPT && !FlxG.keys.pressed.CONTROL; + var upP:Bool = controls.UI_UP_P; + var downP:Bool = controls.UI_DOWN_P; + var accepted:Bool = controls.ACCEPT; if (FlxG.onMobile) { @@ -1378,7 +1392,7 @@ class FreeplayState extends MusicBeatSubState } #end - if (!FlxG.keys.pressed.CONTROL && (controls.UI_UP || controls.UI_DOWN)) + if ((controls.UI_UP || controls.UI_DOWN)) { if (spamming) { @@ -1440,13 +1454,13 @@ class FreeplayState extends MusicBeatSubState } #end - if (controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) + if (controls.UI_LEFT_P) { dj.resetAFKTimer(); changeDiff(-1); generateSongList(currentFilter, true); } - if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) + if (controls.UI_RIGHT_P) { dj.resetAFKTimer(); changeDiff(1); @@ -1720,7 +1734,7 @@ class FreeplayState extends MusicBeatSubState return; } var targetDifficultyId:String = currentDifficulty; - var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId); + var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter); PlayStatePlaylist.campaignId = cap.songData.levelId; var targetDifficulty:SongDifficulty = targetSong.getDifficulty(targetDifficultyId, targetVariation); @@ -1730,8 +1744,18 @@ class FreeplayState extends MusicBeatSubState return; } - // TODO: Change this with alternate instrumentals - var targetInstId:String = targetDifficulty.characters.instrumental; + var baseInstrumentalId:String = targetDifficulty?.characters?.instrumental ?? ''; + var altInstrumentalIds:Array = targetDifficulty?.characters?.altInstrumentals ?? []; + + var targetInstId:String = baseInstrumentalId; + + // TODO: Make this a UI element. + #if (debug || FORCE_DEBUG_VERSION) + if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL) + { + targetInstId = altInstrumentalIds[0]; + } + #end // Visual and audio effects. FunkinSound.playOnce(Paths.sound('confirmMenu')); @@ -1883,9 +1907,23 @@ class FreeplayState extends MusicBeatSubState else { var previewSong:Null = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId); - var instSuffix:String = previewSong?.getDifficulty(currentDifficulty, - previewSong?.getVariationsByCharId(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? ''; + var songDifficulty = previewSong?.getDifficulty(currentDifficulty, + previewSong?.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST); + var baseInstrumentalId:String = songDifficulty?.characters?.instrumental ?? ''; + var altInstrumentalIds:Array = songDifficulty?.characters?.altInstrumentals ?? []; + + var instSuffix:String = baseInstrumentalId; + + // TODO: Make this a UI element. + #if (debug || FORCE_DEBUG_VERSION) + if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL) + { + instSuffix = altInstrumentalIds[0]; + } + #end + instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; + FunkinSound.playMusic(daSongCapsule.songData.songId, { startingVolume: 0.0, @@ -1914,7 +1952,7 @@ class FreeplayState extends MusicBeatSubState public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState { var result:MainMenuState; - if (params?.fromResults.playRankAnim) result = new MainMenuState(true); + if (params?.fromResults?.playRankAnim) result = new MainMenuState(true); else result = new MainMenuState(false); @@ -1952,8 +1990,8 @@ class DifficultySelector extends FlxSprite override function update(elapsed:Float):Void { - if (flipX && controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) moveShitDown(); - if (!flipX && controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) moveShitDown(); + if (flipX && controls.UI_RIGHT_P) moveShitDown(); + if (!flipX && controls.UI_LEFT_P) moveShitDown(); super.update(elapsed); } diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx new file mode 100644 index 000000000..743345004 --- /dev/null +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -0,0 +1,108 @@ +package funkin.ui.freeplay.charselect; + +import funkin.data.IRegistryEntry; +import funkin.data.freeplay.player.PlayerData; +import funkin.data.freeplay.player.PlayerRegistry; + +/** + * An object used to retrieve data about a playable character (also known as "weeks"). + * Can be scripted to override each function, for custom behavior. + */ +class PlayableCharacter implements IRegistryEntry +{ + /** + * The ID of the playable character. + */ + public final id:String; + + /** + * Playable character data as parsed from the JSON file. + */ + public final _data:PlayerData; + + /** + * @param id The ID of the JSON file to parse. + */ + public function new(id:String) + { + this.id = id; + _data = _fetchData(id); + + if (_data == null) + { + throw 'Could not parse playable character data for id: $id'; + } + } + + /** + * Retrieve the readable name of the playable character. + */ + public function getName():String + { + // TODO: Maybe add localization support? + return _data.name; + } + + /** + * Retrieve the list of stage character IDs associated with this playable character. + * @return The list of associated character IDs + */ + public function getOwnedCharacterIds():Array + { + return _data.ownedChars; + } + + /** + * Return `true` if, when this character is selected in Freeplay, + * songs unassociated with a specific character should appear. + */ + public function shouldShowUnownedChars():Bool + { + return _data.showUnownedChars; + } + + public function shouldShowCharacter(id:String):Bool + { + if (_data.ownedChars.contains(id)) + { + return true; + } + + if (_data.showUnownedChars) + { + var result = !PlayerRegistry.instance.isCharacterOwned(id); + return result; + } + + return false; + } + + /** + * Returns whether this character is unlocked. + */ + public function isUnlocked():Bool + { + return _data.unlocked; + } + + /** + * Called when the character is destroyed. + * TODO: Document when this gets called + */ + public function destroy():Void {} + + public function toString():String + { + return 'PlayableCharacter($id)'; + } + + /** + * Retrieve and parse the JSON data for a playable character by ID. + * @param id The ID of the character + * @return The parsed player data, or null if not found or invalid + */ + static function _fetchData(id:String):Null + { + return PlayerRegistry.instance.parseEntryDataWithMigration(id, PlayerRegistry.instance.fetchEntryVersion(id)); + } +} diff --git a/source/funkin/ui/freeplay/charselect/ScriptedPlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/ScriptedPlayableCharacter.hx new file mode 100644 index 000000000..f75a58092 --- /dev/null +++ b/source/funkin/ui/freeplay/charselect/ScriptedPlayableCharacter.hx @@ -0,0 +1,8 @@ +package funkin.ui.freeplay.charselect; + +/** + * A script that can be tied to a PlayableCharacter. + * Create a scripted class that extends PlayableCharacter to use this. + */ +@:hscriptClass +class ScriptedPlayableCharacter extends funkin.ui.freeplay.charselect.PlayableCharacter implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx index 832ce008a..9bf46a188 100644 --- a/source/funkin/util/VersionUtil.hx +++ b/source/funkin/util/VersionUtil.hx @@ -24,7 +24,6 @@ class VersionUtil try { var versionRaw:thx.semver.Version.SemVer = version; - trace('${versionRaw} satisfies (${versionRule})? ${version.satisfies(versionRule)}'); return version.satisfies(versionRule); } catch (e) From 5c2bad888d520117d81264d4690dd046cb1d208b Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 18 Jun 2024 20:07:27 -0400 Subject: [PATCH 16/20] Make Boyfriend DJ animations data driven --- .../funkin/data/freeplay/player/PlayerData.hx | 93 +++++ source/funkin/ui/freeplay/DJBoyfriend.hx | 371 ------------------ source/funkin/ui/freeplay/FreeplayDJ.hx | 369 +++++++++++++++++ source/funkin/ui/freeplay/FreeplayState.hx | 24 +- .../freeplay/charselect/PlayableCharacter.hx | 5 + source/funkin/util/tools/MapTools.hx | 4 + 6 files changed, 483 insertions(+), 383 deletions(-) delete mode 100644 source/funkin/ui/freeplay/DJBoyfriend.hx create mode 100644 source/funkin/ui/freeplay/FreeplayDJ.hx diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index d7b814584..10fc54b78 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -31,6 +31,12 @@ class PlayerData @:default(false) public var showUnownedChars:Bool = false; + /** + * Data for displaying this character in the Freeplay menu. + * If null, display no DJ. + */ + public var freeplayDJ:Null = null; + /** * Whether this character is unlocked by default. * Use a ScriptedPlayableCharacter to add custom logic. @@ -61,3 +67,90 @@ class PlayerData this.version = PlayerRegistry.PLAYER_DATA_VERSION; } } + +class PlayerFreeplayDJData +{ + var assetPath:String; + var animations:Array; + + @:jignored + var animationMap:Map; + + @:optional + var cartoon:Null; + + public function new() + { + animationMap = new Map(); + } + + function mapAnimations() + { + if (animationMap == null) animationMap = new Map(); + + animationMap.clear(); + for (anim in animations) + { + animationMap.set(anim.name, anim); + } + } + + public function getAtlasPath():String + { + return Paths.animateAtlas(assetPath); + } + + public function getAnimationPrefix(name:String):Null + { + if (animationMap.size() == 0) mapAnimations(); + + var anim = animationMap.get(name); + if (anim == null) return null; + return anim.prefix; + } + + public function getAnimationOffsets(name:String):Null> + { + if (animationMap.size() == 0) mapAnimations(); + + var anim = animationMap.get(name); + if (anim == null) return null; + return anim.offsets; + } + + // TODO: These should really be frame labels, ehe. + + public function getCartoonSoundClickFrame():Int + { + return cartoon?.soundClickFrame ?? 80; + } + + public function getCartoonSoundCartoonFrame():Int + { + return cartoon?.soundCartoonFrame ?? 85; + } + + public function getCartoonLoopBlinkFrame():Int + { + return cartoon?.loopBlinkFrame ?? 112; + } + + public function getCartoonLoopFrame():Int + { + return cartoon?.loopFrame ?? 166; + } + + public function getCartoonChannelChangeFrame():Int + { + return cartoon?.channelChangeFrame ?? 60; + } +} + +typedef PlayerFreeplayDJCartoonData = +{ + var soundClickFrame:Int; + var soundCartoonFrame:Int; + var loopBlinkFrame:Int; + var loopFrame:Int; + var channelChangeFrame:Int; +} diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx deleted file mode 100644 index bbf043dd4..000000000 --- a/source/funkin/ui/freeplay/DJBoyfriend.hx +++ /dev/null @@ -1,371 +0,0 @@ -package funkin.ui.freeplay; - -import flixel.FlxSprite; -import flixel.util.FlxSignal; -import funkin.util.assets.FlxAnimationUtil; -import funkin.graphics.adobeanimate.FlxAtlasSprite; -import funkin.audio.FunkinSound; -import flixel.util.FlxTimer; -import funkin.audio.FunkinSound; -import funkin.audio.FlxStreamSound; - -class DJBoyfriend extends FlxAtlasSprite -{ - // Represents the sprite's current status. - // Without state machines I would have driven myself crazy years ago. - public var currentState:DJBoyfriendState = Intro; - - // A callback activated when the intro animation finishes. - public var onIntroDone:FlxSignal = new FlxSignal(); - - // A callback activated when Boyfriend gets spooked. - public var onSpook:FlxSignal = new FlxSignal(); - - // playAnim stolen from Character.hx, cuz im lazy lol! - // TODO: Switch this class to use SwagSprite instead. - public var animOffsets:Map>; - - var gotSpooked:Bool = false; - - static final SPOOK_PERIOD:Float = 60.0; - static final TV_PERIOD:Float = 120.0; - - // Time since dad last SPOOKED you. - var timeSinceSpook:Float = 0; - - public function new(x:Float, y:Float) - { - super(x, y, Paths.animateAtlas("freeplay/freeplay-boyfriend", "preload")); - - animOffsets = new Map>(); - - anim.callback = function(name, number) { - switch (name) - { - case "Boyfriend DJ watchin tv OG": - if (number == 80) - { - FunkinSound.playOnce(Paths.sound('remote_click')); - } - if (number == 85) - { - runTvLogic(); - } - default: - } - }; - - setupAnimations(); - - FlxG.debugger.track(this); - FlxG.console.registerObject("dj", this); - - anim.onComplete = onFinishAnim; - - FlxG.console.registerFunction("tv", function() { - currentState = TV; - }); - } - - /* - [remote hand under,boyfriend top head,brim piece,arm cringe l,red lazer,dj arm in,bf fist pump arm,hand raised right,forearm left,fist shaking,bf smile eyes closed face,arm cringe r,bf clenched face,face shrug,boyfriend falling,blue tint 1,shirt sleeve,bf clenched fist,head BF relaxed,blue tint 2,hand down left,blue tint 3,blue tint 4,head less smooshed,blue tint 5,boyfriend freeplay,BF head slight turn,blue tint 6,arm shrug l,blue tint 7,shoulder raised w sleeve,blue tint 8,fist pump face,blue tint 9,foot rested light,hand turnaround,arm chill right,Boyfriend DJ,arm shrug r,head back bf,hat top piece,dad bod,face surprise snap,Boyfriend DJ fist pump,office chair,foot rested right,chest down,office chair upright,body chill,bf dj afk,head mouth open dad,BF Head defalt HAIR BLOWING,hand shrug l,face piece,foot wag,turn table,shoulder up left,turntable lights,boyfriend dj body shirt blowing,body chunk turned,hand down right,dj arm out,hand shrug r,body chest out,rave hand,palm,chill face default,head back semi bf,boyfriend bottom head,DJ arm,shoulder right dad,bf surprise,boyfriend dj body,hs1,Boyfriend DJ watchin tv OG,spinning disk,hs2,arm chill left,boyfriend dj intro,hs3,hs4,chill face extra,hs5,remote hand upright,hs6,pant over table,face surprise,bf arm peace,arm turnaround,bf eyes 1,arm slammed table,eye squit,leg BF,head mid piece,arm backing,arm swoopin in,shoe right lowering,forearm right,hand out,blue tint 10,body falling back,remote thumb press,shoulder,hair spike single,bf bent - arm,crt,foot raised right,dad hand,chill face 1,chill face 2,clenched fist,head SMOOSHED,shoulder left dad,df1,body chunk upright,df2,df3,df4,hat front piece,df5,foot rested right 2,hand in,arm spun,shoe raised left,bf 1 finger hand,bf mouth 1,Boyfriend DJ confirm,forearm down ,hand raised left,remote thumb up] - */ - override public function listAnimations():Array - { - var anims:Array = []; - @:privateAccess - for (animKey in anim.symbolDictionary) - { - anims.push(animKey.name); - } - return anims; - } - - var lowPumpLoopPoint:Int = 4; - - public override function update(elapsed:Float):Void - { - super.update(elapsed); - - switch (currentState) - { - case Intro: - // Play the intro animation then leave this state immediately. - if (getCurrentAnimation() != 'boyfriend dj intro') playFlashAnimation('boyfriend dj intro', true); - timeSinceSpook = 0; - case Idle: - // We are in this state the majority of the time. - if (getCurrentAnimation() != 'Boyfriend DJ') - { - playFlashAnimation('Boyfriend DJ', true); - } - - if (getCurrentAnimation() == 'Boyfriend DJ' && this.isLoopFinished()) - { - if (timeSinceSpook >= SPOOK_PERIOD && !gotSpooked) - { - currentState = Spook; - } - else if (timeSinceSpook >= TV_PERIOD) - { - currentState = TV; - } - } - timeSinceSpook += elapsed; - case Confirm: - if (getCurrentAnimation() != 'Boyfriend DJ confirm') playFlashAnimation('Boyfriend DJ confirm', false); - timeSinceSpook = 0; - case PumpIntro: - if (getCurrentAnimation() != 'Boyfriend DJ fist pump') playFlashAnimation('Boyfriend DJ fist pump', false); - if (getCurrentAnimation() == 'Boyfriend DJ fist pump' && anim.curFrame >= 4) - { - anim.play("Boyfriend DJ fist pump", true, false, 0); - } - case FistPump: - - case Spook: - if (getCurrentAnimation() != 'bf dj afk') - { - onSpook.dispatch(); - playFlashAnimation('bf dj afk', false); - gotSpooked = true; - } - timeSinceSpook = 0; - case TV: - if (getCurrentAnimation() != 'Boyfriend DJ watchin tv OG') playFlashAnimation('Boyfriend DJ watchin tv OG', true); - timeSinceSpook = 0; - default: - // I shit myself. - } - - if (FlxG.keys.pressed.CONTROL) - { - if (FlxG.keys.justPressed.LEFT) - { - this.offsetX -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); - } - - if (FlxG.keys.justPressed.RIGHT) - { - this.offsetX += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); - } - - if (FlxG.keys.justPressed.UP) - { - this.offsetY -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); - } - - if (FlxG.keys.justPressed.DOWN) - { - this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); - } - - if (FlxG.keys.justPressed.SPACE) - { - currentState = (currentState == Idle ? TV : Idle); - } - } - } - - function onFinishAnim():Void - { - var name = anim.curSymbol.name; - switch (name) - { - case "boyfriend dj intro": - // trace('Finished intro'); - currentState = Idle; - onIntroDone.dispatch(); - case "Boyfriend DJ": - // trace('Finished idle'); - case "bf dj afk": - // trace('Finished spook'); - currentState = Idle; - case "Boyfriend DJ confirm": - - case "Boyfriend DJ fist pump": - currentState = Idle; - - case "Boyfriend DJ loss reaction 1": - currentState = Idle; - - case "Boyfriend DJ watchin tv OG": - var frame:Int = FlxG.random.bool(33) ? 112 : 166; - - // BF switches channels when the video ends, or at a 10% chance each time his idle loops. - if (FlxG.random.bool(5)) - { - frame = 60; - // boyfriend switches channel code? - // runTvLogic(); - } - trace('Replay idle: ${frame}'); - anim.play("Boyfriend DJ watchin tv OG", true, false, frame); - // trace('Finished confirm'); - } - } - - public function resetAFKTimer():Void - { - timeSinceSpook = 0; - gotSpooked = false; - } - - var offsetX:Float = 0.0; - var offsetY:Float = 0.0; - - function setupAnimations():Void - { - // Intro - addOffset('boyfriend dj intro', 8.0 - 1.3, 3.0 - 0.4); - - // Idle - addOffset('Boyfriend DJ', 0, 0); - - // Confirm - addOffset('Boyfriend DJ confirm', 0, 0); - - // AFK: Spook - addOffset('bf dj afk', 649.5, 58.5); - - // AFK: TV - addOffset('Boyfriend DJ watchin tv OG', 0, 0); - } - - var cartoonSnd:Null = null; - - public var playingCartoon:Bool = false; - - public function runTvLogic() - { - if (cartoonSnd == null) - { - // tv is OFF, but getting turned on - FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { - loadCartoon(); - }); - } - else - { - // plays it smidge after the click - FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() { - cartoonSnd.destroy(); - loadCartoon(); - }); - } - - // loadCartoon(); - } - - function loadCartoon() - { - cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() { - anim.play("Boyfriend DJ watchin tv OG", true, false, 60); - }); - - // Fade out music to 40% volume over 1 second. - // This helps make the TV a bit more audible. - FlxG.sound.music.fadeOut(1.0, 0.1); - - // Play the cartoon at a random time between the start and 5 seconds from the end. - cartoonSnd.time = FlxG.random.float(0, Math.max(cartoonSnd.length - (5 * Constants.MS_PER_SEC), 0.0)); - } - - final cartoonList:Array = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/")); - - function getRandomFlashToon():String - { - var randomFile = FlxG.random.getObject(cartoonList); - - // Strip folder prefix - randomFile = randomFile.replace("assets/sounds/", ""); - // Strip file extension - randomFile = randomFile.substring(0, randomFile.length - 4); - - return randomFile; - } - - public function confirm():Void - { - currentState = Confirm; - } - - public function fistPump():Void - { - currentState = PumpIntro; - } - - public function pumpFist():Void - { - currentState = FistPump; - anim.play("Boyfriend DJ fist pump", true, false, 4); - } - - public function pumpFistBad():Void - { - currentState = FistPump; - anim.play("Boyfriend DJ loss reaction 1", true, false, 4); - } - - public inline function addOffset(name:String, x:Float = 0, y:Float = 0) - { - animOffsets[name] = [x, y]; - } - - override public function getCurrentAnimation():String - { - if (this.anim == null || this.anim.curSymbol == null) return ""; - return this.anim.curSymbol.name; - } - - public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void - { - anim.play(id, Force, Reverse, Frame); - applyAnimOffset(); - } - - function applyAnimOffset() - { - var AnimName = getCurrentAnimation(); - var daOffset = animOffsets.get(AnimName); - if (animOffsets.exists(AnimName)) - { - var xValue = daOffset[0]; - var yValue = daOffset[1]; - if (AnimName == "Boyfriend DJ watchin tv OG") - { - xValue += offsetX; - yValue += offsetY; - } - - offset.set(xValue, yValue); - } - else - { - offset.set(0, 0); - } - } - - public override function destroy():Void - { - super.destroy(); - - if (cartoonSnd != null) - { - cartoonSnd.destroy(); - cartoonSnd = null; - } - } -} - -enum DJBoyfriendState -{ - Intro; - Idle; - Confirm; - PumpIntro; - FistPump; - Spook; - TV; -} diff --git a/source/funkin/ui/freeplay/FreeplayDJ.hx b/source/funkin/ui/freeplay/FreeplayDJ.hx new file mode 100644 index 000000000..f9effe793 --- /dev/null +++ b/source/funkin/ui/freeplay/FreeplayDJ.hx @@ -0,0 +1,369 @@ +package funkin.ui.freeplay; + +import flixel.FlxSprite; +import flixel.util.FlxSignal; +import funkin.util.assets.FlxAnimationUtil; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import funkin.audio.FunkinSound; +import flixel.util.FlxTimer; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.data.freeplay.player.PlayerData.PlayerFreeplayDJData; +import funkin.audio.FunkinSound; +import funkin.audio.FlxStreamSound; + +class FreeplayDJ extends FlxAtlasSprite +{ + // Represents the sprite's current status. + // Without state machines I would have driven myself crazy years ago. + public var currentState:DJBoyfriendState = Intro; + + // A callback activated when the intro animation finishes. + public var onIntroDone:FlxSignal = new FlxSignal(); + + // A callback activated when the idle easter egg plays. + public var onIdleEasterEgg:FlxSignal = new FlxSignal(); + + var seenIdleEasterEgg:Bool = false; + + static final IDLE_EGG_PERIOD:Float = 60.0; + static final IDLE_CARTOON_PERIOD:Float = 120.0; + + // Time since last special idle animation you. + var timeIdling:Float = 0; + + final characterId:String = Constants.DEFAULT_CHARACTER; + final playableCharData:PlayerFreeplayDJData; + + public function new(x:Float, y:Float, characterId:String) + { + this.characterId = characterId; + + var playableChar = PlayerRegistry.instance.fetchEntry(characterId); + playableCharData = playableChar.getFreeplayDJData(); + + super(x, y, playableCharData.getAtlasPath()); + + anim.callback = function(name, number) { + if (name == playableCharData.getAnimationPrefix('cartoon')) + { + if (number == playableCharData.getCartoonSoundClickFrame()) + { + FunkinSound.playOnce(Paths.sound('remote_click')); + } + if (number == playableCharData.getCartoonSoundCartoonFrame()) + { + runTvLogic(); + } + } + }; + + FlxG.debugger.track(this); + FlxG.console.registerObject("dj", this); + + anim.onComplete = onFinishAnim; + + FlxG.console.registerFunction("freeplayCartoon", function() { + currentState = Cartoon; + }); + } + + override public function listAnimations():Array + { + var anims:Array = []; + @:privateAccess + for (animKey in anim.symbolDictionary) + { + anims.push(animKey.name); + } + return anims; + } + + var lowPumpLoopPoint:Int = 4; + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + switch (currentState) + { + case Intro: + // Play the intro animation then leave this state immediately. + var animPrefix = playableCharData.getAnimationPrefix('intro'); + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + case Idle: + // We are in this state the majority of the time. + var animPrefix = playableCharData.getAnimationPrefix('idle'); + if (getCurrentAnimation() != animPrefix) + { + playFlashAnimation(animPrefix, true); + } + + if (getCurrentAnimation() == animPrefix && this.isLoopFinished()) + { + if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg) + { + currentState = IdleEasterEgg; + } + else if (timeIdling >= IDLE_CARTOON_PERIOD) + { + currentState = Cartoon; + } + } + timeIdling += elapsed; + case Confirm: + var animPrefix = playableCharData.getAnimationPrefix('confirm'); + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false); + timeIdling = 0; + case FistPumpIntro: + var animPrefix = playableCharData.getAnimationPrefix('fistPump'); + if (getCurrentAnimation() != animPrefix) playFlashAnimation('Boyfriend DJ fist pump', false); + if (getCurrentAnimation() == animPrefix && anim.curFrame >= 4) + { + anim.play("Boyfriend DJ fist pump", true, false, 0); + } + case FistPump: + + case IdleEasterEgg: + var animPrefix = playableCharData.getAnimationPrefix('idleEasterEgg'); + if (getCurrentAnimation() != animPrefix) + { + onIdleEasterEgg.dispatch(); + playFlashAnimation(animPrefix, false); + seenIdleEasterEgg = true; + } + timeIdling = 0; + case Cartoon: + var animPrefix = playableCharData.getAnimationPrefix('cartoon'); + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + default: + // I shit myself. + } + + if (FlxG.keys.pressed.CONTROL) + { + if (FlxG.keys.justPressed.LEFT) + { + this.offsetX -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); + } + + if (FlxG.keys.justPressed.RIGHT) + { + this.offsetX += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); + } + + if (FlxG.keys.justPressed.UP) + { + this.offsetY -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); + } + + if (FlxG.keys.justPressed.DOWN) + { + this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0); + } + + if (FlxG.keys.justPressed.SPACE) + { + currentState = (currentState == Idle ? Cartoon : Idle); + } + } + } + + function onFinishAnim():Void + { + var name = anim.curSymbol.name; + + if (name == playableCharData.getAnimationPrefix('intro')) + { + currentState = Idle; + onIntroDone.dispatch(); + } + else if (name == playableCharData.getAnimationPrefix('idle')) + { + // trace('Finished idle'); + } + else if (name == playableCharData.getAnimationPrefix('confirm')) + { + // trace('Finished confirm'); + } + else if (name == playableCharData.getAnimationPrefix('fistPump')) + { + // trace('Finished fist pump'); + currentState = Idle; + } + else if (name == playableCharData.getAnimationPrefix('idleEasterEgg')) + { + // trace('Finished spook'); + currentState = Idle; + } + else if (name == playableCharData.getAnimationPrefix('loss')) + { + // trace('Finished loss reaction'); + currentState = Idle; + } + else if (name == playableCharData.getAnimationPrefix('cartoon')) + { + // trace('Finished cartoon'); + + var frame:Int = FlxG.random.bool(33) ? playableCharData.getCartoonLoopBlinkFrame() : playableCharData.getCartoonLoopFrame(); + + // Character switches channels when the video ends, or at a 10% chance each time his idle loops. + if (FlxG.random.bool(5)) + { + frame = playableCharData.getCartoonChannelChangeFrame(); + // boyfriend switches channel code? + // runTvLogic(); + } + trace('Replay idle: ${frame}'); + anim.play(playableCharData.getAnimationPrefix('cartoon'), true, false, frame); + // trace('Finished confirm'); + } + else + { + trace('Finished ${name}'); + } + } + + public function resetAFKTimer():Void + { + timeIdling = 0; + seenIdleEasterEgg = false; + } + + var offsetX:Float = 0.0; + var offsetY:Float = 0.0; + + var cartoonSnd:Null = null; + + public var playingCartoon:Bool = false; + + public function runTvLogic() + { + if (cartoonSnd == null) + { + // tv is OFF, but getting turned on + FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { + loadCartoon(); + }); + } + else + { + // plays it smidge after the click + FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() { + cartoonSnd.destroy(); + loadCartoon(); + }); + } + + // loadCartoon(); + } + + function loadCartoon() + { + cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() { + anim.play("Boyfriend DJ watchin tv OG", true, false, 60); + }); + + // Fade out music to 40% volume over 1 second. + // This helps make the TV a bit more audible. + FlxG.sound.music.fadeOut(1.0, 0.1); + + // Play the cartoon at a random time between the start and 5 seconds from the end. + cartoonSnd.time = FlxG.random.float(0, Math.max(cartoonSnd.length - (5 * Constants.MS_PER_SEC), 0.0)); + } + + final cartoonList:Array = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/")); + + function getRandomFlashToon():String + { + var randomFile = FlxG.random.getObject(cartoonList); + + // Strip folder prefix + randomFile = randomFile.replace("assets/sounds/", ""); + // Strip file extension + randomFile = randomFile.substring(0, randomFile.length - 4); + + return randomFile; + } + + public function confirm():Void + { + currentState = Confirm; + } + + public function fistPump():Void + { + currentState = FistPumpIntro; + } + + public function pumpFist():Void + { + currentState = FistPump; + anim.play("Boyfriend DJ fist pump", true, false, 4); + } + + public function pumpFistBad():Void + { + currentState = FistPump; + anim.play("Boyfriend DJ loss reaction 1", true, false, 4); + } + + override public function getCurrentAnimation():String + { + if (this.anim == null || this.anim.curSymbol == null) return ""; + return this.anim.curSymbol.name; + } + + public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void + { + anim.play(id, Force, Reverse, Frame); + applyAnimOffset(); + } + + function applyAnimOffset() + { + var AnimName = getCurrentAnimation(); + var daOffset = playableCharData.getAnimationOffsets(AnimName); + if (daOffset != null) + { + var xValue = daOffset[0]; + var yValue = daOffset[1]; + if (AnimName == "Boyfriend DJ watchin tv OG") + { + xValue += offsetX; + yValue += offsetY; + } + + trace('Successfully applied offset: ' + xValue + ', ' + yValue); + offset.set(xValue, yValue); + } + else + { + trace('No offset found, defaulting to: 0, 0'); + offset.set(0, 0); + } + } + + public override function destroy():Void + { + super.destroy(); + + if (cartoonSnd != null) + { + cartoonSnd.destroy(); + cartoonSnd = null; + } + } +} + +enum DJBoyfriendState +{ + Intro; + Idle; + Confirm; + FistPumpIntro; + FistPump; + IdleEasterEgg; + Cartoon; +} diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 9f23440df..98e48b338 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -167,7 +167,7 @@ class FreeplayState extends MusicBeatSubState var curCapsule:SongMenuItem; var curPlaying:Bool = false; - var dj:DJBoyfriend; + var dj:FreeplayDJ; var ostName:FlxText; var albumRoll:AlbumRoll; @@ -211,6 +211,7 @@ class FreeplayState extends MusicBeatSubState { currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER; currentCharacter = PlayerRegistry.instance.fetchEntry(currentCharacterId); + if (currentCharacter == null) throw 'Could not build Freeplay state for character: $currentCharacterId'; fromResultsParams = params?.fromResults; @@ -450,17 +451,16 @@ class FreeplayState extends MusicBeatSubState add(cardGlow); - dj = new DJBoyfriend(640, 366); - exitMovers.set([dj], - { - x: -dj.width * 1.6, - speed: 0.5 - }); - - // TODO: Replace this. - if (currentCharacterId == 'pico') dj.visible = false; - - add(dj); + if (currentCharacter?.getFreeplayDJData() != null) + { + dj = new FreeplayDJ(640, 366, currentCharacterId); + exitMovers.set([dj], + { + x: -dj.width * 1.6, + speed: 0.5 + }); + add(dj); + } bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); bgDad.shader = new AngleMask(); diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index 743345004..282e35d7a 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -77,6 +77,11 @@ class PlayableCharacter implements IRegistryEntry return false; } + public function getFreeplayDJData():PlayerFreeplayDJData + { + return _data.freeplayDJ; + } + /** * Returns whether this character is unlocked. */ diff --git a/source/funkin/util/tools/MapTools.hx b/source/funkin/util/tools/MapTools.hx index b98cb0adf..807f0aebd 100644 --- a/source/funkin/util/tools/MapTools.hx +++ b/source/funkin/util/tools/MapTools.hx @@ -14,6 +14,7 @@ class MapTools */ public static function size(map:Map):Int { + if (map == null) return 0; return map.keys().array().length; } @@ -22,6 +23,7 @@ class MapTools */ public static function values(map:Map):Array { + if (map == null) return []; return [for (i in map.iterator()) i]; } @@ -30,6 +32,7 @@ class MapTools */ public static function clone(map:Map):Map { + if (map == null) return null; return map.copy(); } @@ -76,6 +79,7 @@ class MapTools */ public static function keyValues(map:Map):Array { + if (map == null) return []; return map.keys().array(); } } From ad57e64994dd8a9f5494d4c6984c9686448ef105 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 20 Jun 2024 16:17:53 -0400 Subject: [PATCH 17/20] Working Pico DJ --- source/funkin/Paths.hx | 11 +- .../funkin/data/freeplay/player/PlayerData.hx | 42 ++- source/funkin/play/scoring/Scoring.hx | 4 +- source/funkin/ui/freeplay/FreeplayDJ.hx | 14 +- source/funkin/ui/freeplay/FreeplayState.hx | 280 ++++++++++-------- .../freeplay/charselect/PlayableCharacter.hx | 5 + source/funkin/ui/mainmenu/MainMenuState.hx | 5 +- source/funkin/util/SortUtil.hx | 2 +- 8 files changed, 217 insertions(+), 146 deletions(-) diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index b0a97c4fa..285af7ca2 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -11,9 +11,16 @@ class Paths { static var currentLevel:Null = null; - public static function setCurrentLevel(name:String):Void + public static function setCurrentLevel(name:Null):Void { - currentLevel = name.toLowerCase(); + if (name == null) + { + currentLevel = null; + } + else + { + currentLevel = name.toLowerCase(); + } } public static function stripLibrary(path:String):String diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx index 10fc54b78..c461c9555 100644 --- a/source/funkin/data/freeplay/player/PlayerData.hx +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -35,6 +35,7 @@ class PlayerData * Data for displaying this character in the Freeplay menu. * If null, display no DJ. */ + @:optional public var freeplayDJ:Null = null; /** @@ -73,9 +74,25 @@ class PlayerFreeplayDJData var assetPath:String; var animations:Array; + @:optional + @:default("BOYFRIEND") + var text1:String; + + @:optional + @:default("HOT BLOODED IN MORE WAYS THAN ONE") + var text2:String; + + @:optional + @:default("PROTECT YO NUTS") + var text3:String; + + @:jignored var animationMap:Map; + @:jignored + var prefixToOffsetsMap:Map>; + @:optional var cartoon:Null; @@ -87,11 +104,14 @@ class PlayerFreeplayDJData function mapAnimations() { if (animationMap == null) animationMap = new Map(); + if (prefixToOffsetsMap == null) prefixToOffsetsMap = new Map(); animationMap.clear(); + prefixToOffsetsMap.clear(); for (anim in animations) { animationMap.set(anim.name, anim); + prefixToOffsetsMap.set(anim.prefix, anim.offsets); } } @@ -100,6 +120,15 @@ class PlayerFreeplayDJData return Paths.animateAtlas(assetPath); } + public function getFreeplayDJText(index:Int):String { + switch (index) { + case 1: return text1; + case 2: return text2; + case 3: return text3; + default: return ''; + } + } + public function getAnimationPrefix(name:String):Null { if (animationMap.size() == 0) mapAnimations(); @@ -109,13 +138,16 @@ class PlayerFreeplayDJData return anim.prefix; } - public function getAnimationOffsets(name:String):Null> + public function getAnimationOffsetsByPrefix(?prefix:String):Array { - if (animationMap.size() == 0) mapAnimations(); + if (prefixToOffsetsMap.size() == 0) mapAnimations(); + if (prefix == null) return [0, 0]; + return prefixToOffsetsMap.get(prefix); + } - var anim = animationMap.get(name); - if (anim == null) return null; - return anim.offsets; + public function getAnimationOffsets(name:String):Array + { + return getAnimationOffsetsByPrefix(getAnimationPrefix(name)); } // TODO: These should really be frame labels, ehe. diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index dc2c40647..02e5750bc 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -590,7 +590,7 @@ enum abstract ScoringRank(String) } } - public function getFreeplayRankIconAsset():Null + public function getFreeplayRankIconAsset():String { switch (abstract) { @@ -607,7 +607,7 @@ enum abstract ScoringRank(String) case SHIT: return 'LOSS'; default: - return null; + return 'LOSS'; } } diff --git a/source/funkin/ui/freeplay/FreeplayDJ.hx b/source/funkin/ui/freeplay/FreeplayDJ.hx index f9effe793..72eddd0ca 100644 --- a/source/funkin/ui/freeplay/FreeplayDJ.hx +++ b/source/funkin/ui/freeplay/FreeplayDJ.hx @@ -135,8 +135,12 @@ class FreeplayDJ extends FlxAtlasSprite timeIdling = 0; case Cartoon: var animPrefix = playableCharData.getAnimationPrefix('cartoon'); - if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); - timeIdling = 0; + if (animPrefix == null) { + currentState = IdleEasterEgg; + } else { + if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true); + timeIdling = 0; + } default: // I shit myself. } @@ -324,7 +328,7 @@ class FreeplayDJ extends FlxAtlasSprite function applyAnimOffset() { var AnimName = getCurrentAnimation(); - var daOffset = playableCharData.getAnimationOffsets(AnimName); + var daOffset = playableCharData.getAnimationOffsetsByPrefix(AnimName); if (daOffset != null) { var xValue = daOffset[0]; @@ -335,12 +339,12 @@ class FreeplayDJ extends FlxAtlasSprite yValue += offsetY; } - trace('Successfully applied offset: ' + xValue + ', ' + yValue); + trace('Successfully applied offset ($AnimName): ' + xValue + ', ' + yValue); offset.set(xValue, yValue); } else { - trace('No offset found, defaulting to: 0, 0'); + trace('No offset found ($AnimName), defaulting to: 0, 0'); offset.set(0, 0); } } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 98e48b338..5725101cd 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1,54 +1,55 @@ package funkin.ui.freeplay; -import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.addons.transition.FlxTransitionableState; import flixel.addons.ui.FlxInputText; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.group.FlxGroup; -import funkin.graphics.shaders.GaussianBlurShader; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.input.touch.FlxTouch; import flixel.math.FlxAngle; import flixel.math.FlxPoint; -import openfl.display.BlendMode; import flixel.system.debug.watch.Tracker.TrackerProfile; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; +import flixel.tweens.misc.ShakeTween; import flixel.util.FlxColor; import flixel.util.FlxSpriteUtil; import flixel.util.FlxTimer; import funkin.audio.FunkinSound; -import funkin.data.story.level.LevelRegistry; -import funkin.data.song.SongRegistry; import funkin.data.freeplay.player.PlayerRegistry; +import funkin.data.song.SongRegistry; +import funkin.data.story.level.LevelRegistry; +import funkin.effects.IntervalShake; +import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinSprite; import funkin.graphics.shaders.AngleMask; +import funkin.graphics.shaders.GaussianBlurShader; import funkin.graphics.shaders.HSVShader; import funkin.graphics.shaders.PureColor; import funkin.graphics.shaders.StrokeShader; import funkin.input.Controls; import funkin.play.PlayStatePlaylist; +import funkin.play.scoring.Scoring; +import funkin.play.scoring.Scoring.ScoringRank; import funkin.play.song.Song; -import funkin.ui.story.Level; import funkin.save.Save; import funkin.save.Save.SaveScoreData; import funkin.ui.AtlasText; -import funkin.play.scoring.Scoring; -import funkin.play.scoring.Scoring.ScoringRank; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.ui.freeplay.SongMenuItem.FreeplayRank; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatSubState; +import funkin.ui.story.Level; import funkin.ui.transition.LoadingState; import funkin.ui.transition.StickerSubState; import funkin.util.MathUtil; +import funkin.util.SortUtil; import lime.utils.Assets; -import flixel.tweens.misc.ShakeTween; -import funkin.effects.IntervalShake; -import funkin.ui.freeplay.SongMenuItem.FreeplayRank; -import funkin.ui.freeplay.charselect.PlayableCharacter; +import openfl.display.BlendMode; /** * Parameters used to initialize the FreeplayState. @@ -94,6 +95,7 @@ typedef FromResultsParams = /** * The state for the freeplay menu, allowing the player to select any song to play. */ +@:nullSafety class FreeplayState extends MusicBeatSubState { // @@ -164,10 +166,9 @@ class FreeplayState extends MusicBeatSubState var grpSongs:FlxTypedGroup; var grpCapsules:FlxTypedGroup; - var curCapsule:SongMenuItem; var curPlaying:Bool = false; - var dj:FreeplayDJ; + var dj:Null = null; var ostName:FlxText; var albumRoll:AlbumRoll; @@ -175,7 +176,7 @@ class FreeplayState extends MusicBeatSubState var letterSort:LetterSort; var exitMovers:ExitMoverData = new Map(); - var stickerSubState:StickerSubState; + var stickerSubState:Null = null; public static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; public static var rememberedSongId:Null = 'tutorial'; @@ -210,8 +211,12 @@ class FreeplayState extends MusicBeatSubState public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER; - currentCharacter = PlayerRegistry.instance.fetchEntry(currentCharacterId); - if (currentCharacter == null) throw 'Could not build Freeplay state for character: $currentCharacterId'; + var fetchPlayableCharacter = function():PlayableCharacter { + var result = PlayerRegistry.instance.fetchEntry(params?.character ?? Constants.DEFAULT_CHARACTER); + if (result == null) throw 'No valid playable character with id ${params?.character}'; + return result; + }; + currentCharacter = fetchPlayableCharacter(); fromResultsParams = params?.fromResults; @@ -220,12 +225,54 @@ class FreeplayState extends MusicBeatSubState prepForNewRank = true; } + super(FlxColor.TRANSPARENT); + if (stickers?.members != null) { stickerSubState = stickers; } - super(FlxColor.TRANSPARENT); + // We build a bunch of sprites BEFORE create() so we can guarantee they aren't null later on. + albumRoll = new AlbumRoll(); + fp = new FreeplayScore(460, 60, 7, 100); + cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow')); + confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow')); + confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText')); + rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height); + funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); + funnyScroll = new BGScrollingText(0, 220, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60); + funnyScroll2 = new BGScrollingText(0, 335, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60); + grpCapsules = new FlxTypedGroup(); + grpDifficulties = new FlxTypedSpriteGroup(-300, 80); + letterSort = new LetterSort(400, 75); + grpSongs = new FlxTypedGroup(); + moreWays = new BGScrollingText(0, 160, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43); + moreWays2 = new BGScrollingText(0, 397, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43); + pinkBack = FunkinSprite.create('freeplay/pinkBack'); + rankBg = new FunkinSprite(0, 0); + rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette')); + sparks = new FlxSprite(0, 0); + sparksADD = new FlxSprite(0, 0); + txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR); + txtNuts = new BGScrollingText(0, 285, currentCharacter.getFreeplayDJText(3), FlxG.width / 2, true, 43); + + ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48); + + orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); + + bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); + alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); + confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2')); + funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, 60); + backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"), + { + FrameRate: 24.0, + Reversed: false, + // ?OnComplete:Void -> Void, + ShowPivot: false, + Antialiasing: true, + ScrollFactor: new FlxPoint(1, 1), + }); } override function create():Void @@ -236,12 +283,6 @@ class FreeplayState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; - // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere - funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); - funnyCam.bgColor = FlxColor.TRANSPARENT; - FlxG.cameras.add(funnyCam, false); - this.cameras = [funnyCam]; - if (stickerSubState != null) { this.persistentUpdate = true; @@ -277,7 +318,7 @@ class FreeplayState extends MusicBeatSubState // programmatically adds the songs via LevelRegistry and SongRegistry for (levelId in LevelRegistry.instance.listSortedLevelIds()) { - var level:Level = LevelRegistry.instance.fetchEntry(levelId); + var level:Null = LevelRegistry.instance.fetchEntry(levelId); if (level == null) { @@ -287,7 +328,7 @@ class FreeplayState extends MusicBeatSubState for (songId in level.getSongs()) { - var song:Song = SongRegistry.instance.fetchEntry(songId); + var song:Null = SongRegistry.instance.fetchEntry(songId); if (song == null) { @@ -319,17 +360,14 @@ class FreeplayState extends MusicBeatSubState trace(FlxG.camera.initialZoom); trace(FlxCamera.defaultZoom); - pinkBack = FunkinSprite.create('freeplay/pinkBack'); pinkBack.color = 0xFFFFD4E9; // sets it to pink! pinkBack.x -= pinkBack.width; FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut}); add(pinkBack); - orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00); add(orangeBackShit); - alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400); add(alsoOrangeLOL); exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL], @@ -344,15 +382,11 @@ class FreeplayState extends MusicBeatSubState orangeBackShit.visible = false; alsoOrangeLOL.visible = false; - confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText')); confirmTextGlow.blend = BlendMode.ADD; confirmTextGlow.visible = false; - confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow')); confirmGlow.blend = BlendMode.ADD; - confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2')); - confirmGlow.visible = false; confirmGlow2.visible = false; @@ -367,7 +401,6 @@ class FreeplayState extends MusicBeatSubState FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ['x', 'y', 'speed', 'size'])); - moreWays = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43); moreWays.funnyColor = 0xFFFFF383; moreWays.speed = 6.8; grpTxtScrolls.add(moreWays); @@ -378,7 +411,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.4, }); - funnyScroll = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60); funnyScroll.funnyColor = 0xFFFF9963; funnyScroll.speed = -3.8; grpTxtScrolls.add(funnyScroll); @@ -391,7 +423,6 @@ class FreeplayState extends MusicBeatSubState wait: 0 }); - txtNuts = new BGScrollingText(0, 285, 'PROTECT YO NUTS', FlxG.width / 2, true, 43); txtNuts.speed = 3.5; grpTxtScrolls.add(txtNuts); exitMovers.set([txtNuts], @@ -400,7 +431,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.4, }); - funnyScroll2 = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60); funnyScroll2.funnyColor = 0xFFFF9963; funnyScroll2.speed = -3.8; grpTxtScrolls.add(funnyScroll2); @@ -411,7 +441,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.5, }); - moreWays2 = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43); moreWays2.funnyColor = 0xFFFFF383; moreWays2.speed = 6.8; grpTxtScrolls.add(moreWays2); @@ -422,7 +451,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.4 }); - funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60); funnyScroll3.funnyColor = 0xFFFEA400; funnyScroll3.speed = -3.8; grpTxtScrolls.add(funnyScroll3); @@ -433,19 +461,8 @@ class FreeplayState extends MusicBeatSubState speed: 0.3 }); - backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"), - { - FrameRate: 24.0, - Reversed: false, - // ?OnComplete:Void -> Void, - ShowPivot: false, - Antialiasing: true, - ScrollFactor: new FlxPoint(1, 1), - }); - add(backingTextYeah); - cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow')); cardGlow.blend = BlendMode.ADD; cardGlow.visible = false; @@ -462,7 +479,6 @@ class FreeplayState extends MusicBeatSubState add(dj); } - bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); bgDad.shader = new AngleMask(); bgDad.visible = false; @@ -488,17 +504,13 @@ class FreeplayState extends MusicBeatSubState blackOverlayBullshitLOLXD.shader = bgDad.shader; - rankBg = new FunkinSprite(0, 0); rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xD3000000); add(rankBg); - grpSongs = new FlxTypedGroup(); add(grpSongs); - grpCapsules = new FlxTypedGroup(); add(grpCapsules); - grpDifficulties = new FlxTypedSpriteGroup(-300, 80); add(grpDifficulties); exitMovers.set([grpDifficulties], @@ -525,7 +537,6 @@ class FreeplayState extends MusicBeatSubState if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true; } - albumRoll = new AlbumRoll(); albumRoll.albumId = null; add(albumRoll); @@ -540,7 +551,6 @@ class FreeplayState extends MusicBeatSubState fnfFreeplay.font = 'VCR OSD Mono'; fnfFreeplay.visible = false; - ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48); ostName.font = 'VCR OSD Mono'; ostName.alignment = RIGHT; ostName.visible = false; @@ -572,7 +582,6 @@ class FreeplayState extends MusicBeatSubState tmr.time = FlxG.random.float(20, 60); }, 0); - fp = new FreeplayScore(460, 60, 7, 100); fp.visible = false; add(fp); @@ -580,11 +589,9 @@ class FreeplayState extends MusicBeatSubState clearBoxSprite.visible = false; add(clearBoxSprite); - txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR); txtCompletion.visible = false; add(txtCompletion); - letterSort = new LetterSort(400, 75); add(letterSort); letterSort.visible = false; @@ -632,7 +639,7 @@ class FreeplayState extends MusicBeatSubState // be careful not to "add()" things in here unless it's to a group that's already added to the state // otherwise it won't be properly attatched to funnyCamera (relavent code should be at the bottom of create()) - dj.onIntroDone.add(function() { + var onDJIntroDone = function() { // when boyfriend hits dat shiii albumRoll.playIntro(); @@ -679,20 +686,27 @@ class FreeplayState extends MusicBeatSubState cardGlow.visible = true; FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut}); - if (prepForNewRank) + if (prepForNewRank && fromResultsParams != null) { rankAnimStart(fromResultsParams); } - }); + }; + + if (dj != null) + { + dj.onIntroDone.add(onDJIntroDone); + } + else + { + onDJIntroDone(); + } generateSongList(null, false); // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere - funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height); funnyCam.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(funnyCam, false); - rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette')); rankVignette.scale.set(2, 2); rankVignette.updateHitbox(); rankVignette.blend = BlendMode.ADD; @@ -704,7 +718,6 @@ class FreeplayState extends MusicBeatSubState bs.cameras = [funnyCam]; }); - rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height); rankCamera.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(rankCamera, false); rankBg.cameras = [rankCamera]; @@ -716,8 +729,8 @@ class FreeplayState extends MusicBeatSubState } } - var currentFilter:SongFilter = null; - var currentFilteredSongs:Array = []; + var currentFilter:Null = null; + var currentFilteredSongs:Array> = []; /** * Given the current filter, rebuild the current song list. @@ -728,7 +741,7 @@ class FreeplayState extends MusicBeatSubState */ public function generateSongList(filterStuff:Null, force:Bool = false, onlyIfChanged:Bool = true):Void { - var tempSongs:Array = songs; + var tempSongs:Array> = songs; // Remember just the difficulty because it's important for song sorting. if (rememberedDifficulty != null) @@ -790,11 +803,12 @@ class FreeplayState extends MusicBeatSubState for (i in 0...tempSongs.length) { - if (tempSongs[i] == null) continue; + var tempSong = tempSongs[i]; + if (tempSong == null) continue; var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem); - funnyMenu.init(FlxG.width, 0, tempSongs[i]); + funnyMenu.init(FlxG.width, 0, tempSong); funnyMenu.onConfirm = function() { capsuleOnConfirmDefault(funnyMenu); }; @@ -803,8 +817,8 @@ class FreeplayState extends MusicBeatSubState funnyMenu.ID = i; funnyMenu.capsule.alpha = 0.5; funnyMenu.songText.visible = false; - funnyMenu.favIcon.visible = tempSongs[i].isFav; - funnyMenu.favIconBlurred.visible = tempSongs[i].isFav; + funnyMenu.favIcon.visible = tempSong.isFav; + funnyMenu.favIconBlurred.visible = tempSong.isFav; funnyMenu.hsvShader = hsvShader; funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45); @@ -828,13 +842,10 @@ class FreeplayState extends MusicBeatSubState * @param songFilter The filter to apply * @return Array */ - public function sortSongs(songsToFilter:Array, songFilter:SongFilter):Array + public function sortSongs(songsToFilter:Array>, songFilter:SongFilter):Array> { - var filterAlphabetically = function(a:FreeplaySongData, b:FreeplaySongData):Int { - if (a?.songName.toLowerCase() < b?.songName.toLowerCase()) return -1; - else if (a?.songName.toLowerCase() > b?.songName.toLowerCase()) return 1; - else - return 0; + var filterAlphabetically = function(a:Null, b:Null):Int { + return SortUtil.alphabetically(a?.songName ?? '', b?.songName ?? ''); }; switch (songFilter.filterType) @@ -858,7 +869,7 @@ class FreeplayState extends MusicBeatSubState songsToFilter = songsToFilter.filter(str -> { if (str == null) return true; // Random - return str.songName.toLowerCase().startsWith(songFilter.filterData); + return str.songName.toLowerCase().startsWith(songFilter.filterData ?? ''); }); case ALL: // no filter! @@ -880,32 +891,28 @@ class FreeplayState extends MusicBeatSubState var sparks:FlxSprite; var sparksADD:FlxSprite; - function rankAnimStart(fromResults:Null):Void + function rankAnimStart(fromResults:FromResultsParams):Void { busy = true; grpCapsules.members[curSelected].sparkle.alpha = 0; // grpCapsules.members[curSelected].forcePosition(); - if (fromResults != null) - { - rememberedSongId = fromResults.songId; - rememberedDifficulty = fromResults.difficultyId; - changeSelection(); - changeDiff(); - } + rememberedSongId = fromResults.songId; + rememberedDifficulty = fromResults.difficultyId; + changeSelection(); + changeDiff(); - dj.fistPump(); + if (dj != null) dj.fistPump(); // rankCamera.fade(FlxColor.BLACK, 0.5, true); rankCamera.fade(0xFF000000, 0.5, true, null, true); if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; rankBg.alpha = 1; - if (fromResults?.oldRank != null) + if (fromResults.oldRank != null) { grpCapsules.members[curSelected].fakeRanking.rank = fromResults.oldRank; grpCapsules.members[curSelected].fakeBlurredRanking.rank = fromResults.oldRank; - sparks = new FlxSprite(0, 0); sparks.frames = Paths.getSparrowAtlas('freeplay/sparks'); sparks.animation.addByPrefix('sparks', 'sparks', 24, false); sparks.visible = false; @@ -915,7 +922,6 @@ class FreeplayState extends MusicBeatSubState add(sparks); sparks.cameras = [rankCamera]; - sparksADD = new FlxSprite(0, 0); sparksADD.visible = false; sparksADD.frames = Paths.getSparrowAtlas('freeplay/sparksadd'); sparksADD.animation.addByPrefix('sparks add', 'sparks add', 24, false); @@ -980,14 +986,14 @@ class FreeplayState extends MusicBeatSubState grpCapsules.members[curSelected].ranking.scale.set(20, 20); grpCapsules.members[curSelected].blurredRanking.scale.set(20, 20); - if (fromResults?.newRank != null) + if (fromResults != null && fromResults.newRank != null) { grpCapsules.members[curSelected].ranking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true); } FlxTween.tween(grpCapsules.members[curSelected].ranking, {"scale.x": 1, "scale.y": 1}, 0.1); - if (fromResults?.newRank != null) + if (fromResults != null && fromResults.newRank != null) { grpCapsules.members[curSelected].blurredRanking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true); } @@ -1078,11 +1084,11 @@ class FreeplayState extends MusicBeatSubState if (fromResultsParams?.newRank == SHIT) { - dj.pumpFistBad(); + if (dj != null) dj.pumpFistBad(); } else { - dj.pumpFist(); + if (dj != null) dj.pumpFist(); } rankCamera.zoom = 0.8; @@ -1196,7 +1202,13 @@ class FreeplayState extends MusicBeatSubState #if debug if (FlxG.keys.justPressed.T) { - rankAnimStart(fromResultsParams); + rankAnimStart(fromResultsParams ?? + { + playRankAnim: true, + newRank: PERFECT_GOLD, + songId: "tutorial", + difficultyId: "hard" + }); } if (FlxG.keys.justPressed.P) @@ -1427,7 +1439,7 @@ class FreeplayState extends MusicBeatSubState } spamTimer += elapsed; - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); } else { @@ -1438,31 +1450,31 @@ class FreeplayState extends MusicBeatSubState #if !html5 if (FlxG.mouse.wheel != 0) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeSelection(-Math.round(FlxG.mouse.wheel)); } #else if (FlxG.mouse.wheel < 0) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeSelection(-Math.round(FlxG.mouse.wheel / 8)); } else if (FlxG.mouse.wheel > 0) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeSelection(-Math.round(FlxG.mouse.wheel / 8)); } #end if (controls.UI_LEFT_P) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeDiff(-1); generateSongList(currentFilter, true); } if (controls.UI_RIGHT_P) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeDiff(1); generateSongList(currentFilter, true); } @@ -1472,7 +1484,7 @@ class FreeplayState extends MusicBeatSubState busy = true; FlxTween.globalManager.clear(); FlxTimer.globalManager.clear(); - dj.onIntroDone.removeAll(); + if (dj != null) dj.onIntroDone.removeAll(); FunkinSound.playOnce(Paths.sound('cancelMenu')); @@ -1498,7 +1510,8 @@ class FreeplayState extends MusicBeatSubState for (grpSpr in exitMovers.keys()) { - var moveData:MoveData = exitMovers.get(grpSpr); + var moveData:Null = exitMovers.get(grpSpr); + if (moveData == null) continue; for (spr in grpSpr) { @@ -1506,14 +1519,14 @@ class FreeplayState extends MusicBeatSubState var funnyMoveShit:MoveData = moveData; - if (moveData.x == null) funnyMoveShit.x = spr.x; - if (moveData.y == null) funnyMoveShit.y = spr.y; - if (moveData.speed == null) funnyMoveShit.speed = 0.2; - if (moveData.wait == null) funnyMoveShit.wait = 0; + var moveDataX = funnyMoveShit.x ?? spr.x; + var moveDataY = funnyMoveShit.y ?? spr.y; + var moveDataSpeed = funnyMoveShit.speed ?? 0.2; + var moveDataWait = funnyMoveShit.wait ?? 0; - FlxTween.tween(spr, {x: funnyMoveShit.x, y: funnyMoveShit.y}, funnyMoveShit.speed, {ease: FlxEase.expoIn}); + FlxTween.tween(spr, {x: moveDataX, y: moveDataY}, moveDataSpeed, {ease: FlxEase.expoIn}); - longestTimer = Math.max(longestTimer, funnyMoveShit.speed + funnyMoveShit.wait); + longestTimer = Math.max(longestTimer, moveDataSpeed + moveDataWait); } } @@ -1586,19 +1599,18 @@ class FreeplayState extends MusicBeatSubState var daSong:Null = grpCapsules.members[curSelected].songData; if (daSong != null) { - // TODO: Make this actually be the variation you're focused on. We don't need to fetch the song metadata just to calculate it. - var targetSong:Song = SongRegistry.instance.fetchEntry(grpCapsules.members[curSelected].songData.songId); + var targetSong:Null = SongRegistry.instance.fetchEntry(daSong.songId); if (targetSong == null) { - FlxG.log.warn('WARN: could not find song with id (${grpCapsules.members[curSelected].songData.songId})'); + FlxG.log.warn('WARN: could not find song with id (${daSong.songId})'); return; } - var targetVariation:String = targetSong.getFirstValidVariation(currentDifficulty); + var targetVariation:String = targetSong.getFirstValidVariation(currentDifficulty) ?? ''; // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. var suffixedDifficulty = (targetVariation != Constants.DEFAULT_VARIATION && targetVariation != 'erect') ? '$currentDifficulty-${targetVariation}' : currentDifficulty; - var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, suffixedDifficulty); + var songScore:Null = Save.instance.getSongScore(daSong.songId, suffixedDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); rememberedDifficulty = currentDifficulty; @@ -1660,7 +1672,7 @@ class FreeplayState extends MusicBeatSubState } // Set the album graphic and play the animation if relevant. - var newAlbumId:String = daSong?.albumId; + var newAlbumId:Null = daSong?.albumId; if (albumRoll.albumId != newAlbumId) { albumRoll.albumId = newAlbumId; @@ -1698,7 +1710,7 @@ class FreeplayState extends MusicBeatSubState }); trace('Available songs: ${availableSongCapsules.map(function(cap) { - return cap.songData.songName; + return cap?.songData?.songName; })}'); if (availableSongCapsules.length == 0) @@ -1727,17 +1739,20 @@ class FreeplayState extends MusicBeatSubState PlayStatePlaylist.isStoryMode = false; - var targetSong:Song = SongRegistry.instance.fetchEntry(cap.songData.songId); - if (targetSong == null) + var targetSongId:String = cap?.songData?.songId ?? 'unknown'; + var targetSongNullable:Null = SongRegistry.instance.fetchEntry(targetSongId); + if (targetSongNullable == null) { - FlxG.log.warn('WARN: could not find song with id (${cap.songData.songId})'); + FlxG.log.warn('WARN: could not find song with id (${targetSongId})'); return; } + var targetSong:Song = targetSongNullable; var targetDifficultyId:String = currentDifficulty; - var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter); - PlayStatePlaylist.campaignId = cap.songData.levelId; + var targetVariation:Null = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter); + var targetLevelId:Null = cap?.songData?.levelId; + PlayStatePlaylist.campaignId = targetLevelId ?? null; - var targetDifficulty:SongDifficulty = targetSong.getDifficulty(targetDifficultyId, targetVariation); + var targetDifficulty:Null = targetSong.getDifficulty(targetDifficultyId, targetVariation); if (targetDifficulty == null) { FlxG.log.warn('WARN: could not find difficulty with id (${targetDifficultyId})'); @@ -1759,7 +1774,7 @@ class FreeplayState extends MusicBeatSubState // Visual and audio effects. FunkinSound.playOnce(Paths.sound('confirmMenu')); - dj.confirm(); + if (dj != null) dj.confirm(); grpCapsules.members[curSelected].forcePosition(); grpCapsules.members[curSelected].confirm(); @@ -1801,7 +1816,7 @@ class FreeplayState extends MusicBeatSubState new FlxTimer().start(1, function(tmr:FlxTimer) { FunkinSound.emptyPartialQueue(); - Paths.setCurrentLevel(cap.songData.levelId); + Paths.setCurrentLevel(cap?.songData?.levelId); LoadingState.loadPlayState( { targetSong: targetSong, @@ -1856,7 +1871,7 @@ class FreeplayState extends MusicBeatSubState var daSongCapsule:SongMenuItem = grpCapsules.members[curSelected]; if (daSongCapsule.songData != null) { - var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty); + var songScore:Null = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); diffIdsCurrent = daSongCapsule.songData.songDifficulties; @@ -1906,7 +1921,10 @@ class FreeplayState extends MusicBeatSubState } else { - var previewSong:Null = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId); + var previewSongId:Null = daSongCapsule?.songData?.songId; + if (previewSongId == null) return; + + var previewSong:Null = SongRegistry.instance.fetchEntry(previewSongId); var songDifficulty = previewSong?.getDifficulty(currentDifficulty, previewSong?.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST); var baseInstrumentalId:String = songDifficulty?.characters?.instrumental ?? ''; @@ -1924,7 +1942,9 @@ class FreeplayState extends MusicBeatSubState instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; - FunkinSound.playMusic(daSongCapsule.songData.songId, + trace('Attempting to play partial preview: ${previewSongId}:${instSuffix}'); + + FunkinSound.playMusic(previewSongId, { startingVolume: 0.0, overrideExisting: true, @@ -1952,7 +1972,7 @@ class FreeplayState extends MusicBeatSubState public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState { var result:MainMenuState; - if (params?.fromResults?.playRankAnim) result = new MainMenuState(true); + if (params?.fromResults?.playRankAnim ?? false) result = new MainMenuState(true); else result = new MainMenuState(false); diff --git a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx index 282e35d7a..6d7b96c58 100644 --- a/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -82,6 +82,11 @@ class PlayableCharacter implements IRegistryEntry return _data.freeplayDJ; } + public function getFreeplayDJText(index:Int):String + { + return _data.freeplayDJ.getFreeplayDJText(index); + } + /** * Returns whether this character is unlocked. */ diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index 2eba406d9..9bf465484 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -117,7 +117,10 @@ class MainMenuState extends MusicBeatState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - openSubState(new FreeplayState()); + openSubState(new FreeplayState( + { + character: FlxG.keys.pressed.SHIFT ? 'pico' : 'bf', + })); }); #if CAN_OPEN_LINKS diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx index c5ac175be..f6d3721f0 100644 --- a/source/funkin/util/SortUtil.hx +++ b/source/funkin/util/SortUtil.hx @@ -97,7 +97,7 @@ class SortUtil * @param b The second string to compare. * @return 1 if `a` comes before `b`, -1 if `b` comes before `a`, 0 if they are equal */ - public static function alphabetically(a:String, b:String):Int + public static function alphabetically(?a:String, ?b:String):Int { a = a.toUpperCase(); b = b.toUpperCase(); From bd17d965f57ef1a62638def0f0f5ff35e14613a2 Mon Sep 17 00:00:00 2001 From: AppleHair <95587502+AppleHair@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:53:33 +0300 Subject: [PATCH 18/20] [BUGFIX] Fixed Ranks not appearing in freeplay for custom variations Freeplay tries to access a song's rank using the current difficulty name alone, but custom variation ranks are being saved with a variation prefix. This PR makes freeplay look for the variation prefix when necessary. --- source/funkin/ui/freeplay/FreeplayState.hx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 5725101cd..e36b6942f 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -2158,8 +2158,13 @@ class FreeplaySongData { this.albumId = songDifficulty.album; } + + // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. + // `easy`, `erect`, `normal-pico`, etc. + var suffixedDifficulty = (songDifficulty.variation != Constants.DEFAULT_VARIATION + && songDifficulty.variation != 'erect') ? '$currentDifficulty-${songDifficulty.variation}' : currentDifficulty; - this.scoringRank = Save.instance.getSongRank(songId, currentDifficulty); + this.scoringRank = Save.instance.getSongRank(songId, suffixedDifficulty); this.isNew = song.isSongNew(currentDifficulty); } From a7cfae85453adb0372141f211a5a9c85e8071e57 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Thu, 4 Jul 2024 14:48:24 -0400 Subject: [PATCH 19/20] mer --- .gitignore | 1 + .gitmodules | 4 +- assets | 2 +- hmm.json | 18 +- source/funkin/audio/FunkinSound.hx | 4 +- source/funkin/effects/RetroCameraFade.hx | 106 +++++ source/funkin/graphics/FunkinSprite.hx | 101 +++++ source/funkin/input/Controls.hx | 370 +++++++++++------- source/funkin/play/Countdown.hx | 19 +- source/funkin/play/GameOverSubState.hx | 26 +- source/funkin/play/ResultState.hx | 4 +- source/funkin/play/character/CharacterData.hx | 4 +- .../play/character/MultiSparrowCharacter.hx | 2 + .../funkin/play/character/PackerCharacter.hx | 2 + .../funkin/play/character/SparrowCharacter.hx | 2 + source/funkin/play/components/PopUpStuff.hx | 30 +- source/funkin/play/stage/Bopper.hx | 2 - source/funkin/play/stage/Stage.hx | 4 + source/funkin/ui/MenuItem.hx | 23 +- source/funkin/ui/freeplay/FreeplayState.hx | 17 +- source/funkin/ui/freeplay/SongMenuItem.hx | 46 ++- source/funkin/ui/mainmenu/MainMenuState.hx | 10 +- source/funkin/ui/story/LevelTitle.hx | 24 +- source/funkin/ui/story/StoryMenuState.hx | 2 +- source/funkin/ui/transition/LoadingState.hx | 2 +- source/funkin/util/logging/AnsiTrace.hx | 2 +- 26 files changed, 602 insertions(+), 225 deletions(-) create mode 100644 source/funkin/effects/RetroCameraFade.hx diff --git a/.gitignore b/.gitignore index 84585eee0..143cf08c9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ shitAudio/ node_modules/ package.json package-lock.json +.aider.* diff --git a/.gitmodules b/.gitmodules index be5e0aaa8..452c0089b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "assets"] path = assets - url = https://github.com/FunkinCrew/funkin.assets + url = https://github.com/FunkinCrew/Funkin-Assets-secret [submodule "art"] path = art - url = https://github.com/FunkinCrew/funkin.art + url = https://github.com/FunkinCrew/Funkin-Art-secret diff --git a/assets b/assets index 8dd51cde0..d7ecd602d 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8dd51cde0b9a3730abe9f97d0f50365c396ca784 +Subproject commit d7ecd602df733f0625763a2d7b6056f52147b9e6 diff --git a/hmm.json b/hmm.json index 68e0c5cb0..8eaf24212 100644 --- a/hmm.json +++ b/hmm.json @@ -5,7 +5,7 @@ "type": "git", "dir": null, "ref": "2d83fa863ef0c1eace5f1cf67c3ac315d1a3a8a5", - "url": "https://github.com/Aidan63/linc_discord-rpc" + "url": "https://github.com/FunkinCrew/linc_discord-rpc" }, { "name": "flixel", @@ -44,7 +44,7 @@ "name": "FlxPartialSound", "type": "git", "dir": null, - "ref": "f986332ba5ab02abd386ce662578baf04904604a", + "ref": "a1eab7b9bf507b87200a3341719054fe427f3b15", "url": "https://github.com/FunkinCrew/FlxPartialSound.git" }, { @@ -75,14 +75,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "0212d8fdfcafeb5f0d5a41e1ddba8ff21d0e183b", + "ref": "5dc4c933bdc029f6139a47962e3b8c754060f210", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "63a906a6148958dbfde8c7b48d90b0693767fd95", + "ref": "57c1604d6b5174839d7e0e012a4dd5dcbfc129da", "url": "https://github.com/haxeui/haxeui-flixel" }, { @@ -112,7 +112,7 @@ { "name": "hxp", "type": "haxelib", - "version": "1.2.2" + "version": "1.3.0" }, { "name": "json2object", @@ -174,15 +174,15 @@ "name": "thx.core", "type": "git", "dir": null, - "ref": "22605ff44f01971d599641790d6bae4869f7d9f4", - "url": "https://github.com/FunkinCrew/thx.core" + "ref": "6240b6e136f7490d9298edbe8c1891374bd7cdf2", + "url": "https://github.com/fponticelli/thx.core" }, { "name": "thx.semver", "type": "git", "dir": null, - "ref": "cf8d213589a2c7ce4a59b0fdba9e8ff36bc029fa", - "url": "https://github.com/FunkinCrew/thx.semver" + "ref": "bdb191fe7cf745c02a980749906dbf22719e200b", + "url": "https://github.com/fponticelli/thx.semver" } ] } diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index c70f195d2..11b713f4d 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -491,8 +491,10 @@ class FunkinSound extends FlxSound implements ICloneable var promise:lime.app.Promise> = new lime.app.Promise>(); // split the path and get only after first : - // we are bypassing the openfl/lime asset library fuss + // we are bypassing the openfl/lime asset library fuss on web only + #if web path = Paths.stripLibrary(path); + #end var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end); diff --git a/source/funkin/effects/RetroCameraFade.hx b/source/funkin/effects/RetroCameraFade.hx new file mode 100644 index 000000000..d4c1da5ef --- /dev/null +++ b/source/funkin/effects/RetroCameraFade.hx @@ -0,0 +1,106 @@ +package funkin.effects; + +import flixel.util.FlxTimer; +import flixel.FlxCamera; +import openfl.filters.ColorMatrixFilter; + +class RetroCameraFade +{ + // im lazy, but we only use this for week 6 + // and also sorta yoinked for djflixel, lol ! + public static function fadeWhite(camera:FlxCamera, camSteps:Int = 5, time:Float = 1):Void + { + var steps:Int = 0; + var stepsTotal:Int = camSteps; + + new FlxTimer().start(time / stepsTotal, _ -> { + var V:Float = (1 / stepsTotal) * steps; + if (steps == stepsTotal) V = 1; + + var matrix = [ + 1, 0, 0, 0, V * 255, + 0, 1, 0, 0, V * 255, + 0, 0, 1, 0, V * 255, + 0, 0, 0, 1, 0 + ]; + camera.filters = [new ColorMatrixFilter(matrix)]; + steps++; + }, stepsTotal + 1); + } + + public static function fadeFromWhite(camera:FlxCamera, camSteps:Int = 5, time:Float = 1):Void + { + var steps:Int = camSteps; + var stepsTotal:Int = camSteps; + + var matrixDerp = [ + 1, 0, 0, 0, 1.0 * 255, + 0, 1, 0, 0, 1.0 * 255, + 0, 0, 1, 0, 1.0 * 255, + 0, 0, 0, 1, 0 + ]; + camera.filters = [new ColorMatrixFilter(matrixDerp)]; + + new FlxTimer().start(time / stepsTotal, _ -> { + var V:Float = (1 / stepsTotal) * steps; + if (steps == stepsTotal) V = 1; + + var matrix = [ + 1, 0, 0, 0, V * 255, + 0, 1, 0, 0, V * 255, + 0, 0, 1, 0, V * 255, + 0, 0, 0, 1, 0 + ]; + camera.filters = [new ColorMatrixFilter(matrix)]; + steps--; + }, camSteps); + } + + public static function fadeToBlack(camera:FlxCamera, camSteps:Int = 5, time:Float = 1):Void + { + var steps:Int = 0; + var stepsTotal:Int = camSteps; + + new FlxTimer().start(time / stepsTotal, _ -> { + var V:Float = (1 / stepsTotal) * steps; + if (steps == stepsTotal) V = 1; + + var matrix = [ + 1, 0, 0, 0, -V * 255, + 0, 1, 0, 0, -V * 255, + 0, 0, 1, 0, -V * 255, + 0, 0, 0, 1, 0 + ]; + camera.filters = [new ColorMatrixFilter(matrix)]; + steps++; + }, camSteps); + } + + public static function fadeBlack(camera:FlxCamera, camSteps:Int = 5, time:Float = 1):Void + { + var steps:Int = camSteps; + var stepsTotal:Int = camSteps; + + var matrixDerp = [ + 1, 0, 0, 0, -1.0 * 255, + 0, 1, 0, 0, -1.0 * 255, + 0, 0, 1, 0, -1.0 * 255, + 0, 0, 0, 1, 0 + ]; + camera.filters = [new ColorMatrixFilter(matrixDerp)]; + + new FlxTimer().start(time / stepsTotal, _ -> { + var V:Float = (1 / stepsTotal) * steps; + if (steps == stepsTotal) V = 1; + + var matrix = [ + 1, 0, 0, 0, -V * 255, + 0, 1, 0, 0, -V * 255, + 0, 0, 1, 0, -V * 255, + 0, 0, 0, 1, 0 + ]; + camera.filters = [new ColorMatrixFilter(matrix)]; + steps--; + }, camSteps + 1); + } +} diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx index bfd2e8028..521553527 100644 --- a/source/funkin/graphics/FunkinSprite.hx +++ b/source/funkin/graphics/FunkinSprite.hx @@ -7,6 +7,10 @@ import flixel.tweens.FlxTween; import openfl.display3D.textures.TextureBase; import funkin.graphics.framebuffer.FixedBitmapData; import openfl.display.BitmapData; +import flixel.math.FlxRect; +import flixel.math.FlxPoint; +import flixel.graphics.frames.FlxFrame; +import flixel.FlxCamera; /** * An FlxSprite with additional functionality. @@ -269,6 +273,103 @@ class FunkinSprite extends FlxSprite return result; } + @:access(flixel.FlxCamera) + override function getBoundingBox(camera:FlxCamera):FlxRect + { + getScreenPosition(_point, camera); + + _rect.set(_point.x, _point.y, width, height); + _rect = camera.transformRect(_rect); + + if (isPixelPerfectRender(camera)) + { + _rect.width = _rect.width / this.scale.x; + _rect.height = _rect.height / this.scale.y; + _rect.x = _rect.x / this.scale.x; + _rect.y = _rect.y / this.scale.y; + _rect.floor(); + _rect.x = _rect.x * this.scale.x; + _rect.y = _rect.y * this.scale.y; + _rect.width = _rect.width * this.scale.x; + _rect.height = _rect.height * this.scale.y; + } + + return _rect; + } + + /** + * Returns the screen position of this object. + * + * @param result Optional arg for the returning point + * @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used. + * @return The screen position of this object. + */ + public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint + { + if (result == null) result = FlxPoint.get(); + + if (camera == null) camera = FlxG.camera; + + result.set(x, y); + if (pixelPerfectPosition) + { + _rect.width = _rect.width / this.scale.x; + _rect.height = _rect.height / this.scale.y; + _rect.x = _rect.x / this.scale.x; + _rect.y = _rect.y / this.scale.y; + _rect.round(); + _rect.x = _rect.x * this.scale.x; + _rect.y = _rect.y * this.scale.y; + _rect.width = _rect.width * this.scale.x; + _rect.height = _rect.height * this.scale.y; + } + + return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y); + } + + override function drawSimple(camera:FlxCamera):Void + { + getScreenPosition(_point, camera).subtractPoint(offset); + if (isPixelPerfectRender(camera)) + { + _point.x = _point.x / this.scale.x; + _point.y = _point.y / this.scale.y; + _point.round(); + + _point.x = _point.x * this.scale.x; + _point.y = _point.y * this.scale.y; + } + + _point.copyToFlash(_flashPoint); + camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing); + } + + override function drawComplex(camera:FlxCamera):Void + { + _frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY()); + _matrix.translate(-origin.x, -origin.y); + _matrix.scale(scale.x, scale.y); + + if (bakedRotationAngle <= 0) + { + updateTrig(); + + if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle); + } + + getScreenPosition(_point, camera).subtractPoint(offset); + _point.add(origin.x, origin.y); + _matrix.translate(_point.x, _point.y); + + if (isPixelPerfectRender(camera)) + { + _matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x; + _matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y; + } + + camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader); + } + public override function destroy():Void { frames = null; diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx index e2cae5613..f6c881f6d 100644 --- a/source/funkin/input/Controls.hx +++ b/source/funkin/input/Controls.hx @@ -31,6 +31,7 @@ class Controls extends FlxActionSet * Uses FlxActions to funnel various inputs to a single action. */ var _ui_up = new FunkinAction(Action.UI_UP); + var _ui_left = new FunkinAction(Action.UI_LEFT); var _ui_right = new FunkinAction(Action.UI_RIGHT); var _ui_down = new FunkinAction(Action.UI_DOWN); @@ -325,19 +326,18 @@ class Controls extends FlxActionSet add(_volume_down); add(_volume_mute); - for (action in digitalActions) { - if (Std.isOfType(action, FunkinAction)) { + for (action in digitalActions) + { + if (Std.isOfType(action, FunkinAction)) + { var funkinAction:FunkinAction = cast action; byName[funkinAction.name] = funkinAction; - if (funkinAction.namePressed != null) - byName[funkinAction.namePressed] = funkinAction; - if (funkinAction.nameReleased != null) - byName[funkinAction.nameReleased] = funkinAction; + if (funkinAction.namePressed != null) byName[funkinAction.namePressed] = funkinAction; + if (funkinAction.nameReleased != null) byName[funkinAction.nameReleased] = funkinAction; } } - if (scheme == null) - scheme = None; + if (scheme == null) scheme = None; setKeyboardScheme(scheme, false); } @@ -350,38 +350,38 @@ class Controls extends FlxActionSet public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool { #if debug - if (!byName.exists(name)) - throw 'Invalid name: $name'; + if (!byName.exists(name)) throw 'Invalid name: $name'; #end var action = byName[name]; - if (gamepadOnly) - return action.checkFiltered(trigger, GAMEPAD); + if (gamepadOnly) return action.checkFiltered(trigger, GAMEPAD); else return action.checkFiltered(trigger); } - public function getKeysForAction(name:Action):Array { + public function getKeysForAction(name:Action):Array + { #if debug - if (!byName.exists(name)) - throw 'Invalid name: $name'; + if (!byName.exists(name)) throw 'Invalid name: $name'; #end // TODO: Revert to `.map().filter()` once HashLink doesn't complain anymore. var result:Array = []; - for (input in byName[name].inputs) { + for (input in byName[name].inputs) + { if (input.device == KEYBOARD) result.push(input.inputID); } return result; } - public function getButtonsForAction(name:Action):Array { + public function getButtonsForAction(name:Action):Array + { #if debug - if (!byName.exists(name)) - throw 'Invalid name: $name'; + if (!byName.exists(name)) throw 'Invalid name: $name'; #end var result:Array = []; - for (input in byName[name].inputs) { + for (input in byName[name].inputs) + { if (input.device == GAMEPAD) result.push(input.inputID); } return result; @@ -405,7 +405,7 @@ class Controls extends FlxActionSet function getActionFromControl(control:Control):FlxActionDigital { - return switch(control) + return switch (control) { case UI_UP: _ui_up; case UI_DOWN: _ui_down; @@ -448,7 +448,7 @@ class Controls extends FlxActionSet */ function forEachBound(control:Control, func:FlxActionDigital->FlxInputState->Void) { - switch(control) + switch (control) { case UI_UP: func(_ui_up, PRESSED); @@ -519,10 +519,9 @@ class Controls extends FlxActionSet public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int) { - if (toAdd == toRemove) - return; + if (toAdd == toRemove) return; - switch(device) + switch (device) { case Keys: forEachBound(control, function(action, state) replaceKey(action, toAdd, toRemove, state)); @@ -534,7 +533,8 @@ class Controls extends FlxActionSet function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState) { - if (action.inputs.length == 0) { + if (action.inputs.length == 0) + { // Add the keybind, don't replace. addKeys(action, [toAdd], state); return; @@ -548,34 +548,44 @@ class Controls extends FlxActionSet if (input.device == KEYBOARD && input.inputID == toRemove) { - if (toAdd == FlxKey.NONE) { + if (toAdd == FlxKey.NONE) + { // Remove the keybind, don't replace. action.inputs.remove(input); - } else { + } + else + { // Replace the keybind. @:privateAccess action.inputs[i].inputID = toAdd; } hasReplaced = true; - } else if (input.device == KEYBOARD && input.inputID == toAdd) { + } + else if (input.device == KEYBOARD && input.inputID == toAdd) + { // This key is already bound! - if (hasReplaced) { + if (hasReplaced) + { // Remove the duplicate keybind, don't replace. action.inputs.remove(input); - } else { + } + else + { hasReplaced = true; } } } - if (!hasReplaced) { + if (!hasReplaced) + { addKeys(action, [toAdd], state); } } function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState) { - if (action.inputs.length == 0) { + if (action.inputs.length == 0) + { addButtons(action, [toAdd], state, deviceID); return; } @@ -594,7 +604,8 @@ class Controls extends FlxActionSet } } - if (!hasReplaced) { + if (!hasReplaced) + { addButtons(action, [toAdd], state, deviceID); } } @@ -606,18 +617,16 @@ class Controls extends FlxActionSet var action = controls.byName[name]; for (input in action.inputs) { - if (device == null || isDevice(input, device)) - byName[name].add(cast input); + if (device == null || isDevice(input, device)) byName[name].add(cast input); } } - switch(device) + switch (device) { case null: // add all for (gamepad in controls.gamepadsAdded) - if (gamepadsAdded.indexOf(gamepad) == -1) - gamepadsAdded.push(gamepad); + if (gamepadsAdded.indexOf(gamepad) == -1) gamepadsAdded.push(gamepad); mergeKeyboardScheme(controls.keyboardScheme); @@ -637,7 +646,7 @@ class Controls extends FlxActionSet { if (scheme != None) { - switch(keyboardScheme) + switch (keyboardScheme) { case None: keyboardScheme = scheme; @@ -672,7 +681,8 @@ class Controls extends FlxActionSet static function addKeys(action:FlxActionDigital, keys:Array, state:FlxInputState) { - for (key in keys) { + for (key in keys) + { if (key == FlxKey.NONE) continue; // Ignore unbound keys. action.addKey(key, state); } @@ -684,15 +694,13 @@ class Controls extends FlxActionSet while (i-- > 0) { var input = action.inputs[i]; - if (input.device == KEYBOARD && keys.indexOf(cast input.inputID) != -1) - action.remove(input); + if (input.device == KEYBOARD && keys.indexOf(cast input.inputID) != -1) action.remove(input); } } public function setKeyboardScheme(scheme:KeyboardScheme, reset = true) { - if (reset) - removeKeyboard(); + if (reset) removeKeyboard(); keyboardScheme = scheme; @@ -724,10 +732,13 @@ class Controls extends FlxActionSet bindMobileLol(); } - function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array { - switch (scheme) { + function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array + { + switch (scheme) + { case Solo: - switch (control) { + switch (control) + { case Control.UI_UP: return [W, FlxKey.UP]; case Control.UI_DOWN: return [S, FlxKey.DOWN]; case Control.UI_LEFT: return [A, FlxKey.LEFT]; @@ -754,7 +765,8 @@ class Controls extends FlxActionSet case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO]; } case Duo(true): - switch (control) { + switch (control) + { case Control.UI_UP: return [W]; case Control.UI_DOWN: return [S]; case Control.UI_LEFT: return [A]; @@ -779,10 +791,10 @@ class Controls extends FlxActionSet case Control.VOLUME_UP: return [PLUS]; case Control.VOLUME_DOWN: return [MINUS]; case Control.VOLUME_MUTE: return [ZERO]; - } case Duo(false): - switch (control) { + switch (control) + { case Control.UI_UP: return [FlxKey.UP]; case Control.UI_DOWN: return [FlxKey.DOWN]; case Control.UI_LEFT: return [FlxKey.LEFT]; @@ -807,7 +819,6 @@ class Controls extends FlxActionSet case Control.VOLUME_UP: return [NUMPADPLUS]; case Control.VOLUME_DOWN: return [NUMPADMINUS]; case Control.VOLUME_MUTE: return [NUMPADZERO]; - } default: // Fallthrough. @@ -834,8 +845,7 @@ class Controls extends FlxActionSet #end #if android - forEachBound(Control.BACK, function(action, pres) - { + forEachBound(Control.BACK, function(action, pres) { action.add(new FlxActionInputDigitalAndroid(FlxAndroidKey.BACK, JUST_PRESSED)); }); #end @@ -849,8 +859,7 @@ class Controls extends FlxActionSet while (i-- > 0) { var input = action.inputs[i]; - if (input.device == KEYBOARD) - action.remove(input); + if (input.device == KEYBOARD) action.remove(input); } } } @@ -862,11 +871,13 @@ class Controls extends FlxActionSet fromSaveData(padData, Gamepad(id)); } - public function getGamepadIds():Array { + public function getGamepadIds():Array + { return gamepadsAdded; } - public function getGamepads():Array { + public function getGamepads():Array + { return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)]; } @@ -886,8 +897,7 @@ class Controls extends FlxActionSet while (i-- > 0) { var input = action.inputs[i]; - if (isGamepad(input, deviceID)) - action.remove(input); + if (isGamepad(input, deviceID)) action.remove(input); } } @@ -924,32 +934,58 @@ class Controls extends FlxActionSet ]); } - function getDefaultGamepadBinds(control:Control):Array { - switch(control) { - case Control.ACCEPT: return [#if switch B #else A #end]; - case Control.BACK: return [#if switch A #else B #end]; - case Control.UI_UP: return [DPAD_UP, LEFT_STICK_DIGITAL_UP]; - case Control.UI_DOWN: return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN]; - case Control.UI_LEFT: return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT]; - case Control.UI_RIGHT: return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT]; - case Control.NOTE_UP: return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP]; - case Control.NOTE_DOWN: return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN]; - 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 [FlxGamepadInputID.BACK]; // Back (i.e. Select) - case Control.WINDOW_FULLSCREEN: []; - case Control.WINDOW_SCREENSHOT: []; - case Control.CUTSCENE_ADVANCE: return [A]; - case Control.FREEPLAY_FAVORITE: [FlxGamepadInputID.BACK]; // Back (i.e. Select) - case Control.FREEPLAY_LEFT: [LEFT_SHOULDER]; - case Control.FREEPLAY_RIGHT: [RIGHT_SHOULDER]; - case Control.VOLUME_UP: []; - case Control.VOLUME_DOWN: []; - case Control.VOLUME_MUTE: []; - case Control.DEBUG_MENU: []; - case Control.DEBUG_CHART: []; - case Control.DEBUG_STAGE: []; + function getDefaultGamepadBinds(control:Control):Array + { + switch (control) + { + case Control.ACCEPT: + return [#if switch B #else A #end]; + case Control.BACK: + return [#if switch A #else B #end]; + case Control.UI_UP: + return [DPAD_UP, LEFT_STICK_DIGITAL_UP]; + case Control.UI_DOWN: + return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN]; + case Control.UI_LEFT: + return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT]; + case Control.UI_RIGHT: + return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT]; + case Control.NOTE_UP: + return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP]; + case Control.NOTE_DOWN: + return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN]; + 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 [FlxGamepadInputID.BACK]; // Back (i.e. Select) + case Control.WINDOW_FULLSCREEN: + []; + case Control.WINDOW_SCREENSHOT: + []; + case Control.CUTSCENE_ADVANCE: + return [A]; + case Control.FREEPLAY_FAVORITE: + [FlxGamepadInputID.BACK]; // Back (i.e. Select) + case Control.FREEPLAY_LEFT: + [LEFT_SHOULDER]; + case Control.FREEPLAY_RIGHT: + [RIGHT_SHOULDER]; + case Control.VOLUME_UP: + []; + case Control.VOLUME_DOWN: + []; + case Control.VOLUME_MUTE: + []; + case Control.DEBUG_MENU: + []; + case Control.DEBUG_CHART: + []; + case Control.DEBUG_STAGE: + []; default: // Fallthrough. } @@ -967,8 +1003,7 @@ class Controls extends FlxActionSet public function touchShit(control:Control, id) { - forEachBound(control, function(action, state) - { + forEachBound(control, function(action, state) { // action }); } @@ -984,7 +1019,8 @@ class Controls extends FlxActionSet inline static function addButtons(action:FlxActionDigital, buttons:Array, state, id) { - for (button in buttons) { + for (button in buttons) + { if (button == FlxGamepadInputID.NONE) continue; // Ignore unbound keys. action.addGamepad(button, state, id); } @@ -996,29 +1032,25 @@ class Controls extends FlxActionSet while (i-- > 0) { var input = action.inputs[i]; - if (isGamepad(input, gamepadID) && buttons.indexOf(cast input.inputID) != -1) - action.remove(input); + if (isGamepad(input, gamepadID) && buttons.indexOf(cast input.inputID) != -1) action.remove(input); } } public function getInputsFor(control:Control, device:Device, ?list:Array):Array { - if (list == null) - list = []; + if (list == null) list = []; - switch(device) + switch (device) { case Keys: for (input in getActionFromControl(control).inputs) { - if (input.device == KEYBOARD) - list.push(input.inputID); + if (input.device == KEYBOARD) list.push(input.inputID); } case Gamepad(id): for (input in getActionFromControl(control).inputs) { - if (isGamepad(input, id)) - list.push(input.inputID); + if (isGamepad(input, id)) list.push(input.inputID); } } return list; @@ -1026,7 +1058,7 @@ class Controls extends FlxActionSet public function removeDevice(device:Device) { - switch(device) + switch (device) { case Keys: setKeyboardScheme(None); @@ -1040,27 +1072,32 @@ class Controls extends FlxActionSet * An EMPTY array means the control is uninitialized and needs to be reset to default. * An array with a single FlxKey.NONE means the control was intentionally unbound by the user. */ - public function fromSaveData(data:Dynamic, device:Device) + public function fromSaveData(data:Dynamic, device:Device):Void { for (control in Control.createAll()) { var inputs:Array = Reflect.field(data, control.getName()); - inputs = inputs.distinct(); + inputs = inputs?.distinct(); if (inputs != null) { - if (inputs.length == 0) { + if (inputs.length == 0) + { trace('Control ${control} is missing bindings, resetting to default.'); - switch(device) + switch (device) { case Keys: bindKeys(control, getDefaultKeybinds(Solo, control)); case Gamepad(id): bindButtons(control, id, getDefaultGamepadBinds(control)); } - } else if (inputs == [FlxKey.NONE]) { + } + else if (inputs == [FlxKey.NONE]) + { trace('Control ${control} is unbound, leaving it be.'); - } else { - switch(device) + } + else + { + switch (device) { case Keys: bindKeys(control, inputs.copy()); @@ -1068,9 +1105,11 @@ class Controls extends FlxActionSet bindButtons(control, id, inputs.copy()); } } - } else { + } + else + { trace('Control ${control} is missing bindings, resetting to default.'); - switch(device) + switch (device) { case Keys: bindKeys(control, getDefaultKeybinds(Solo, control)); @@ -1095,9 +1134,12 @@ class Controls extends FlxActionSet var inputs = getInputsFor(control, device); isEmpty = isEmpty && inputs.length == 0; - if (inputs.length == 0) { + if (inputs.length == 0) + { inputs = [FlxKey.NONE]; - } else { + } + else + { inputs = inputs.distinct(); } @@ -1109,7 +1151,7 @@ class Controls extends FlxActionSet static function isDevice(input:FlxActionInput, device:Device) { - return switch(device) + return switch (device) { case Keys: input.device == KEYBOARD; case Gamepad(id): isGamepad(input, id); @@ -1141,7 +1183,8 @@ typedef Swipes = * - Combining `pressed` and `released` inputs into one action. * - Filtering by input method (`KEYBOARD`, `MOUSE`, `GAMEPAD`, etc). */ -class FunkinAction extends FlxActionDigital { +class FunkinAction extends FlxActionDigital +{ public var namePressed(default, null):Null; public var nameReleased(default, null):Null; @@ -1158,83 +1201,102 @@ class FunkinAction extends FlxActionDigital { /** * Input checks default to whether the input was just pressed, on any input device. */ - public override function check():Bool { + public override function check():Bool + { return checkFiltered(JUST_PRESSED); } /** * Check whether the input is currently being held. */ - public function checkPressed():Bool { + public function checkPressed():Bool + { return checkFiltered(PRESSED); } /** * Check whether the input is currently being held, and was not held last frame. */ - public function checkJustPressed():Bool { + public function checkJustPressed():Bool + { return checkFiltered(JUST_PRESSED); } /** * Check whether the input is not currently being held. */ - public function checkReleased():Bool { + public function checkReleased():Bool + { return checkFiltered(RELEASED); } /** * Check whether the input is not currently being held, and was held last frame. */ - public function checkJustReleased():Bool { + public function checkJustReleased():Bool + { return checkFiltered(JUST_RELEASED); } /** * Check whether the input is currently being held by a gamepad device. */ - public function checkPressedGamepad():Bool { + public function checkPressedGamepad():Bool + { return checkFiltered(PRESSED, GAMEPAD); } /** * Check whether the input is currently being held by a gamepad device, and was not held last frame. */ - public function checkJustPressedGamepad():Bool { + public function checkJustPressedGamepad():Bool + { return checkFiltered(JUST_PRESSED, GAMEPAD); } /** * Check whether the input is not currently being held by a gamepad device. */ - public function checkReleasedGamepad():Bool { + public function checkReleasedGamepad():Bool + { return checkFiltered(RELEASED, GAMEPAD); } /** * Check whether the input is not currently being held by a gamepad device, and was held last frame. */ - public function checkJustReleasedGamepad():Bool { + public function checkJustReleasedGamepad():Bool + { return checkFiltered(JUST_RELEASED, GAMEPAD); } - public function checkMultiFiltered(?filterTriggers:Array, ?filterDevices:Array):Bool { - if (filterTriggers == null) { + public function checkMultiFiltered(?filterTriggers:Array, ?filterDevices:Array):Bool + { + if (filterTriggers == null) + { filterTriggers = [PRESSED, JUST_PRESSED]; } - if (filterDevices == null) { + if (filterDevices == null) + { filterDevices = []; } // Perform checkFiltered for each combination. - for (i in filterTriggers) { - if (filterDevices.length == 0) { - if (checkFiltered(i)) { + for (i in filterTriggers) + { + if (filterDevices.length == 0) + { + if (checkFiltered(i)) + { return true; } - } else { - for (j in filterDevices) { - if (checkFiltered(i, j)) { + } + else + { + for (j in filterDevices) + { + if (checkFiltered(i, j)) + { return true; } } @@ -1249,52 +1311,56 @@ class FunkinAction extends FlxActionDigital { * @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`). * @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`). */ - public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool { + public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool + { // The normal // Make sure we only update the inputs once per frame. var key = '${filterTrigger}:${filterDevice}'; var cacheEntry = cache.get(key); - if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) { + if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) + { return cacheEntry.value; } // Use a for loop instead so we can remove inputs while iterating. // We don't return early because we need to call check() on ALL inputs. var result = false; - var len = inputs != null ? inputs.length : 0; - for (i in 0...len) - { - var j = len - i - 1; - var input = inputs[j]; + var len = inputs != null ? inputs.length : 0; + for (i in 0...len) + { + var j = len - i - 1; + var input = inputs[j]; // Filter out dead inputs. - if (input.destroyed) - { - inputs.splice(j, 1); - continue; - } + if (input.destroyed) + { + inputs.splice(j, 1); + continue; + } // Update the input. input.update(); // Check whether the input is the right trigger. - if (filterTrigger != null && input.trigger != filterTrigger) { + if (filterTrigger != null && input.trigger != filterTrigger) + { continue; } // Check whether the input is the right device. - if (filterDevice != null && input.device != filterDevice) { + if (filterDevice != null && input.device != filterDevice) + { continue; } // Check whether the input has triggered. - if (input.check(this)) - { - result = true; - } - } + if (input.check(this)) + { + result = true; + } + } // We need to cache this result. cache.set(key, {timestamp: FlxG.game.ticks, value: result}); @@ -1391,12 +1457,12 @@ class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital { var degAngle = FlxAngle.asDegrees(swp.touchAngle); - switch(trigger) + switch (trigger) { case JUST_PRESSED: if (swp.touchLength >= activateLength) { - switch(inputID) + switch (inputID) { case FlxDirectionFlags.UP: if (degAngle >= 45 && degAngle <= 90 + 45) return properTouch(swp); @@ -1440,7 +1506,7 @@ class FlxActionInputDigitalAndroid extends FlxActionInputDigital override public function check(Action:FlxAction):Bool { - return switch(trigger) + return switch (trigger) { #if android case PRESSED: FlxG.android.checkStatus(inputID, PRESSED) || FlxG.android.checkStatus(inputID, PRESSED); diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index 10636afdf..55c2a8992 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -9,6 +9,7 @@ import funkin.modding.module.ModuleHandler; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent.CountdownScriptEvent; import flixel.util.FlxTimer; +import funkin.util.EaseUtil; import funkin.audio.FunkinSound; class Countdown @@ -117,7 +118,7 @@ class Countdown * * If you want to call this from a module, it's better to use the event system and cancel the onCountdownStep event. */ - public static function pauseCountdown() + public static function pauseCountdown():Void { if (countdownTimer != null && !countdownTimer.finished) { @@ -130,7 +131,7 @@ class Countdown * * If you want to call this from a module, it's better to use the event system and cancel the onCountdownStep event. */ - public static function resumeCountdown() + public static function resumeCountdown():Void { if (countdownTimer != null && !countdownTimer.finished) { @@ -143,7 +144,7 @@ class Countdown * * If you want to call this from a module, it's better to use the event system and cancel the onCountdownStart event. */ - public static function stopCountdown() + public static function stopCountdown():Void { if (countdownTimer != null) { @@ -156,7 +157,7 @@ class Countdown /** * Stops the current countdown, then starts the song for you. */ - public static function skipCountdown() + public static function skipCountdown():Void { stopCountdown(); // This will trigger PlayState.startSong() @@ -185,8 +186,11 @@ class Countdown { var spritePath:String = null; + var fadeEase = FlxEase.cubeInOut; + if (isPixelStyle) { + fadeEase = EaseUtil.stepped(8); switch (index) { case TWO: @@ -227,7 +231,7 @@ class Countdown countdownSprite.screenCenter(); // Fade sprite in, then out, then destroy it. - FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000, + FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100}, Conductor.instance.beatLengthMs / 1000, { ease: FlxEase.cubeInOut, onComplete: function(twn:FlxTween) { @@ -235,6 +239,11 @@ class Countdown } }); + FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000, + { + ease: fadeEase + }); + PlayState.instance.add(countdownSprite); } diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index c84d5b154..6eb954c0f 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -16,6 +16,7 @@ import funkin.ui.MusicBeatSubState; import funkin.ui.story.StoryMenuState; import funkin.util.MathUtil; import openfl.utils.Assets; +import funkin.effects.RetroCameraFade; /** * A substate which renders over the PlayState when the player dies. @@ -144,6 +145,7 @@ class GameOverSubState extends MusicBeatSubState else { boyfriend = PlayState.instance.currentStage.getBoyfriend(true); + boyfriend.canPlayOtherAnims = true; boyfriend.isDead = true; add(boyfriend); boyfriend.resetCharacter(); @@ -331,9 +333,12 @@ class GameOverSubState extends MusicBeatSubState // After the animation finishes... new FlxTimer().start(0.7, function(tmr:FlxTimer) { // ...fade out the graphics. Then after that happens... - FlxG.camera.fade(FlxColor.BLACK, 2, false, function() { + + var resetPlaying = function(pixel:Bool = false) { // ...close the GameOverSubState. - FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true); + if (pixel) RetroCameraFade.fadeBlack(FlxG.camera, 10, 1); + else + FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true); PlayState.instance.needsReset = true; if (PlayState.instance.isMinimalMode || boyfriend == null) {} @@ -350,7 +355,22 @@ class GameOverSubState extends MusicBeatSubState // Close the substate. close(); - }); + }; + + if (musicSuffix == '-pixel') + { + RetroCameraFade.fadeToBlack(FlxG.camera, 10, 2); + new FlxTimer().start(2, _ -> { + FlxG.camera.filters = []; + resetPlaying(true); + }); + } + else + { + FlxG.camera.fade(FlxColor.BLACK, 2, false, function() { + resetPlaying(); + }); + } }); } } diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 48fb3b04e..a2c5f7e62 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -464,7 +464,9 @@ class ResultState extends MusicBeatSubState { bgFlash.visible = true; FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24); - var clearPercentFloat = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100; + // NOTE: Only divide if totalNotes > 0 to prevent divide-by-zero errors. + var clearPercentFloat = params.scoreData.tallies.totalNotes == 0 ? 0.0 : (params.scoreData.tallies.sick + + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100; clearPercentTarget = Math.floor(clearPercentFloat); // Prevent off-by-one errors. diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index 7d3d6cfb9..8b1649e26 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -305,8 +305,8 @@ class CharacterDataParser icon = "darnell"; case "senpai-angry": icon = "senpai"; - case "tankman" | "tankman-atlas": - icon = "tankmen"; + case "tankman-atlas": + icon = "tankman"; } var path = Paths.image("freeplay/icons/" + icon + "pixel"); diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx index 48c5afb58..41c96fbfa 100644 --- a/source/funkin/play/character/MultiSparrowCharacter.hx +++ b/source/funkin/play/character/MultiSparrowCharacter.hx @@ -41,6 +41,8 @@ class MultiSparrowCharacter extends BaseCharacter { this.isPixel = true; this.antialiasing = false; + pixelPerfectRender = true; + pixelPerfectPosition = true; } else { diff --git a/source/funkin/play/character/PackerCharacter.hx b/source/funkin/play/character/PackerCharacter.hx index 2bfac800a..22edbe339 100644 --- a/source/funkin/play/character/PackerCharacter.hx +++ b/source/funkin/play/character/PackerCharacter.hx @@ -43,6 +43,8 @@ class PackerCharacter extends BaseCharacter { this.isPixel = true; this.antialiasing = false; + pixelPerfectRender = true; + pixelPerfectPosition = true; } else { diff --git a/source/funkin/play/character/SparrowCharacter.hx b/source/funkin/play/character/SparrowCharacter.hx index a36aed84d..81d98b138 100644 --- a/source/funkin/play/character/SparrowCharacter.hx +++ b/source/funkin/play/character/SparrowCharacter.hx @@ -46,6 +46,8 @@ class SparrowCharacter extends BaseCharacter { this.isPixel = true; this.antialiasing = false; + pixelPerfectRender = true; + pixelPerfectPosition = true; } else { diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx index b7e206e97..1bdfd98a8 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -7,8 +7,9 @@ import flixel.util.FlxDirection; import funkin.graphics.FunkinSprite; import funkin.play.PlayState; import funkin.util.TimerUtil; +import funkin.util.EaseUtil; -class PopUpStuff extends FlxTypedGroup +class PopUpStuff extends FlxTypedGroup { public var offsets:Array = [0, 0]; @@ -17,7 +18,7 @@ class PopUpStuff extends FlxTypedGroup super(); } - public function displayRating(daRating:String) + public function displayRating(daRating:String):Void { var perfStart:Float = TimerUtil.start(); @@ -40,10 +41,15 @@ class PopUpStuff extends FlxTypedGroup add(rating); + var fadeEase = null; + if (PlayState.instance.currentStageId.startsWith('school')) { rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7)); rating.antialiasing = false; + rating.pixelPerfectRender = true; + rating.pixelPerfectPosition = true; + fadeEase = EaseUtil.stepped(2); } else { @@ -61,7 +67,8 @@ class PopUpStuff extends FlxTypedGroup remove(rating, true); rating.destroy(); }, - startDelay: Conductor.instance.beatLengthMs * 0.001 + startDelay: Conductor.instance.beatLengthMs * 0.001, + ease: fadeEase }); trace('displayRating took: ${TimerUtil.seconds(perfStart)}'); @@ -92,10 +99,15 @@ class PopUpStuff extends FlxTypedGroup // add(comboSpr); + var fadeEase = null; + if (PlayState.instance.currentStageId.startsWith('school')) { - comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 0.7)); + comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 1)); comboSpr.antialiasing = false; + comboSpr.pixelPerfectRender = true; + comboSpr.pixelPerfectPosition = true; + fadeEase = EaseUtil.stepped(2); } else { @@ -110,7 +122,8 @@ class PopUpStuff extends FlxTypedGroup remove(comboSpr, true); comboSpr.destroy(); }, - startDelay: Conductor.instance.beatLengthMs * 0.001 + startDelay: Conductor.instance.beatLengthMs * 0.001, + ease: fadeEase }); var seperatedScore:Array = []; @@ -133,8 +146,10 @@ class PopUpStuff extends FlxTypedGroup if (PlayState.instance.currentStageId.startsWith('school')) { - numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 0.7)); + numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE * 1)); numScore.antialiasing = false; + numScore.pixelPerfectRender = true; + numScore.pixelPerfectPosition = true; } else { @@ -156,7 +171,8 @@ class PopUpStuff extends FlxTypedGroup remove(numScore, true); numScore.destroy(); }, - startDelay: Conductor.instance.beatLengthMs * 0.002 + startDelay: Conductor.instance.beatLengthMs * 0.002, + ease: fadeEase }); daLoop++; diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 262aff7bc..11fb9e499 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -200,12 +200,10 @@ class Bopper extends StageProp implements IPlayStateScriptedClass { if (hasDanced) { - trace('DanceRight (alternate)'); playAnimation('danceRight$idleSuffix', forceRestart); } else { - trace('DanceLeft (alternate)'); playAnimation('danceLeft$idleSuffix', forceRestart); } hasDanced = !hasDanced; diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 4f8ab4434..f4e22e380 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -249,6 +249,10 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements // If pixel, disable antialiasing. propSprite.antialiasing = !dataProp.isPixel; + // If pixel, we render it pixel perfect so there's less "mixels" + propSprite.pixelPerfectRender = dataProp.isPixel; + propSprite.pixelPerfectPosition = dataProp.isPixel; + propSprite.scrollFactor.x = dataProp.scroll[0]; propSprite.scrollFactor.y = dataProp.scroll[1]; diff --git a/source/funkin/ui/MenuItem.hx b/source/funkin/ui/MenuItem.hx index ba5cc066b..2a483ea78 100644 --- a/source/funkin/ui/MenuItem.hx +++ b/source/funkin/ui/MenuItem.hx @@ -11,7 +11,6 @@ class MenuItem extends FlxSpriteGroup { public var targetY:Float = 0; public var week:FlxSprite; - public var flashingInt:Int = 0; public function new(x:Float, y:Float, weekNum:Int = 0, weekType:WeekType) { @@ -30,28 +29,28 @@ class MenuItem extends FlxSpriteGroup } var isFlashing:Bool = false; + var flashTick:Float = 0; + final flashFramerate:Float = 20; public function startFlashing():Void { isFlashing = true; } - // if it runs at 60fps, fake framerate will be 6 - // if it runs at 144 fps, fake framerate will be like 14, and will update the graphic every 0.016666 * 3 seconds still??? - // so it runs basically every so many seconds, not dependant on framerate?? - // I'm still learning how math works thanks whoever is reading this lol - var fakeFramerate:Int = Math.round((1 / FlxG.elapsed) / 10); - override function update(elapsed:Float) { super.update(elapsed); y = MathUtil.coolLerp(y, (targetY * 120) + 480, 0.17); - if (isFlashing) flashingInt += 1; - - if (flashingInt % fakeFramerate >= Math.floor(fakeFramerate / 2)) week.color = 0xFF33ffff; - else - week.color = FlxColor.WHITE; + if (isFlashing) + { + flashTick += elapsed; + if (flashTick >= 1 / flashFramerate) + { + flashTick %= 1 / flashFramerate; + week.color = (week.color == FlxColor.WHITE) ? 0xFF33ffff : FlxColor.WHITE; + } + } } } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 637c23c95..abbe2d6bd 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -227,7 +227,7 @@ class FreeplayState extends MusicBeatSubState super(FlxColor.TRANSPARENT); - if (stickers != null) + if (stickers?.members != null) { stickerSubState = stickers; } @@ -640,7 +640,6 @@ class FreeplayState extends MusicBeatSubState // be careful not to "add()" things in here unless it's to a group that's already added to the state // otherwise it won't be properly attatched to funnyCamera (relavent code should be at the bottom of create()) var onDJIntroDone = function() { - // when boyfriend hits dat shiii albumRoll.playIntro(); @@ -693,9 +692,12 @@ class FreeplayState extends MusicBeatSubState } }; - if (dj != null) { + if (dj != null) + { dj.onIntroDone.add(onDJIntroDone); - } else { + } + else + { onDJIntroDone(); } @@ -1775,7 +1777,7 @@ class FreeplayState extends MusicBeatSubState if (dj != null) dj.confirm(); grpCapsules.members[curSelected].forcePosition(); - grpCapsules.members[curSelected].songText.flickerText(); + grpCapsules.members[curSelected].confirm(); // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut}); FlxTween.color(pinkBack, 0.33, 0xFFFFD0D5, 0xFF171831, {ease: FlxEase.quadOut}); @@ -1947,6 +1949,7 @@ class FreeplayState extends MusicBeatSubState startingVolume: 0.0, overrideExisting: true, restartTrack: false, + mapTimeChanges: false, // The music metadata is not alongside the audio file so this won't work. pathsFunction: INST, suffix: instSuffix, partialParams: @@ -2097,6 +2100,8 @@ class FreeplaySongData function set_currentDifficulty(value:String):String { + if (currentDifficulty == value) return value; + currentDifficulty = value; updateValues(displayedVariations); return value; @@ -2153,7 +2158,7 @@ class FreeplaySongData { this.albumId = songDifficulty.album; } - + // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration. // `easy`, `erect`, `normal-pico`, etc. var suffixedDifficulty = (songDifficulty.variation != Constants.DEFAULT_VARIATION diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 7708b3bcf..68525a5f7 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -213,6 +213,7 @@ class SongMenuItem extends FlxSpriteGroup favIconBlurred.frames = Paths.getSparrowAtlas('freeplay/favHeart'); favIconBlurred.animation.addByPrefix('fav', 'favorite heart', 24, false); favIconBlurred.animation.play('fav'); + favIconBlurred.setGraphicSize(50, 50); favIconBlurred.blend = BlendMode.ADD; favIconBlurred.shader = new GaussianBlurShader(1.2); @@ -516,6 +517,9 @@ class SongMenuItem extends FlxSpriteGroup updateDifficultyRating(songData?.difficultyRating ?? 0); updateScoringRank(songData?.scoringRank); newText.visible = songData?.isNew; + favIcon.animation.curAnim.curFrame = favIcon.animation.curAnim.numFrames - 1; + favIconBlurred.animation.curAnim.curFrame = favIconBlurred.animation.curAnim.numFrames - 1; + // Update opacity, offsets, etc. updateSelected(); @@ -539,8 +543,6 @@ class SongMenuItem extends FlxSpriteGroup charPath += 'monsterpixel'; case 'mom-car': charPath += 'mommypixel'; - case 'dad': - charPath += 'daddypixel'; case 'darnell-blazin': charPath += 'darnellpixel'; case 'senpai-angry': @@ -555,7 +557,17 @@ class SongMenuItem extends FlxSpriteGroup return; } - pixelIcon.loadGraphic(Paths.image(charPath)); + var isAnimated = openfl.utils.Assets.exists(Paths.file('images/$charPath.xml')); + + if (isAnimated) + { + pixelIcon.frames = Paths.getSparrowAtlas(charPath); + } + else + { + pixelIcon.loadGraphic(Paths.image(charPath)); + } + pixelIcon.scale.x = pixelIcon.scale.y = 2; switch (char) @@ -567,6 +579,22 @@ class SongMenuItem extends FlxSpriteGroup } // pixelIcon.origin.x = capsule.origin.x; // pixelIcon.offset.x -= pixelIcon.origin.x; + + if (isAnimated) + { + pixelIcon.active = true; + + pixelIcon.animation.addByPrefix('idle', 'idle0', 10, true); + pixelIcon.animation.addByPrefix('confirm', 'confirm0', 10, false); + pixelIcon.animation.addByPrefix('confirm-hold', 'confirm-hold0', 10, true); + + pixelIcon.animation.finishCallback = function(name:String):Void { + trace('Finish pixel animation: ${name}'); + if (name == 'confirm') pixelIcon.animation.play('confirm-hold'); + }; + + pixelIcon.animation.play('idle'); + } } var frameInTicker:Float = 0; @@ -707,6 +735,18 @@ class SongMenuItem extends FlxSpriteGroup super.update(elapsed); } + /** + * Play any animations associated with selecting this song. + */ + public function confirm():Void + { + if (songText != null) songText.flickerText(); + if (pixelIcon != null) + { + pixelIcon.animation.play('confirm'); + } + } + public function intendedY(index:Int):Float { return (index * ((height * realScaled) + 10)) + 120; diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index 56ffc9a27..9bf465484 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -156,6 +156,9 @@ class MainMenuState extends MusicBeatState resetCamStuff(); + // reset camera when debug menu is closed + subStateClosed.add(_ -> resetCamStuff(false)); + subStateOpened.add(sub -> { if (Type.getClass(sub) == FreeplayState) { @@ -185,10 +188,11 @@ class MainMenuState extends MusicBeatState }); } - function resetCamStuff():Void + function resetCamStuff(?snap:Bool = true):Void { FlxG.camera.follow(camFollow, null, 0.06); - FlxG.camera.snapToTarget(); + + if (snap) FlxG.camera.snapToTarget(); } function createMenuItem(name:String, atlas:String, callback:Void->Void, fireInstantly:Bool = false):Void @@ -347,8 +351,6 @@ class MainMenuState extends MusicBeatState persistentUpdate = false; FlxG.state.openSubState(new DebugMenuSubState()); - // reset camera when debug menu is closed - subStateClosed.addOnce(_ -> resetCamStuff()); } #end diff --git a/source/funkin/ui/story/LevelTitle.hx b/source/funkin/ui/story/LevelTitle.hx index e6f989016..2be2da154 100644 --- a/source/funkin/ui/story/LevelTitle.hx +++ b/source/funkin/ui/story/LevelTitle.hx @@ -13,13 +13,10 @@ class LevelTitle extends FlxSpriteGroup public final level:Level; public var targetY:Float; - public var isFlashing:Bool = false; var title:FlxSprite; var lock:FlxSprite; - var flashingInt:Int = 0; - public function new(x:Int, y:Int, level:Level) { super(x, y); @@ -46,20 +43,23 @@ class LevelTitle extends FlxSpriteGroup } } - // if it runs at 60fps, fake framerate will be 6 - // if it runs at 144 fps, fake framerate will be like 14, and will update the graphic every 0.016666 * 3 seconds still??? - // so it runs basically every so many seconds, not dependant on framerate?? - // I'm still learning how math works thanks whoever is reading this lol - var fakeFramerate:Int = Math.round((1 / FlxG.elapsed) / 10); + public var isFlashing:Bool = false; + var flashTick:Float = 0; + final flashFramerate:Float = 20; public override function update(elapsed:Float):Void { this.y = MathUtil.coolLerp(y, targetY, 0.17); - if (isFlashing) flashingInt += 1; - if (flashingInt % fakeFramerate >= Math.floor(fakeFramerate / 2)) title.color = 0xFF33ffff; - else - title.color = FlxColor.WHITE; + if (isFlashing) + { + flashTick += elapsed; + if (flashTick >= 1 / flashFramerate) + { + flashTick %= 1 / flashFramerate; + title.color = (title.color == FlxColor.WHITE) ? 0xFF33ffff : FlxColor.WHITE; + } + } } public function showLock():Void diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 06a83ab4d..7707850ce 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -113,7 +113,7 @@ class StoryMenuState extends MusicBeatState { super(); - if (stickers != null) + if (stickers?.members != null) { stickerSubState = stickers; } diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index bc26ad97a..0f2ce1076 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -346,7 +346,7 @@ class LoadingState extends MusicBeatSubState return 'Done precaching ${path}'; }, true); - trace("Queued ${path} for precaching"); + trace('Queued ${path} for precaching'); // FunkinSprite.cacheTexture(path); } diff --git a/source/funkin/util/logging/AnsiTrace.hx b/source/funkin/util/logging/AnsiTrace.hx index 9fdc19e1b..2c18d494d 100644 --- a/source/funkin/util/logging/AnsiTrace.hx +++ b/source/funkin/util/logging/AnsiTrace.hx @@ -51,7 +51,7 @@ class AnsiTrace public static function traceBF() { - #if sys + #if (sys && debug) if (colorSupported) { for (line in ansiBF) From c9c94a5fda5378effd0cf060696882a75d016b53 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Thu, 4 Jul 2024 15:55:19 -0400 Subject: [PATCH 20/20] note fadein fix --- .gitignore | 1 + source/funkin/play/PlayState.hx | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 143cf08c9..5c8cb5f55 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ node_modules/ package.json package-lock.json .aider.* +.aider* diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index de8492882..5ede97e6c 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1795,11 +1795,8 @@ class PlayState extends MusicBeatSubState opponentStrumline.zIndex = 1000; opponentStrumline.cameras = [camHUD]; - if (!PlayStatePlaylist.isStoryMode) - { - playerStrumline.fadeInArrows(); - opponentStrumline.fadeInArrows(); - } + playerStrumline.fadeInArrows(); + opponentStrumline.fadeInArrows(); } /**