package funkin.ui.story; import funkin.util.SortUtil; import flixel.FlxSprite; import flixel.util.FlxColor; import funkin.play.song.Song; import funkin.data.IRegistryEntry; import funkin.data.song.SongRegistry; import funkin.data.level.LevelRegistry; import funkin.data.level.LevelData; /** * An object used to retrieve data about a story mode level (also known as "weeks"). * Can be scripted to override each function, for custom behavior. */ class Level implements IRegistryEntry { /** * The ID of the story mode level. */ public final id:String; /** * Level data as parsed from the JSON file. */ public final _data:LevelData; /** * @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 level data for id: $id'; } } /** * Get the list of songs in this level, as an array of IDs. * @return Array */ public function getSongs():Array { // Copy the array so that it can't be modified on accident return _data.songs.copy(); } /** * Retrieve the title of the level for display on the menu. */ public function getTitle():String { // TODO: Maybe add localization support? return _data.name; } public function buildTitleGraphic():FlxSprite { var result = new FlxSprite().loadGraphic(Paths.image(_data.titleAsset)); return result; } /** * Get the list of songs in this level, as an array of names, for display on the menu. * @return Array */ public function getSongDisplayNames(difficulty:String):Array { var songList:Array = getSongs() ?? []; var songNameList:Array = songList.map(function(songId:String) { return getSongDisplayName(songId, difficulty); }); return songNameList; } static function getSongDisplayName(songId:String, difficulty:String):String { var song:Null = SongRegistry.instance.fetchEntry(songId); if (song == null) return 'Unknown'; return song.songName; } /** * Whether this level is unlocked. If not, it will be greyed out on the menu and have a lock icon. * TODO: Change this behavior in a later release. */ public function isUnlocked():Bool { return true; } /** * Whether this level is visible. If not, it will not be shown on the menu at all. */ public function isVisible():Bool { return true; } /** * Build a sprite for the background of the level. * Can be overriden by ScriptedLevel. Not used if `isBackgroundSimple` returns true. */ public function buildBackground():FlxSprite { if (!_data.background.startsWith('#')) { // Image specified return new FlxSprite().loadGraphic(Paths.image(_data.background)); } // Color specified var result:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 400, FlxColor.WHITE); result.color = getBackgroundColor(); return result; } /** * Returns true if the background is a solid color. * If you have a ScriptedLevel with a fancy background, you may want to override this to false. */ public function isBackgroundSimple():Bool { return _data.background.startsWith('#'); } /** * Returns true if the background is a solid color. * If you have a ScriptedLevel with a fancy background, you may want to override this to false. */ public function getBackgroundColor():FlxColor { return FlxColor.fromString(_data.background); } public function getDifficulties():Array { var difficulties:Array = []; var songList = getSongs(); var firstSongId:String = songList[0]; var firstSong:Song = SongRegistry.instance.fetchEntry(firstSongId); if (firstSong != null) { // Don't display alternate characters in Story Mode. for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, "erect"])) { difficulties.push(difficulty); } } difficulties.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST)); // Filter to only include difficulties that are present in all songs for (songIndex in 1...songList.length) { var songId:String = songList[songIndex]; var song:Song = SongRegistry.instance.fetchEntry(songId); if (song == null) continue; for (difficulty in difficulties) { if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, "erect"])) { difficulties.remove(difficulty); } } } if (difficulties.length == 0) difficulties = ['normal']; return difficulties; } public function buildProps(?existingProps:Array):Array { var props:Array = existingProps == null ? [] : [for (x in existingProps) x]; if (_data.props.length == 0) return props; for (propIndex in 0..._data.props.length) { var propData = _data.props[propIndex]; // Attempt to reuse the `LevelProp` object. // This prevents animations from resetting. var existingProp:Null = props[propIndex]; if (existingProp != null) { existingProp.propData = propData; existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex; } else { var propSprite:Null = LevelProp.build(propData); if (propSprite == null) continue; propSprite.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex; props.push(propSprite); } } return props; } public function destroy():Void {} public function toString():String { return 'Level($id)'; } static function _fetchData(id:String):Null { return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id)); } }