diff --git a/.gitignore b/.gitignore index 84585eee0..068780a25 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,10 @@ shitAudio/ node_modules/ package.json package-lock.json +<<<<<<< HEAD +.aider* +||||||| bcaeae27 +======= +.aider.* +.aider* +>>>>>>> rewrite/master diff --git a/Project.xml b/Project.xml index fae9c768b..8eb62bb1d 100644 --- a/Project.xml +++ b/Project.xml @@ -2,7 +2,7 @@ - + diff --git a/assets b/assets index 225e248f1..361f696ce 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 225e248f148a92500a6fe90e4f10e4cd2acee782 +Subproject commit 361f696cec5c4027ebcfa6f7cec5ba718eaab0d2 diff --git a/checkstyle.json b/checkstyle.json index 41f0a7998..6c463f75a 100644 --- a/checkstyle.json +++ b/checkstyle.json @@ -327,7 +327,8 @@ "INLINE", "DYNAMIC", "FINAL" - ] + ], + "severity": "IGNORE" }, "type": "ModifierOrder" }, diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 49b15ddf6..6e370b5ff 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(); @@ -221,6 +223,7 @@ class InitState extends FlxState storyMode: false, title: "Cum Song Erect by Kawai Sprite", songId: "cum", + characterId: "pico-playable", difficultyId: "nightmare", isNewHighscore: true, scoreData: @@ -236,8 +239,13 @@ class InitState extends FlxState combo: 69, maxCombo: 69, totalNotesHit: 140, - totalNotes: 200 // 0, + totalNotes: 190 } + // 2000 = loss + // 240 = good + // 230 = great + // 210 = excellent + // 190 = perfect }, })); #elseif ANIMDEBUG 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/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 11b713f4d..9cb131c91 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):Null { var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad); + return result; } /** 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..f6c085018 --- /dev/null +++ b/source/funkin/data/freeplay/player/PlayerData.hx @@ -0,0 +1,244 @@ +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; + + /** + * Data for displaying this character in the Freeplay menu. + * If null, display no DJ. + */ + @:optional + public var freeplayDJ:Null = null; + + public var results:Null = null; + + /** + * 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; + } +} + +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; + + public function new() + { + animationMap = new Map(); + } + + 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); + } + } + + public function getAtlasPath():String + { + 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(); + + var anim = animationMap.get(name); + if (anim == null) return null; + return anim.prefix; + } + + public function getAnimationOffsetsByPrefix(?prefix:String):Array + { + if (prefixToOffsetsMap.size() == 0) mapAnimations(); + if (prefix == null) return [0, 0]; + return prefixToOffsetsMap.get(prefix); + } + + public function getAnimationOffsets(name:String):Array + { + return getAnimationOffsetsByPrefix(getAnimationPrefix(name)); + } + + // 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 PlayerResultsData = +{ + var perfect:Array; + var excellent:Array; + var great:Array; + var good:Array; + var loss:Array; +}; + +typedef PlayerResultsAnimationData = +{ + /** + * `sparrow` or `animate` or whatever + */ + var renderType:String; + + var assetPath:String; + + @:optional + @:default([0, 0]) + var offsets:Array; + + @:optional + @:default(500) + var zIndex:Int; + + @:optional + @:default(0.0) + var delay:Float; + + @:optional + @:default(1.0) + var scale:Float; + + @:optional + @:default('') + var startFrameLabel:Null; + + @:optional + @:default(true) + var looped:Bool; + + @:optional + var loopFrame:Null; + + @:optional + var loopFrameLabel:Null; +}; + +typedef PlayerFreeplayDJCartoonData = +{ + var soundClickFrame:Int; + var soundCartoonFrame:Int; + var loopBlinkFrame:Int; + var loopFrame:Int; + var channelChangeFrame:Int; +} diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx new file mode 100644 index 000000000..4656a1286 --- /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):Null + { + 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/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/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index 8a77c1c85..eb331b9c3 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -131,12 +131,14 @@ class FlxAtlasSprite extends FlxAnimate anim.play('', false, false); } } - - // Skip if the animation doesn't exist - if (!hasAnimation(id)) + else { - trace('Animation ' + id + ' not found'); - return; + // Skip if the animation doesn't exist + if (!hasAnimation(id)) + { + trace('Animation ' + id + ' not found'); + return; + } } anim.callback = function(_, frame:Int) { @@ -156,6 +158,10 @@ class FlxAtlasSprite extends FlxAnimate } }; + anim.onComplete = function() { + onAnimationFinish.dispatch(id); + }; + // Prevent other animations from playing if `ignoreOther` is true. if (ignoreOther) canPlayOtherAnims = false; diff --git a/source/funkin/graphics/shaders/MosaicEffect.hx b/source/funkin/graphics/shaders/MosaicEffect.hx new file mode 100644 index 000000000..fc3737aff --- /dev/null +++ b/source/funkin/graphics/shaders/MosaicEffect.hx @@ -0,0 +1,23 @@ +package funkin.graphics.shaders; + +import flixel.addons.display.FlxRuntimeShader; +import openfl.utils.Assets; +import funkin.Paths; +import flixel.math.FlxPoint; + +class MosaicEffect extends FlxRuntimeShader +{ + public var blockSize:FlxPoint = FlxPoint.get(1.0, 1.0); + + public function new() + { + super(Assets.getText(Paths.frag('mosaic'))); + setBlockSize(1.0, 1.0); + } + + public function setBlockSize(w:Float, h:Float) + { + blockSize.set(w, h); + setFloatArray("uBlocksize", [w, h]); + } +} diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx index f6c881f6d..6b8e9aa3e 100644 --- a/source/funkin/input/Controls.hx +++ b/source/funkin/input/Controls.hx @@ -64,6 +64,7 @@ class Controls extends FlxActionSet var _freeplay_favorite = new FunkinAction(Action.FREEPLAY_FAVORITE); var _freeplay_left = new FunkinAction(Action.FREEPLAY_LEFT); var _freeplay_right = new FunkinAction(Action.FREEPLAY_RIGHT); + var _freeplay_char_select = new FunkinAction(Action.FREEPLAY_CHAR_SELECT); var _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE); var _debug_menu = new FunkinAction(Action.DEBUG_MENU); var _debug_chart = new FunkinAction(Action.DEBUG_CHART); @@ -262,6 +263,11 @@ class Controls extends FlxActionSet inline function get_FREEPLAY_RIGHT() return _freeplay_right.check(); + public var FREEPLAY_CHAR_SELECT(get, never):Bool; + + inline function get_FREEPLAY_CHAR_SELECT() + return _freeplay_char_select.check(); + public var CUTSCENE_ADVANCE(get, never):Bool; inline function get_CUTSCENE_ADVANCE() @@ -318,6 +324,7 @@ class Controls extends FlxActionSet add(_freeplay_favorite); add(_freeplay_left); add(_freeplay_right); + add(_freeplay_char_select); add(_cutscene_advance); add(_debug_menu); add(_debug_chart); @@ -424,6 +431,7 @@ class Controls extends FlxActionSet case FREEPLAY_FAVORITE: _freeplay_favorite; case FREEPLAY_LEFT: _freeplay_left; case FREEPLAY_RIGHT: _freeplay_right; + case FREEPLAY_CHAR_SELECT: _freeplay_char_select; case CUTSCENE_ADVANCE: _cutscene_advance; case DEBUG_MENU: _debug_menu; case DEBUG_CHART: _debug_chart; @@ -500,6 +508,8 @@ class Controls extends FlxActionSet func(_freeplay_left, JUST_PRESSED); case FREEPLAY_RIGHT: func(_freeplay_right, JUST_PRESSED); + case FREEPLAY_CHAR_SELECT: + func(_freeplay_char_select, JUST_PRESSED); case CUTSCENE_ADVANCE: func(_cutscene_advance, JUST_PRESSED); case DEBUG_MENU: @@ -721,6 +731,7 @@ class Controls extends FlxActionSet bindKeys(Control.FREEPLAY_FAVORITE, getDefaultKeybinds(scheme, Control.FREEPLAY_FAVORITE)); bindKeys(Control.FREEPLAY_LEFT, getDefaultKeybinds(scheme, Control.FREEPLAY_LEFT)); bindKeys(Control.FREEPLAY_RIGHT, getDefaultKeybinds(scheme, Control.FREEPLAY_RIGHT)); + bindKeys(Control.FREEPLAY_CHAR_SELECT, getDefaultKeybinds(scheme, Control.FREEPLAY_CHAR_SELECT)); bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE)); bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU)); bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART)); @@ -756,6 +767,7 @@ class Controls extends FlxActionSet case Control.FREEPLAY_FAVORITE: return [F]; // Favorite a song on the menu case Control.FREEPLAY_LEFT: return [Q]; // Switch tabs on the menu case Control.FREEPLAY_RIGHT: return [E]; // Switch tabs on the menu + case Control.FREEPLAY_CHAR_SELECT: return [TAB]; case Control.CUTSCENE_ADVANCE: return [Z, ENTER]; case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_CHART: return []; @@ -784,6 +796,7 @@ class Controls extends FlxActionSet case Control.FREEPLAY_FAVORITE: return [F]; // Favorite a song on the menu case Control.FREEPLAY_LEFT: return [Q]; // Switch tabs on the menu case Control.FREEPLAY_RIGHT: return [E]; // Switch tabs on the menu + case Control.FREEPLAY_CHAR_SELECT: return [TAB]; case Control.CUTSCENE_ADVANCE: return [G, Z]; case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_CHART: return []; @@ -812,6 +825,7 @@ class Controls extends FlxActionSet case Control.FREEPLAY_FAVORITE: return []; case Control.FREEPLAY_LEFT: return []; case Control.FREEPLAY_RIGHT: return []; + case Control.FREEPLAY_CHAR_SELECT: return []; case Control.CUTSCENE_ADVANCE: return [ENTER]; case Control.DEBUG_MENU: return []; case Control.DEBUG_CHART: return []; @@ -1548,6 +1562,7 @@ enum Control FREEPLAY_FAVORITE; FREEPLAY_LEFT; FREEPLAY_RIGHT; + FREEPLAY_CHAR_SELECT; // WINDOW WINDOW_SCREENSHOT; WINDOW_FULLSCREEN; @@ -1602,6 +1617,7 @@ enum abstract Action(String) to String from String var FREEPLAY_FAVORITE = "freeplay_favorite"; var FREEPLAY_LEFT = "freeplay_left"; var FREEPLAY_RIGHT = "freeplay_right"; + var FREEPLAY_CHAR_SELECT = "freeplay_char_select"; // VOLUME var VOLUME_UP = "volume_up"; var VOLUME_DOWN = "volume_down"; 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/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/PlayState.hx b/source/funkin/play/PlayState.hx index f55cef388..8d7d82aab 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -16,6 +16,7 @@ import flixel.tweens.FlxTween; import flixel.ui.FlxBar; import flixel.util.FlxColor; import flixel.util.FlxTimer; +import flixel.util.FlxStringUtil; import funkin.api.newgrounds.NGio; import funkin.audio.FunkinSound; import funkin.audio.VoicesGroup; @@ -1301,12 +1302,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 +1325,7 @@ class PlayState extends MusicBeatSubState else DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); } + #end super.onFocus(); } @@ -1327,12 +1335,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, @@ -1783,11 +1796,8 @@ class PlayState extends MusicBeatSubState opponentStrumline.zIndex = 1000; opponentStrumline.cameras = [camHUD]; - if (!PlayStatePlaylist.isStoryMode) - { - playerStrumline.fadeInArrows(); - opponentStrumline.fadeInArrows(); - } + playerStrumline.fadeInArrows(); + opponentStrumline.fadeInArrows(); } /** @@ -2051,7 +2061,9 @@ class PlayState extends MusicBeatSubState } else { - scoreText.text = 'Score:' + songScore; + // TODO: Add an option for this maybe? + var commaSeparated:Bool = true; + scoreText.text = 'Score: ${FlxStringUtil.formatMoney(songScore, false, commaSeparated)}'; } } @@ -2619,10 +2631,18 @@ class PlayState extends MusicBeatSubState { disableKeys = true; persistentUpdate = false; - FlxG.switchState(() -> new ChartEditorState( - { - targetSongId: currentSong.id, - })); + if (isChartingMode) + { + FlxG.sound.music?.pause(); + this.close(); + } + else + { + FlxG.switchState(() -> new ChartEditorState( + { + targetSongId: currentSong.id, + })); + } } #end @@ -3156,6 +3176,7 @@ class PlayState extends MusicBeatSubState storyMode: PlayStatePlaylist.isStoryMode, songId: currentChart.song.id, difficultyId: currentDifficulty, + characterId: currentChart.characters.player, title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), prevScoreData: prevScoreData, scoreData: diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index a2c5f7e62..c2d9d42b3 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -14,6 +14,9 @@ import flixel.math.FlxRect; import flixel.text.FlxBitmapText; import funkin.ui.freeplay.FreeplayScore; import flixel.text.FlxText; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.data.freeplay.player.PlayerData; +import funkin.ui.freeplay.charselect.PlayableCharacter; import flixel.util.FlxColor; import flixel.tweens.FlxEase; import funkin.graphics.FunkinCamera; @@ -55,14 +58,17 @@ class ResultState extends MusicBeatSubState final highscoreNew:FlxSprite; final score:ResultScore; - var bfPerfect:Null = null; - var heartsPerfect:Null = null; - var bfExcellent:Null = null; - var bfGreat:Null = null; - var gfGreat:Null = null; - var bfGood:Null = null; - var gfGood:Null = null; - var bfShit:Null = null; + var characterAtlasAnimations:Array< + { + sprite:FlxAtlasSprite, + delay:Float, + forceLoop:Bool + }> = []; + var characterSparrowAnimations:Array< + { + sprite:FunkinSprite, + delay:Float + }> = []; var rankBg:FunkinSprite; final cameraBG:FunkinCamera; @@ -157,118 +163,95 @@ class ResultState extends MusicBeatSubState soundSystem.zIndex = 1100; add(soundSystem); - switch (rank) + // Fetch playable character data. Default to BF on the results screen if we can't find it. + var playerCharacterId:Null = PlayerRegistry.instance.getCharacterOwnerId(params.characterId); + var playerCharacter:Null = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); + + trace('Got playable character: ${playerCharacter?.getName()}'); + // Query JSON data based on the rank, then use that to build the animation(s) the player sees. + var playerAnimationDatas:Array = playerCharacter != null ? playerCharacter.getResultsAnimationDatas(rank) : []; + + for (animData in playerAnimationDatas) { - case PERFECT | PERFECT_GOLD: - heartsPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT/hearts", "shared")); - heartsPerfect.visible = false; - heartsPerfect.zIndex = 501; - add(heartsPerfect); + if (animData == null) continue; - heartsPerfect.anim.onComplete = () -> { - if (heartsPerfect != null) + var animPath:String = Paths.stripLibrary(animData.assetPath); + var animLibrary:String = Paths.getLibrary(animData.assetPath); + var offsets = animData.offsets ?? [0, 0]; + switch (animData.renderType) + { + case 'animateatlas': + var animation:FlxAtlasSprite = new FlxAtlasSprite(offsets[0], offsets[1], Paths.animateAtlas(animPath, animLibrary)); + animation.zIndex = animData.zIndex ?? 500; + + animation.scale.set(animData.scale ?? 1.0, animData.scale ?? 1.0); + + if (!(animData.looped ?? true)) { - // bfPerfect.anim.curFrame = 137; - heartsPerfect.anim.curFrame = 43; - heartsPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + // Animation is not looped. + animation.onAnimationFinish.add((_name:String) -> { + if (animation != null) + { + animation.anim.pause(); + } + }); } - }; - - bfPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared")); - bfPerfect.visible = false; - bfPerfect.zIndex = 500; - add(bfPerfect); - - bfPerfect.anim.onComplete = () -> { - if (bfPerfect != null) + else if (animData.loopFrameLabel != null) { - // bfPerfect.anim.curFrame = 137; - bfPerfect.anim.curFrame = 137; - bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce! + animation.onAnimationFinish.add((_name:String) -> { + if (animation != null) + { + animation.playAnimation(animData.loopFrameLabel ?? ''); // unpauses this anim, since it's on PlayOnce! + } + }); } - }; - - case EXCELLENT: - bfExcellent = new FlxAtlasSprite(1329, 429, Paths.animateAtlas("resultScreen/results-bf/resultsEXCELLENT", "shared")); - bfExcellent.visible = false; - bfExcellent.zIndex = 500; - add(bfExcellent); - - bfExcellent.anim.onComplete = () -> { - if (bfExcellent != null) + else if (animData.loopFrame != null) { - bfExcellent.anim.curFrame = 28; - bfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce! + animation.onAnimationFinish.add((_name:String) -> { + if (animation != null) + { + animation.anim.curFrame = animData.loopFrame ?? 0; + animation.anim.play(); // unpauses this anim, since it's on PlayOnce! + } + }); } - }; - case GREAT: - gfGreat = new FlxAtlasSprite(802, 331, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/gf", "shared")); - gfGreat.visible = false; - gfGreat.zIndex = 499; - add(gfGreat); + // Hide until ready to play. + animation.visible = false; + // Queue to play. + characterAtlasAnimations.push( + { + sprite: animation, + delay: animData.delay ?? 0.0, + forceLoop: (animData.loopFrame ?? -1) == 0 + }); + // Add to the scene. + add(animation); + case 'sparrow': + var animation:FunkinSprite = FunkinSprite.createSparrow(offsets[0], offsets[1], animPath); + animation.animation.addByPrefix('idle', '', 24, false, false, false); - gfGreat.scale.set(0.93, 0.93); - - gfGreat.anim.onComplete = () -> { - if (gfGreat != null) + if (animData.loopFrame != null) { - gfGreat.anim.curFrame = 9; - gfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce! + animation.animation.finishCallback = (_name:String) -> { + if (animation != null) + { + animation.animation.play('idle', true, false, animData.loopFrame ?? 0); + } + } } - }; - bfGreat = new FlxAtlasSprite(929, 363, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/bf", "shared")); - bfGreat.visible = false; - bfGreat.zIndex = 500; - add(bfGreat); - - bfGreat.scale.set(0.93, 0.93); - - bfGreat.anim.onComplete = () -> { - if (bfGreat != null) - { - bfGreat.anim.curFrame = 15; - bfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce! - } - }; - - case GOOD: - gfGood = FunkinSprite.createSparrow(625, 325, 'resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD'); - gfGood.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false); - gfGood.visible = false; - gfGood.zIndex = 500; - gfGood.animation.finishCallback = _ -> { - if (gfGood != null) - { - gfGood.animation.play('clap', true, false, 9); - } - }; - add(gfGood); - - bfGood = FunkinSprite.createSparrow(640, -200, 'resultScreen/results-bf/resultsGOOD/resultBoyfriendGOOD'); - bfGood.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false); - bfGood.visible = false; - bfGood.zIndex = 501; - bfGood.animation.finishCallback = function(_) { - if (bfGood != null) - { - bfGood.animation.play('fall', true, false, 14); - } - }; - add(bfGood); - - case SHIT: - bfShit = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/results-bf/resultsSHIT", "shared")); - bfShit.visible = false; - bfShit.zIndex = 500; - add(bfShit); - bfShit.onAnimationFinish.add((animName) -> { - if (bfShit != null) - { - bfShit.playAnimation('Loop Start'); - } - }); + // Hide until ready to play. + animation.visible = false; + // Queue to play. + characterSparrowAnimations.push( + { + sprite: animation, + delay: animData.delay ?? 0.0 + }); + // Add to the scene. + add(animation); + } } var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}'; @@ -587,94 +570,22 @@ class ResultState extends MusicBeatSubState { showSmallClearPercent(); - switch (rank) + for (atlas in characterAtlasAnimations) { - case PERFECT | PERFECT_GOLD: - if (bfPerfect == null) - { - trace("Could not build PERFECT animation!"); - } - else - { - bfPerfect.visible = true; - bfPerfect.playAnimation(''); - } - new FlxTimer().start(106 / 24, _ -> { - if (heartsPerfect == null) - { - trace("Could not build heartsPerfect animation!"); - } - else - { - heartsPerfect.visible = true; - heartsPerfect.playAnimation(''); - } - }); - case EXCELLENT: - if (bfExcellent == null) - { - trace("Could not build EXCELLENT animation!"); - } - else - { - bfExcellent.visible = true; - bfExcellent.playAnimation(''); - } - case GREAT: - if (bfGreat == null) - { - trace("Could not build GREAT animation!"); - } - else - { - bfGreat.visible = true; - bfGreat.playAnimation(''); - } + new FlxTimer().start(atlas.delay, _ -> { + if (atlas.sprite == null) return; + atlas.sprite.visible = true; + atlas.sprite.playAnimation(''); + }); + } - new FlxTimer().start(6 / 24, _ -> { - if (gfGreat == null) - { - trace("Could not build GREAT animation for gf!"); - } - else - { - gfGreat.visible = true; - gfGreat.playAnimation(''); - } - }); - case SHIT: - if (bfShit == null) - { - trace("Could not build SHIT animation!"); - } - else - { - bfShit.visible = true; - bfShit.playAnimation('Intro'); - } - case GOOD: - if (bfGood == null) - { - trace("Could not build GOOD animation!"); - } - else - { - bfGood.animation.play('fall'); - bfGood.visible = true; - new FlxTimer().start((1 / 24) * 22, _ -> { - // plays about 22 frames (at 24fps timing) after bf spawns in - if (gfGood != null) - { - gfGood.animation.play('clap', true); - gfGood.visible = true; - } - else - { - trace("Could not build GOOD animation!"); - } - }); - } - default: + for (sprite in characterSparrowAnimations) + { + new FlxTimer().start(sprite.delay, _ -> { + if (sprite.sprite == null) return; + sprite.sprite.visible = true; + sprite.sprite.animation.play('idle', true); + }); } } @@ -776,52 +687,6 @@ class ResultState extends MusicBeatSubState // })); // } - // if(heartsPerfect != null){ - // if (FlxG.keys.justPressed.I) - // { - // heartsPerfect.y -= 1; - // trace(heartsPerfect.x, heartsPerfect.y); - // } - // if (FlxG.keys.justPressed.J) - // { - // heartsPerfect.x -= 1; - // trace(heartsPerfect.x, heartsPerfect.y); - // } - // if (FlxG.keys.justPressed.L) - // { - // heartsPerfect.x += 1; - // trace(heartsPerfect.x, heartsPerfect.y); - // } - // if (FlxG.keys.justPressed.K) - // { - // heartsPerfect.y += 1; - // trace(heartsPerfect.x, heartsPerfect.y); - // } - // } - - // if(bfGreat != null){ - // if (FlxG.keys.justPressed.W) - // { - // bfGreat.y -= 1; - // trace(bfGreat.x, bfGreat.y); - // } - // if (FlxG.keys.justPressed.A) - // { - // bfGreat.x -= 1; - // trace(bfGreat.x, bfGreat.y); - // } - // if (FlxG.keys.justPressed.D) - // { - // bfGreat.x += 1; - // trace(bfGreat.x, bfGreat.y); - // } - // if (FlxG.keys.justPressed.S) - // { - // bfGreat.y += 1; - // trace(bfGreat.x, bfGreat.y); - // } - // } - // maskShaderSongName.swagSprX = songName.x; maskShaderDifficulty.swagSprX = difficulty.x; @@ -922,12 +787,21 @@ typedef ResultsStateParams = var storyMode:Bool; /** + * A readable title for the song we just played. * Either "Song Name by Artist Name" or "Week Name" */ var title:String; + /** + * The internal song ID for the song we just played. + */ var songId:String; + /** + * The character ID for the song we just played. + */ + var characterId:String; + /** * Whether the displayed score is a new highscore */ diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 4ef86c6a9..c228d803a 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); @@ -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; @@ -308,13 +309,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 +382,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 +411,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 +442,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 +453,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 +534,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 +566,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/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/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]; diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 0e4b6645f..fdb32bb85 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -37,7 +37,7 @@ class Strumline extends FlxSpriteGroup static function get_RENDER_DISTANCE_MS():Float { - return FlxG.height / 0.45; + return FlxG.height / Constants.PIXELS_PER_MS; } /** @@ -598,7 +598,6 @@ class Strumline extends FlxSpriteGroup { note.holdNoteSprite.hitNote = true; note.holdNoteSprite.missedNote = false; - note.holdNoteSprite.alpha = 1.0; note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition; } diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index b358d7f03..f6d43b33f 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -160,7 +160,7 @@ class SustainTrail extends FlxSprite */ public static inline function sustainHeight(susLength:Float, scroll:Float) { - return (susLength * 0.45 * scroll); + return (susLength * Constants.PIXELS_PER_MS * scroll); } function set_sustainLength(s:Float):Float 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/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 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) {} 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/PixelatedIcon.hx b/source/funkin/ui/PixelatedIcon.hx new file mode 100644 index 000000000..8d9b97d9c --- /dev/null +++ b/source/funkin/ui/PixelatedIcon.hx @@ -0,0 +1,79 @@ +package funkin.ui; + +import flixel.FlxSprite; + +/** + * The icon that gets used for Freeplay capsules and char select + * NOT to be confused with the CharIcon class, which is for the in-game icons + */ +class PixelatedIcon extends FlxSprite +{ + public function new(x:Float, y:Float) + { + super(x, y); + this.makeGraphic(32, 32, 0x00000000); + this.antialiasing = false; + this.active = false; + } + + public function setCharacter(char:String):Void + { + var charPath:String = "freeplay/icons/"; + + switch (char) + { + case 'monster-christmas': + charPath += 'monsterpixel'; + case 'mom-car': + charPath += 'mommypixel'; + case 'darnell-blazin': + charPath += 'darnellpixel'; + case 'senpai-angry': + charPath += 'senpaipixel'; + default: + charPath += '${char}pixel'; + } + + if (!openfl.utils.Assets.exists(Paths.image(charPath))) + { + trace('[WARN] Character ${char} has no freeplay icon.'); + return; + } + + var isAnimated = openfl.utils.Assets.exists(Paths.file('images/$charPath.xml')); + + if (isAnimated) + { + this.frames = Paths.getSparrowAtlas(charPath); + } + else + { + this.loadGraphic(Paths.image(charPath)); + } + + this.scale.x = this.scale.y = 2; + + switch (char) + { + case 'parents-christmas': + this.origin.x = 140; + default: + this.origin.x = 100; + } + + if (isAnimated) + { + this.active = true; + this.animation.addByPrefix('idle', 'idle0', 10, true); + this.animation.addByPrefix('confirm', 'confirm0', 10, false); + this.animation.addByPrefix('confirm-hold', 'confirm-hold0', 10, true); + + this.animation.finishCallback = function(name:String):Void { + trace('Finish pixel animation: ${name}'); + if (name == 'confirm') this.animation.play('confirm-hold'); + }; + + this.animation.play('idle'); + } + } +} diff --git a/source/funkin/ui/charSelect/CharIcon.hx b/source/funkin/ui/charSelect/CharIcon.hx new file mode 100644 index 000000000..6d6274286 --- /dev/null +++ b/source/funkin/ui/charSelect/CharIcon.hx @@ -0,0 +1,17 @@ +package funkin.ui.charSelect; + +import flixel.FlxSprite; + +class CharIcon extends FlxSprite +{ + public var locked:Bool = false; + + public function new(x:Float, y:Float, locked:Bool = false) + { + super(x, y); + + this.locked = locked; + + makeGraphic(128, 128); + } +} diff --git a/source/funkin/ui/charSelect/CharIconCharacter.hx b/source/funkin/ui/charSelect/CharIconCharacter.hx new file mode 100644 index 000000000..7f7b5c212 --- /dev/null +++ b/source/funkin/ui/charSelect/CharIconCharacter.hx @@ -0,0 +1,49 @@ +package funkin.ui.charSelect; + +import openfl.display.BitmapData; +import openfl.filters.DropShadowFilter; +import openfl.filters.ConvolutionFilter; +import funkin.graphics.shaders.StrokeShader; + +class CharIconCharacter extends CharIcon +{ + public var dropShadowFilter:DropShadowFilter; + + var matrixFilter:Array = [ + 1, 1, 1, + 1, 1, 1, + 1, 1, 1 + ]; + + var divisor:Int = 1; + var bias:Int = 0; + var convolutionFilter:ConvolutionFilter; + + public var noDropShadow:BitmapData; + public var withDropShadow:BitmapData; + + var strokeShader:StrokeShader; + + public function new(path:String) + { + super(0, 0, false); + + loadGraphic(Paths.image('freeplay/icons/' + path + 'pixel')); + setGraphicSize(128, 128); + updateHitbox(); + antialiasing = false; + + strokeShader = new StrokeShader(); + // shader = strokeShader; + + // noDropShadow = pixels.clone(); + + // dropShadowFilter = new DropShadowFilter(5, 45, 0, 1, 0, 0); + // convolutionFilter = new ConvolutionFilter(3, 3, matrixFilter, divisor, bias); + // pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), dropShadowFilter); + // pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), convolutionFilter); + // withDropShadow = pixels.clone(); + + // pixels = noDropShadow.clone(); + } +} diff --git a/source/funkin/ui/charSelect/CharIconLocked.hx b/source/funkin/ui/charSelect/CharIconLocked.hx new file mode 100644 index 000000000..dbe84a6ce --- /dev/null +++ b/source/funkin/ui/charSelect/CharIconLocked.hx @@ -0,0 +1,3 @@ +package funkin.ui.charSelect; + +class CharIconLocked extends CharIcon {} diff --git a/source/funkin/ui/charSelect/CharSelectGF.hx b/source/funkin/ui/charSelect/CharSelectGF.hx new file mode 100644 index 000000000..6d8e3e657 --- /dev/null +++ b/source/funkin/ui/charSelect/CharSelectGF.hx @@ -0,0 +1,137 @@ +package funkin.ui.charSelect; + +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import flixel.tweens.FlxTween; +import flixel.tweens.FlxEase; +import flixel.math.FlxMath; +import funkin.util.FramesJSFLParser; +import funkin.util.FramesJSFLParser.FramesJSFLInfo; +import funkin.util.FramesJSFLParser.FramesJSFLFrame; +import flixel.math.FlxMath; + +class CharSelectGF extends FlxAtlasSprite +{ + var fadeTimer:Float = 0; + var fadingStatus:FadeStatus = OFF; + var fadeAnimIndex:Int = 0; + + var animInInfo:FramesJSFLInfo; + var animOutInfo:FramesJSFLInfo; + + var intendedYPos:Float = 0; + var intendedAlpha:Float = 0; + + public function new() + { + super(0, 0, Paths.animateAtlas("charSelect/gfChill")); + anim.play(""); + switchGF("bf"); + } + + override public function update(elapsed:Float) + { + super.update(elapsed); + + switch (fadingStatus) + { + case OFF: + // do nothing if it's off! + // or maybe force position to be 0,0? + // maybe reset timers? + resetFadeAnimParams(); + case FADE_OUT: + doFade(animOutInfo); + case FADE_IN: + doFade(animInInfo); + default: + } + + if (FlxG.keys.justPressed.J) + { + alpha = 1; + x = y = 0; + fadingStatus = FADE_OUT; + } + if (FlxG.keys.justPressed.K) + { + alpha = 0; + fadingStatus = FADE_IN; + } + } + + /** + * @param animInfo Should not be confused with animInInfo! + * This is merely a local var for the function! + */ + function doFade(animInfo:FramesJSFLInfo) + { + fadeTimer += FlxG.elapsed; + if (fadeTimer >= 1 / 24) + { + fadeTimer = 0; + // only inc the index for the first frame, used for reference of where to "start" + if (fadeAnimIndex == 0) + { + fadeAnimIndex++; + return; + } + + var curFrame:FramesJSFLFrame = animInfo.frames[fadeAnimIndex]; + var prevFrame:FramesJSFLFrame = animInfo.frames[fadeAnimIndex - 1]; + + var xDiff:Float = curFrame.x - prevFrame.x; + var yDiff:Float = curFrame.y - prevFrame.y; + var alphaDiff:Float = curFrame.alpha - prevFrame.alpha; + alphaDiff /= 100; // flash exports alpha as a whole number + + alpha += alphaDiff; + alpha = FlxMath.bound(alpha, 0, 1); + x += xDiff; + y += yDiff; + + fadeAnimIndex++; + } + + if (fadeAnimIndex >= animInfo.frames.length) fadingStatus = OFF; + } + + function resetFadeAnimParams() + { + fadeTimer = 0; + fadeAnimIndex = 0; + } + + public function switchGF(str:String) + { + str = switch (str) + { + case "pico": + "nene"; + case "bf": + "gf"; + default: + "gf"; + } + + switch str + { + default: + loadAtlas(Paths.animateAtlas("charSelect/" + str + "Chill")); + } + + animInInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + str + "AnimInfo/" + str + "In.txt")); + animOutInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + str + "AnimInfo/" + str + "Out.txt")); + + anim.play(""); + playAnimation("idle", true, false, true); + + updateHitbox(); + } +} + +enum FadeStatus +{ + OFF; + FADE_OUT; + FADE_IN; +} diff --git a/source/funkin/ui/charSelect/CharSelectPlayer.hx b/source/funkin/ui/charSelect/CharSelectPlayer.hx new file mode 100644 index 000000000..9322369ba --- /dev/null +++ b/source/funkin/ui/charSelect/CharSelectPlayer.hx @@ -0,0 +1,58 @@ +package funkin.ui.charSelect; + +import flixel.FlxSprite; +import funkin.graphics.adobeanimate.FlxAtlasSprite; + +class CharSelectPlayer extends FlxAtlasSprite +{ + public function new(x:Float, y:Float) + { + super(x, y, Paths.animateAtlas("charSelect/bfChill")); + + onAnimationFinish.add(function(animLabel:String) { + switch (animLabel) + { + case "slidein": + if (hasAnimation("slidein idle point")) playAnimation("slidein idle point", true, false, false); + else + playAnimation("idle", true, false, true); + case "slidein idle point": + playAnimation("idle", true, false, true); + case "select": + anim.pause(); + case "deselect": + playAnimation("deselect loop start", true, false, true); + } + }); + } + + public function updatePosition(str:String) + { + switch (str) + { + case "bf": + x = 0; + y = 0; + case "pico": + x = 0; + y = 0; + case "random": + } + } + + public function switchChar(str:String) + { + switch str + { + default: + loadAtlas(Paths.animateAtlas("charSelect/" + str + "Chill")); + } + + anim.play(""); + playAnimation("slidein", true, false, false); + + updateHitbox(); + + updatePosition(str); + } +} diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx new file mode 100644 index 000000000..14a5b36e0 --- /dev/null +++ b/source/funkin/ui/charSelect/CharSelectSubState.hx @@ -0,0 +1,632 @@ +package funkin.ui.charSelect; + +import funkin.ui.freeplay.FreeplayState; +import flixel.text.FlxText; +import funkin.ui.PixelatedIcon; +import flixel.system.debug.watch.Tracker.TrackerProfile; +import flixel.math.FlxPoint; +import flixel.tweens.FlxTween; +import openfl.display.BlendMode; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.FlxSprite; +import flixel.group.FlxSpriteGroup; +import funkin.play.stage.Stage; +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEventDispatcher; +import funkin.graphics.adobeanimate.FlxAtlasSprite; +import flixel.FlxObject; +import openfl.display.BlendMode; +import flixel.group.FlxGroup; +import funkin.util.MathUtil; +import flixel.util.FlxTimer; +import flixel.tweens.FlxEase; +import flixel.sound.FlxSound; +import funkin.audio.FunkinSound; + +class CharSelectSubState extends MusicBeatSubState +{ + var cursor:FlxSprite; + var cursorBlue:FlxSprite; + var cursorDarkBlue:FlxSprite; + + var grpCursors:FlxTypedGroup; + + var cursorConfirmed:FlxSprite; + var cursorDenied:FlxSprite; + + var cursorX:Int = 0; + var cursorY:Int = 0; + + var cursorFactor:Float = 110; + var cursorOffsetX:Float = -16; + var cursorOffsetY:Float = -48; + + var cursorLocIntended:FlxPoint = new FlxPoint(0, 0); + var lerpAmnt:Float = 0.95; + + var tmrFrames:Int = 60; + + var currentStage:Stage; + + var playerChill:CharSelectPlayer; + var playerChillOut:CharSelectPlayer; + var gfChill:CharSelectGF; + var gfChillOut:CharSelectGF; + + var curChar(default, set):String = "pico"; + var nametag:Nametag; + var camFollow:FlxObject; + + var availableChars:Map = new Map(); + var pressedSelect:Bool = false; + + var selectTimer:FlxTimer = new FlxTimer(); + var selectSound:FunkinSound; + + public function new() + { + super(); + + availableChars.set(4, "bf"); + availableChars.set(3, "pico"); + } + + override public function create():Void + { + super.create(); + + selectSound = new FunkinSound(); + selectSound.loadEmbedded(Paths.sound('CS_select')); + selectSound.pitch = 1; + selectSound.volume = 0.7; + FlxG.sound.defaultSoundGroup.add(selectSound); + + // playing it here to preload it. not doing this makes a super awkward pause at the end of the intro + // TODO: probably make an intro thing for funkinSound itself that preloads the next audio? + FunkinSound.playMusic('stayFunky', + { + startingVolume: 0, + overrideExisting: true, + restartTrack: true + }); + var introMusic:String = Paths.music('stayFunky/stayFunky-intro'); + FunkinSound.load(introMusic, 1.0, false, true, true, () -> { + FunkinSound.playMusic('stayFunky', + { + startingVolume: 1, + overrideExisting: true, + restartTrack: true + }); + }); + + var bg:FlxSprite = new FlxSprite(-153, -140); + bg.loadGraphic(Paths.image('charSelect/charSelectBG')); + bg.scrollFactor.set(0.1, 0.1); + add(bg); + + var crowd:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/crowd")); + crowd.anim.play(""); + crowd.scrollFactor.set(0.3, 0.3); + add(crowd); + + var stageSpr:FlxSprite = new FlxSprite(-40, 391); + stageSpr.frames = Paths.getSparrowAtlas("charSelect/charSelectStage"); + stageSpr.animation.addByPrefix("idle", "stage", 24, true); + stageSpr.animation.play("idle"); + add(stageSpr); + + var curtains:FlxSprite = new FlxSprite(-47, -49); + curtains.loadGraphic(Paths.image('charSelect/curtains')); + curtains.scrollFactor.set(1.4, 1.4); + add(curtains); + + var barthing:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/barThing")); + barthing.anim.play(""); + barthing.blend = BlendMode.MULTIPLY; + barthing.scrollFactor.set(0, 0); + add(barthing); + + var charLight:FlxSprite = new FlxSprite(800, 250); + charLight.loadGraphic(Paths.image('charSelect/charLight')); + add(charLight); + + var charLightGF:FlxSprite = new FlxSprite(180, 240); + charLightGF.loadGraphic(Paths.image('charSelect/charLight')); + add(charLightGF); + + gfChill = new CharSelectGF(); + gfChill.switchGF("bf"); + add(gfChill); + + playerChill = new CharSelectPlayer(0, 0); + playerChill.switchChar("bf"); + add(playerChill); + + playerChillOut = new CharSelectPlayer(0, 0); + playerChillOut.switchChar("bf"); + add(playerChillOut); + + var speakers:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/charSelectSpeakers")); + speakers.anim.play(""); + speakers.scrollFactor.set(1.8, 1.8); + add(speakers); + + var fgBlur:FlxSprite = new FlxSprite(-125, 170); + fgBlur.loadGraphic(Paths.image('charSelect/foregroundBlur')); + fgBlur.blend = openfl.display.BlendMode.MULTIPLY; + add(fgBlur); + + var dipshitBlur:FlxSprite = new FlxSprite(419, -65); + dipshitBlur.frames = Paths.getSparrowAtlas("charSelect/dipshitBlur"); + dipshitBlur.animation.addByPrefix('idle', "CHOOSE vertical", 24, true); + dipshitBlur.blend = BlendMode.ADD; + dipshitBlur.animation.play("idle"); + add(dipshitBlur); + + var dipshitBacking:FlxSprite = new FlxSprite(423, -17); + dipshitBacking.frames = Paths.getSparrowAtlas("charSelect/dipshitBacking"); + dipshitBacking.animation.addByPrefix('idle', "CHOOSE horizontal", 24, true); + dipshitBacking.blend = BlendMode.ADD; + dipshitBacking.animation.play("idle"); + add(dipshitBacking); + + var chooseDipshit:FlxSprite = new FlxSprite(426, -13); + chooseDipshit.loadGraphic(Paths.image('charSelect/chooseDipshit')); + add(chooseDipshit); + + chooseDipshit.scrollFactor.set(); + dipshitBacking.scrollFactor.set(); + dipshitBlur.scrollFactor.set(); + + nametag = new Nametag(); + add(nametag); + + nametag.scrollFactor.set(); + + FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSprite, ["x", "y", "alpha", "scale", "blend"])); + FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxAtlasSprite, ["x", "y"])); + FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSound, ["pitch", "volume"])); + + // FlxG.debugger.track(crowd); + // FlxG.debugger.track(stageSpr, "stageSpr"); + // FlxG.debugger.track(bfChill, "bf chill"); + // FlxG.debugger.track(playerChill, "player"); + // FlxG.debugger.track(nametag, "nametag"); + FlxG.debugger.track(selectSound, "selectSound"); + // FlxG.debugger.track(chooseDipshit, "choose dipshit"); + // FlxG.debugger.track(barthing, "barthing"); + // FlxG.debugger.track(fgBlur, "fgBlur"); + // FlxG.debugger.track(dipshitBlur, "dipshitBlur"); + // FlxG.debugger.track(dipshitBacking, "dipshitBacking"); + // FlxG.debugger.track(charLightGF, "charLight"); + // FlxG.debugger.track(gfChill, "gfChill"); + + grpCursors = new FlxTypedGroup(); + add(grpCursors); + + cursor = new FlxSprite(0, 0); + cursor.loadGraphic(Paths.image('charSelect/charSelector')); + cursor.color = 0xFFFFFF00; + + // FFCC00 + + cursorBlue = new FlxSprite(0, 0); + cursorBlue.loadGraphic(Paths.image('charSelect/charSelector')); + cursorBlue.color = 0xFF3EBBFF; + + cursorDarkBlue = new FlxSprite(0, 0); + cursorDarkBlue.loadGraphic(Paths.image('charSelect/charSelector')); + cursorDarkBlue.color = 0xFF3C74F7; + + cursorBlue.blend = BlendMode.SCREEN; + cursorDarkBlue.blend = BlendMode.SCREEN; + + cursorConfirmed = new FlxSprite(0, 0); + cursorConfirmed.scrollFactor.set(); + cursorConfirmed.frames = Paths.getSparrowAtlas("charSelect/charSelectorConfirm"); + cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED", 24, true); + cursorConfirmed.visible = false; + add(cursorConfirmed); + + cursorDenied = new FlxSprite(0, 0); + cursorDenied.scrollFactor.set(); + cursorDenied.frames = Paths.getSparrowAtlas("charSelect/charSelectorDenied"); + cursorDenied.animation.addByPrefix("idle", "cursor DENIED", 24, false); + cursorDenied.visible = false; + add(cursorDenied); + + grpCursors.add(cursorDarkBlue); + grpCursors.add(cursorBlue); + grpCursors.add(cursor); + + initLocks(); + + cursor.scrollFactor.set(); + cursorBlue.scrollFactor.set(); + cursorDarkBlue.scrollFactor.set(); + + FlxTween.color(cursor, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: FlxTween.PINGPONG}); + + // FlxG.debugger.track(cursor); + + FlxG.debugger.addTrackerProfile(new TrackerProfile(CharSelectSubState, ["curChar", "grpXSpread", "grpYSpread"])); + FlxG.debugger.track(this); + + FlxG.sound.playMusic(Paths.music('charSelect/charSelectMusic')); + + camFollow = new FlxObject(0, 0, 1, 1); + add(camFollow); + camFollow.screenCenter(); + + FlxG.camera.follow(camFollow, LOCKON, 0.01); + + var temp:FlxSprite = new FlxSprite(); + temp.loadGraphic(Paths.image('charSelect/placement')); + add(temp); + temp.alpha = 0.0; + Conductor.stepHit.add(spamOnStep); + // FlxG.debugger.track(temp, "tempBG"); + } + + var grpIcons:FlxSpriteGroup; + + var grpXSpread(default, set):Float = 107; + var grpYSpread(default, set):Float = 127; + + function initLocks() + { + grpIcons = new FlxSpriteGroup(); + add(grpIcons); + + FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSpriteGroup, ["x", "y"])); + // FlxG.debugger.track(grpIcons, "iconGrp"); + + for (i in 0...9) + { + if (availableChars.exists(i)) + { + var path:String = availableChars.get(i); + var temp:PixelatedIcon = new PixelatedIcon(0, 0); + temp.setCharacter(path); + temp.setGraphicSize(128, 128); + temp.updateHitbox(); + temp.ID = 0; + grpIcons.add(temp); + } + else + { + var temp:FlxSprite = new FlxSprite(); + temp.ID = 1; + temp.frames = Paths.getSparrowAtlas("charSelect/locks"); + + var lockIndex:Int = i + 1; + + if (i == 3) lockIndex = 3; + + if (i >= 4) lockIndex = i - 2; + + temp.animation.addByIndices("idle", "LOCK FULL " + lockIndex + " instance 1", [0], "", 24); + temp.animation.addByIndices("selected", "LOCK FULL " + lockIndex + " instance 1", [3, 4, 5], "", 24, false); + temp.animation.addByIndices("clicked", "LOCK FULL " + lockIndex + " instance 1", [9, 10, 11, 12, 13, 14, 15], "", 24, false); + + temp.animation.play("idle"); + + grpIcons.add(temp); + } + } + + updateIconPositions(); + + grpIcons.scrollFactor.set(); + } + + function updateIconPositions() + { + grpIcons.x = 450; + grpIcons.y = 120; + for (index => member in grpIcons.members) + { + var posX:Float = (index % 3); + var posY:Float = Math.floor(index / 3); + + member.x = posX * grpXSpread; + member.y = posY * grpYSpread; + + member.x += grpIcons.x; + member.y += grpIcons.y; + } + } + + var holdTmrUp:Float = 0; + var holdTmrDown:Float = 0; + var holdTmrLeft:Float = 0; + var holdTmrRight:Float = 0; + var spamUp:Bool = false; + var spamDown:Bool = false; + var spamLeft:Bool = false; + var spamRight:Bool = false; + + override public function update(elapsed:Float):Void + { + super.update(elapsed); + + Conductor.instance.update(); + + if (controls.UI_UP_R || controls.UI_DOWN_R || controls.UI_LEFT_R || controls.UI_RIGHT_R) selectSound.pitch = 1; + + if (controls.UI_UP) holdTmrUp += elapsed; + if (controls.UI_UP_R) + { + holdTmrUp = 0; + spamUp = false; + } + + if (controls.UI_DOWN) holdTmrDown += elapsed; + if (controls.UI_DOWN_R) + { + holdTmrDown = 0; + spamDown = false; + } + + if (controls.UI_LEFT) holdTmrLeft += elapsed; + if (controls.UI_LEFT_R) + { + holdTmrLeft = 0; + spamLeft = false; + } + + if (controls.UI_RIGHT) holdTmrRight += elapsed; + if (controls.UI_RIGHT_R) + { + holdTmrRight = 0; + spamRight = false; + } + + var initSpam = 0.5; + + if (holdTmrUp >= initSpam) spamUp = true; + if (holdTmrDown >= initSpam) spamDown = true; + if (holdTmrLeft >= initSpam) spamLeft = true; + if (holdTmrRight >= initSpam) spamRight = true; + + if (controls.UI_UP_P) + { + cursorY -= 1; + holdTmrUp = 0; + + selectSound.play(true); + } + if (controls.UI_DOWN_P) + { + cursorY += 1; + holdTmrDown = 0; + selectSound.play(true); + } + if (controls.UI_LEFT_P) + { + cursorX -= 1; + holdTmrLeft = 0; + selectSound.play(true); + } + if (controls.UI_RIGHT_P) + { + cursorX += 1; + holdTmrRight = 0; + selectSound.play(true); + } + + if (cursorX < -1) + { + cursorX = 1; + } + if (cursorX > 1) + { + cursorX = -1; + } + if (cursorY < -1) + { + cursorY = 1; + } + if (cursorY > 1) + { + cursorY = -1; + } + + if (availableChars.exists(getCurrentSelected())) + { + curChar = availableChars.get(getCurrentSelected()); + + if (controls.ACCEPT) + { + cursorConfirmed.visible = true; + cursorConfirmed.x = cursor.x - 2; + cursorConfirmed.y = cursor.y - 4; + cursorConfirmed.animation.play("idle", true); + + grpCursors.visible = false; + + FlxG.sound.play(Paths.sound('CS_confirm')); + + FlxTween.tween(FlxG.sound.music, {pitch: 0.1}, 1.5, {ease: FlxEase.quadInOut}); + playerChill.playAnimation("select"); + pressedSelect = true; + selectTimer.start(1.5, (_) -> { + pressedSelect = false; + FlxG.switchState(FreeplayState.build( + { + { + character: curChar + } + })); + }); + } + + if (pressedSelect && controls.BACK) + { + cursorConfirmed.visible = false; + grpCursors.visible = true; + + FlxTween.globalManager.cancelTweensOf(FlxG.sound.music); + FlxTween.tween(FlxG.sound.music, {pitch: 1.0}, 1, {ease: FlxEase.quartInOut}); + playerChill.playAnimation("deselect"); + pressedSelect = false; + selectTimer.cancel(); + } + } + else + { + curChar = "locked"; + + if (controls.ACCEPT) + { + cursorDenied.visible = true; + cursorDenied.x = cursor.x - 2; + cursorDenied.y = cursor.y - 4; + cursorDenied.animation.play("idle", true); + cursorDenied.animation.finishCallback = (_) -> { + cursorDenied.visible = false; + }; + } + } + + updateLockAnims(); + + camFollow.screenCenter(); + camFollow.x += cursorX * 10; + camFollow.y += cursorY * 10; + + cursorLocIntended.x = (cursorFactor * cursorX) + (FlxG.width / 2) - cursor.width / 2; + cursorLocIntended.y = (cursorFactor * cursorY) + (FlxG.height / 2) - cursor.height / 2; + + cursorLocIntended.x += cursorOffsetX; + cursorLocIntended.y += cursorOffsetY; + + cursor.x = MathUtil.coolLerp(cursor.x, cursorLocIntended.x, lerpAmnt); + cursor.y = MathUtil.coolLerp(cursor.y, cursorLocIntended.y, lerpAmnt); + + cursorBlue.x = MathUtil.coolLerp(cursorBlue.x, cursor.x, lerpAmnt * 0.4); + cursorBlue.y = MathUtil.coolLerp(cursorBlue.y, cursor.y, lerpAmnt * 0.4); + + cursorDarkBlue.x = MathUtil.coolLerp(cursorDarkBlue.x, cursorLocIntended.x, lerpAmnt * 0.2); + cursorDarkBlue.y = MathUtil.coolLerp(cursorDarkBlue.y, cursorLocIntended.y, lerpAmnt * 0.2); + } + + function spamOnStep():Void + { + if (spamUp || spamDown || spamLeft || spamRight) + { + // selectSound.changePitchBySemitone(1); + if (selectSound.pitch > 5) selectSound.pitch = 5; + selectSound.play(true); + + if (spamUp) + { + cursorY -= 1; + holdTmrUp = 0; + } + if (spamDown) + { + cursorY += 1; + holdTmrDown = 0; + } + if (spamLeft) + { + cursorX -= 1; + holdTmrLeft = 0; + } + if (spamRight) + { + cursorX += 1; + holdTmrRight = 0; + } + } + } + + private function updateLockAnims():Void + { + for (index => member in grpIcons.group.members) + { + switch (member.ID) + { + case 1: + if (index == getCurrentSelected()) + { + switch (member.animation.curAnim.name) + { + case "idle": + member.animation.play("selected"); + case "selected" | "clicked": + if (controls.ACCEPT) member.animation.play("clicked", true); + } + } + else + { + member.animation.play("idle"); + } + case 0: + var memb:PixelatedIcon = cast member; + + if (index == getCurrentSelected()) + { + // memb.pixels = memb.withDropShadow.clone(); + memb.scale.set(2.6, 2.6); + + if (controls.ACCEPT) memb.animation.play("confirm"); + } + else + { + // memb.pixels = memb.noDropShadow.clone(); + memb.scale.set(2, 2); + } + } + } + } + + function getCurrentSelected():Int + { + var tempX:Int = cursorX + 1; + var tempY:Int = cursorY + 1; + var gridPosition:Int = tempX + tempY * 3; + return gridPosition; + } + + function set_curChar(value:String):String + { + if (curChar == value) return value; + + curChar = value; + + nametag.switchChar(value); + playerChill.visible = false; + playerChillOut.visible = true; + playerChillOut.anim.goToFrameLabel("slideout"); + playerChillOut.anim.callback = (_, frame:Int) -> { + if (frame == playerChillOut.anim.getFrameLabel("slideout").index + 1) + { + playerChill.visible = true; + playerChill.switchChar(value); + gfChill.switchGF(value); + } + if (frame == playerChillOut.anim.getFrameLabel("slideout").index + 2) + { + playerChillOut.switchChar(value); + playerChillOut.visible = false; + } + }; + return value; + } + + function set_grpXSpread(value:Float):Float + { + grpXSpread = value; + updateIconPositions(); + return value; + } + + function set_grpYSpread(value:Float):Float + { + grpYSpread = value; + updateIconPositions(); + return value; + } +} diff --git a/source/funkin/ui/charSelect/Nametag.hx b/source/funkin/ui/charSelect/Nametag.hx new file mode 100644 index 000000000..b6cedb0c7 --- /dev/null +++ b/source/funkin/ui/charSelect/Nametag.hx @@ -0,0 +1,101 @@ +package funkin.ui.charSelect; + +import flixel.FlxSprite; +import funkin.graphics.shaders.MosaicEffect; +import flixel.util.FlxTimer; + +class Nametag extends FlxSprite +{ + var midpointX(default, set):Float = 1008; + var midpointY(default, set):Float = 100; + var mosaicShader:MosaicEffect; + + public function new(?x:Float = 0, ?y:Float = 0) + { + super(x, y); + + mosaicShader = new MosaicEffect(); + shader = mosaicShader; + + switchChar("bf"); + + FlxG.debugger.addTrackerProfile(new TrackerProfile(Nametag, ["midpointX", "midpointY"])); + FlxG.debugger.track(this, "Nametag"); + } + + public function updatePosition():Void + { + var offsetX:Float = getMidpoint().x - midpointX; + var offsetY:Float = getMidpoint().y - midpointY; + + x -= offsetX; + y -= offsetY; + } + + public function switchChar(str:String):Void + { + shaderEffect(); + + new FlxTimer().start(4 / 30, _ -> { + var path:String = str; + switch str + { + case "bf": + path = "boyfriend"; + } + + loadGraphic(Paths.image('charSelect/' + path + "Nametag")); + updateHitbox(); + scale.x = scale.y = 0.77; + + updatePosition(); + shaderEffect(true); + }); + } + + function shaderEffect(fadeOut:Bool = false):Void + { + if (fadeOut) + { + setBlockTimer(0, 1, 1); + setBlockTimer(1, width / 27, height / 26); + setBlockTimer(2, width / 10, height / 10); + + setBlockTimer(3, 1, 1); + } + else + { + setBlockTimer(0, (width / 10), (height / 10)); + setBlockTimer(1, width / 73, height / 6); + setBlockTimer(2, width / 10, height / 10); + } + } + + function setBlockTimer(frame:Int, ?forceX:Float, ?forceY:Float) + { + var daX:Float = 10 * FlxG.random.int(1, 4); + var daY:Float = 10 * FlxG.random.int(1, 4); + + if (forceX != null) daX = forceX; + + if (forceY != null) daY = forceY; + + new FlxTimer().start(frame / 30, _ -> { + mosaicShader.setBlockSize(daX, daY); + }); + } + + function set_midpointX(val:Float):Float + { + this.midpointX = val; + updatePosition(); + return val; + } + + function set_midpointY(val:Float):Float + { + this.midpointY = val; + updatePosition(); + return val; + } +} diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx index f8b1be9d2..590cce88b 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -54,8 +54,11 @@ class DebugMenuSubState extends MusicBeatSubState // Create each menu item. // Call onMenuChange when the first item is created to move the camera . + #if CHART_EDITOR_SUPPORTED onMenuChange(createItem("CHART EDITOR", openChartEditor)); + #end // createItem("Input Offset Testing", openInputOffsetTesting); + createItem("CHARACTER SELECT", openCharSelect, true); createItem("ANIMATION EDITOR", openAnimationEditor); // createItem("STAGE EDITOR", openStageEditor); // createItem("TEST STICKERS", testStickers); @@ -102,6 +105,11 @@ class DebugMenuSubState extends MusicBeatSubState trace('Input Offset Testing'); } + function openCharSelect() + { + FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); + } + function openAnimationEditor() { FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState()); diff --git a/source/funkin/ui/debug/MemoryCounter.hx b/source/funkin/ui/debug/MemoryCounter.hx index b25b55645..50421f398 100644 --- a/source/funkin/ui/debug/MemoryCounter.hx +++ b/source/funkin/ui/debug/MemoryCounter.hx @@ -36,7 +36,7 @@ class MemoryCounter extends TextField @:noCompletion #if !flash override #end function __enterFrame(deltaTime:Float):Void { - var mem:Float = Math.round(MemoryUtil.getMemoryUsed() / BYTES_PER_MEG / ROUND_TO) * ROUND_TO; + var mem:Float = Math.fround(MemoryUtil.getMemoryUsed() / BYTES_PER_MEG / ROUND_TO) * ROUND_TO; if (mem > memPeak) memPeak = mem; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index b84c68f8d..ab13da1d9 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -808,8 +808,11 @@ class ChartEditorDialogHandler } songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel); #if FILE_DROP_SUPPORTED - state.addDropHandler({component: songVariationMetadataEntry, handler: onDropFileMetadataVariation.bind(variation) - .bind(songVariationMetadataEntryLabel)}); + state.addDropHandler( + { + component: songVariationMetadataEntry, + handler: onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel) + }); #end chartContainerB.addComponent(songVariationMetadataEntry); 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..72eddd0ca --- /dev/null +++ b/source/funkin/ui/freeplay/FreeplayDJ.hx @@ -0,0 +1,373 @@ +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 (animPrefix == null) { + currentState = IdleEasterEgg; + } else { + 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.getAnimationOffsetsByPrefix(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 ($AnimName): ' + xValue + ', ' + yValue); + offset.set(xValue, yValue); + } + else + { + trace('No offset found ($AnimName), 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 68c63efc4..dc42bd651 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1,52 +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.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 openfl.display.BlendMode; /** * Parameters used to initialize the FreeplayState. @@ -92,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 { // @@ -102,7 +106,9 @@ class FreeplayState extends MusicBeatSubState * The current character for this FreeplayState. * You can't change this without transitioning to a new FreeplayState. */ - final currentCharacter:String; + final currentCharacterId:String; + + final currentCharacter:PlayableCharacter; /** * For the audio preview, the duration of the fade-in effect. @@ -160,10 +166,9 @@ class FreeplayState extends MusicBeatSubState var grpSongs:FlxTypedGroup; var grpCapsules:FlxTypedGroup; - var curCapsule:SongMenuItem; var curPlaying:Bool = false; - var dj:DJBoyfriend; + var dj:Null = null; var ostName:FlxText; var albumRoll:AlbumRoll; @@ -171,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'; @@ -205,7 +210,13 @@ class FreeplayState extends MusicBeatSubState public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { - currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER; + currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER; + 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; @@ -214,12 +225,54 @@ class FreeplayState extends MusicBeatSubState prepForNewRank = true; } - if (stickers != null) + 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 @@ -230,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; @@ -271,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) { @@ -281,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) { @@ -290,11 +337,10 @@ class FreeplayState extends MusicBeatSubState } // Only display songs which actually have available difficulties for the current character. - var displayedVariations = song.getVariationsByCharId(currentCharacter); - trace(songId); - trace(displayedVariations); + var displayedVariations = song.getVariationsByCharacter(currentCharacter); + trace('Displayed Variations (${songId}): $displayedVariations'); var availableDifficultiesForSong:Array = song.listDifficulties(displayedVariations, false); - trace(availableDifficultiesForSong); + trace('Available Difficulties: $availableDifficultiesForSong'); if (availableDifficultiesForSong.length == 0) continue; songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); @@ -314,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], @@ -339,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; @@ -362,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); @@ -373,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); @@ -386,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], @@ -395,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); @@ -406,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); @@ -417,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); @@ -428,37 +461,24 @@ 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; add(cardGlow); - dj = new DJBoyfriend(640, 366); - exitMovers.set([dj], - { - x: -dj.width * 1.6, - speed: 0.5 - }); + if (currentCharacter?.getFreeplayDJData() != null) + { + dj = new FreeplayDJ(640, 366, currentCharacterId); + exitMovers.set([dj], + { + x: -dj.width * 1.6, + speed: 0.5 + }); + add(dj); + } - // TODO: Replace this. - if (currentCharacter == 'pico') dj.visible = false; - - add(dj); - - bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad')); bgDad.shader = new AngleMask(); bgDad.visible = false; @@ -484,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], @@ -521,7 +537,6 @@ class FreeplayState extends MusicBeatSubState if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true; } - albumRoll = new AlbumRoll(); albumRoll.albumId = null; add(albumRoll); @@ -536,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; @@ -568,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); @@ -576,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; @@ -628,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(); @@ -675,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; @@ -700,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]; @@ -712,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. @@ -724,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) @@ -786,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); }; @@ -799,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); @@ -824,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) @@ -854,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! @@ -876,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; @@ -911,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); @@ -976,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); } @@ -1074,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; @@ -1192,7 +1202,23 @@ 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) + { + FlxG.switchState(FreeplayState.build( + { + { + character: currentCharacterId == "pico" ? "bf" : "pico", + } + })); } // if (FlxG.keys.justPressed.H) @@ -1206,6 +1232,11 @@ class FreeplayState extends MusicBeatSubState // } #end + if (controls.FREEPLAY_CHAR_SELECT && !busy) + { + FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); + } + if (controls.FREEPLAY_FAVORITE && !busy) { var targetSong = grpCapsules.members[curSelected]?.songData; @@ -1302,9 +1333,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 +1409,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) { @@ -1413,7 +1444,7 @@ class FreeplayState extends MusicBeatSubState } spamTimer += elapsed; - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); } else { @@ -1424,31 +1455,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 && !FlxG.keys.pressed.CONTROL) + if (controls.UI_LEFT_P) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeDiff(-1); generateSongList(currentFilter, true); } - if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) + if (controls.UI_RIGHT_P) { - dj.resetAFKTimer(); + if (dj != null) dj.resetAFKTimer(); changeDiff(1); generateSongList(currentFilter, true); } @@ -1458,7 +1489,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')); @@ -1484,7 +1515,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) { @@ -1492,14 +1524,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); } } @@ -1572,19 +1604,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; @@ -1646,7 +1677,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; @@ -1684,7 +1715,7 @@ class FreeplayState extends MusicBeatSubState }); trace('Available songs: ${availableSongCapsules.map(function(cap) { - return cap.songData.songName; + return cap?.songData?.songName; })}'); if (availableSongCapsules.length == 0) @@ -1713,32 +1744,45 @@ 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); - 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})'); 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')); - dj.confirm(); + 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}); @@ -1777,7 +1821,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, @@ -1832,7 +1876,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; @@ -1882,15 +1926,37 @@ class FreeplayState extends MusicBeatSubState } else { - var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : ""; - FunkinSound.playMusic(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 ?? ''; + 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' : ''; + + trace('Attempting to play partial preview: ${previewSongId}:${instSuffix}'); + + FunkinSound.playMusic(previewSongId, { 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: potentiallyErect, + suffix: instSuffix, partialParams: { loadPartial: true, @@ -1911,7 +1977,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); @@ -1949,8 +2015,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); } @@ -2039,6 +2105,8 @@ class FreeplaySongData function set_currentDifficulty(value:String):String { + if (currentDifficulty == value) return value; + currentDifficulty = value; updateValues(displayedVariations); return value; @@ -2096,7 +2164,12 @@ class FreeplaySongData this.albumId = songDifficulty.album; } - this.scoringRank = Save.instance.getSongRank(songId, currentDifficulty); + // 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, suffixedDifficulty); this.isNew = song.isSongNew(currentDifficulty); } diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 65a5d2e2d..2eec83223 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -24,12 +24,13 @@ import funkin.play.scoring.Scoring.ScoringRank; import funkin.save.Save; import funkin.save.Save.SaveScoreData; import flixel.util.FlxColor; +import funkin.ui.PixelatedIcon; class SongMenuItem extends FlxSpriteGroup { public var capsule:FlxSprite; - var pixelIcon:FlxSprite; + var pixelIcon:PixelatedIcon; /** * Modify this by calling `init()` @@ -201,11 +202,7 @@ class SongMenuItem extends FlxSpriteGroup // TODO: Use value from metadata instead of random. updateDifficultyRating(FlxG.random.int(0, 20)); - pixelIcon = new FlxSprite(160, 35); - - pixelIcon.makeGraphic(32, 32, 0x00000000); - pixelIcon.antialiasing = false; - pixelIcon.active = false; + pixelIcon = new PixelatedIcon(160, 35); add(pixelIcon); grpHide.add(pixelIcon); @@ -512,7 +509,7 @@ class SongMenuItem extends FlxSpriteGroup // Update capsule text. songText.text = songData?.songName ?? 'Random'; // Update capsule character. - if (songData?.songCharacter != null) setCharacter(songData.songCharacter); + if (songData?.songCharacter != null) pixelIcon.setCharacter(songData.songCharacter); updateBPM(Std.int(songData?.songStartingBpm) ?? 0); updateDifficultyRating(songData?.difficultyRating ?? 0); updateScoringRank(songData?.scoringRank); @@ -526,52 +523,6 @@ class SongMenuItem extends FlxSpriteGroup checkWeek(songData?.songId); } - /** - * Set the character displayed next to this song in the freeplay menu. - * @param char The character ID used by this song. - * If the character has no freeplay icon, a warning will be thrown and nothing will display. - */ - public function setCharacter(char:String):Void - { - var charPath:String = "freeplay/icons/"; - - // TODO: Put this in the character metadata where it belongs. - // TODO: Also, can use CharacterDataParser.getCharPixelIconAsset() - switch (char) - { - case 'monster-christmas': - charPath += 'monsterpixel'; - case 'mom-car': - charPath += 'mommypixel'; - case 'dad': - charPath += 'daddypixel'; - case 'darnell-blazin': - charPath += 'darnellpixel'; - case 'senpai-angry': - charPath += 'senpaipixel'; - default: - charPath += '${char}pixel'; - } - - if (!openfl.utils.Assets.exists(Paths.image(charPath))) - { - trace('[WARN] Character ${char} has no freeplay icon.'); - return; - } - - pixelIcon.loadGraphic(Paths.image(charPath)); - pixelIcon.scale.x = pixelIcon.scale.y = 2; - - switch (char) - { - case 'parents-christmas': - pixelIcon.origin.x = 140; - default: - pixelIcon.origin.x = 100; - } - // pixelIcon.origin.x = capsule.origin.x; - // pixelIcon.offset.x -= pixelIcon.origin.x; - } var frameInTicker:Float = 0; var frameInTypeBeat:Int = 0; @@ -711,6 +662,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/freeplay/charselect/PlayableCharacter.hx b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx new file mode 100644 index 000000000..c46b4b930 --- /dev/null +++ b/source/funkin/ui/freeplay/charselect/PlayableCharacter.hx @@ -0,0 +1,145 @@ +package funkin.ui.freeplay.charselect; + +import funkin.data.IRegistryEntry; +import funkin.data.freeplay.player.PlayerData; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.play.scoring.Scoring.ScoringRank; + +/** + * 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; + } + + public function getFreeplayDJData():PlayerFreeplayDJData + { + return _data.freeplayDJ; + } + + public function getFreeplayDJText(index:Int):String + { + return _data.freeplayDJ.getFreeplayDJText(index); + } + + /** + * @param rank Which rank to get info for + * @return An array of animations. For example, BF Great has two animations, one for BF and one for GF + */ + public function getResultsAnimationDatas(rank:ScoringRank):Array + { + if (_data.results == null) + { + return []; + } + + switch (rank) + { + case PERFECT | PERFECT_GOLD: + return _data.results.perfect; + case EXCELLENT: + return _data.results.excellent; + case GREAT: + return _data.results.great; + case GOOD: + return _data.results.good; + case SHIT: + return _data.results.loss; + } + } + + /** + * 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/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index 2eba406d9..d219bfbcf 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 @@ -341,17 +344,15 @@ class MainMenuState extends MusicBeatState } } + #if (debug || FORCE_DEBUG_VERSION) // Open the debug menu, defaults to ` / ~ - #if CHART_EDITOR_SUPPORTED if (controls.DEBUG_MENU) { persistentUpdate = false; FlxG.state.openSubState(new DebugMenuSubState()); } - #end - #if (debug || FORCE_DEBUG_VERSION) if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.W) { // Give the user a score of 1 point on Weekend 1 story mode. @@ -411,8 +412,8 @@ class MainMenuState extends MusicBeatState if (controls.BACK && menuItems.enabled && !menuItems.busy) { - FunkinSound.playOnce(Paths.sound('cancelMenu')); FlxG.switchState(() -> new TitleState()); + FunkinSound.playOnce(Paths.sound('cancelMenu')); } } } diff --git a/source/funkin/ui/options/OptionsState.hx b/source/funkin/ui/options/OptionsState.hx index 40308d96b..a2301e6a3 100644 --- a/source/funkin/ui/options/OptionsState.hx +++ b/source/funkin/ui/options/OptionsState.hx @@ -145,8 +145,8 @@ class Page extends FlxGroup { if (canExit && controls.BACK) { - FunkinSound.playOnce(Paths.sound('cancelMenu')); exit(); + FunkinSound.playOnce(Paths.sound('cancelMenu')); } } 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..4e51fb229 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; } @@ -336,6 +336,22 @@ class StoryMenuState extends MusicBeatState changeDifficulty(0); } + #if !html5 + if (FlxG.mouse.wheel != 0) + { + changeLevel(-Math.round(FlxG.mouse.wheel)); + } + #else + if (FlxG.mouse.wheel < 0) + { + changeLevel(-Math.round(FlxG.mouse.wheel / 8)); + } + else if (FlxG.mouse.wheel > 0) + { + changeLevel(-Math.round(FlxG.mouse.wheel / 8)); + } + #end + // TODO: Querying UI_RIGHT_P (justPressed) after UI_RIGHT always returns false. Fix it! if (controls.UI_RIGHT_P) { @@ -374,9 +390,9 @@ class StoryMenuState extends MusicBeatState if (controls.BACK && !exitingMenu && !selectedLevel) { - FunkinSound.playOnce(Paths.sound('cancelMenu')); exitingMenu = true; FlxG.switchState(() -> new MainMenuState()); + FunkinSound.playOnce(Paths.sound('cancelMenu')); } } 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/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 */ diff --git a/source/funkin/util/FramesJSFLParser.hx b/source/funkin/util/FramesJSFLParser.hx new file mode 100644 index 000000000..33bcf9d9d --- /dev/null +++ b/source/funkin/util/FramesJSFLParser.hx @@ -0,0 +1,48 @@ +package funkin.util; + +import openfl.Assets; + +/** + * See `funScripts/jsfl/frames.jsfl` for more information in the art repo/folder! + * Homemade dipshit proprietary format to get simple animation info out of flash! + * Pure convienience! + */ +class FramesJSFLParser +{ + public static function parse(path:String):FramesJSFLInfo + { + var text:String = Assets.getText(path); + + // TODO: error handle if text is null + + var output:FramesJSFLInfo = {frames: []}; + + var frames:Array = text.split("\n"); + + for (frame in frames) + { + var frameInfo:Array = frame.split(" "); + + var x:Float = Std.parseFloat(frameInfo[0]); + var y:Float = Std.parseFloat(frameInfo[1]); + var alpha:Float = Std.parseFloat(frameInfo[2]); + + var shit:FramesJSFLFrame = {x: x, y: y, alpha: alpha}; + output.frames.push(shit); + } + + return output; + } +} + +typedef FramesJSFLInfo = +{ + var frames:Array; +} + +typedef FramesJSFLFrame = +{ + var x:Float; + var y:Float; + var alpha:Float; +} diff --git a/source/funkin/util/MemoryUtil.hx b/source/funkin/util/MemoryUtil.hx index f5935ed67..18fd41472 100644 --- a/source/funkin/util/MemoryUtil.hx +++ b/source/funkin/util/MemoryUtil.hx @@ -48,11 +48,11 @@ class MemoryUtil * Calculate the total memory usage of the program, in bytes. * @return Int */ - public static function getMemoryUsed():Int + public static function getMemoryUsed():#if cpp Float #else Int #end { #if cpp // There is also Gc.MEM_INFO_RESERVED, MEM_INFO_CURRENT, and MEM_INFO_LARGE. - return cpp.vm.Gc.memInfo(cpp.vm.Gc.MEM_INFO_USAGE); + return cpp.vm.Gc.memInfo64(cpp.vm.Gc.MEM_INFO_USAGE); #else return openfl.system.System.totalMemory; #end 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(); 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) 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(); } }