2023-05-17 02:09:53 +00:00
|
|
|
package funkin.ui.story;
|
|
|
|
|
2023-10-17 21:27:11 +00:00
|
|
|
import funkin.util.SortUtil;
|
2023-05-17 02:09:53 +00:00
|
|
|
import flixel.FlxSprite;
|
2023-05-17 20:42:58 +00:00
|
|
|
import flixel.util.FlxColor;
|
|
|
|
import funkin.play.song.Song;
|
2023-05-17 02:09:53 +00:00
|
|
|
import funkin.data.IRegistryEntry;
|
2023-09-08 21:46:44 +00:00
|
|
|
import funkin.data.song.SongRegistry;
|
2024-03-23 19:34:37 +00:00
|
|
|
import funkin.data.story.level.LevelRegistry;
|
|
|
|
import funkin.data.story.level.LevelData;
|
2023-05-17 02:09:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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<LevelData>
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* 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<String>
|
|
|
|
*/
|
|
|
|
public function getSongs():Array<String>
|
|
|
|
{
|
2023-05-30 20:21:12 +00:00
|
|
|
// Copy the array so that it can't be modified on accident
|
|
|
|
return _data.songs.copy();
|
2023-05-17 02:09:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve the title of the level for display on the menu.
|
2024-03-05 07:29:44 +00:00
|
|
|
* @return Title of the level as a string
|
2023-05-17 02:09:53 +00:00
|
|
|
*/
|
|
|
|
public function getTitle():String
|
|
|
|
{
|
|
|
|
// TODO: Maybe add localization support?
|
|
|
|
return _data.name;
|
|
|
|
}
|
|
|
|
|
2024-03-05 07:29:44 +00:00
|
|
|
/**
|
|
|
|
* Construct the title graphic for the level.
|
|
|
|
* @return The constructed graphic as a sprite.
|
|
|
|
*/
|
2023-05-17 02:09:53 +00:00
|
|
|
public function buildTitleGraphic():FlxSprite
|
|
|
|
{
|
2024-03-05 07:29:44 +00:00
|
|
|
var result:FlxSprite = new FlxSprite().loadGraphic(Paths.image(_data.titleAsset));
|
2023-05-17 02:09:53 +00:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the list of songs in this level, as an array of names, for display on the menu.
|
2024-03-05 07:29:44 +00:00
|
|
|
* @param difficulty The difficulty of the level being displayed
|
|
|
|
* @return The display names of the songs in this level
|
2023-05-17 02:09:53 +00:00
|
|
|
*/
|
|
|
|
public function getSongDisplayNames(difficulty:String):Array<String>
|
|
|
|
{
|
2023-05-17 20:42:58 +00:00
|
|
|
var songList:Array<String> = getSongs() ?? [];
|
2023-09-08 21:46:44 +00:00
|
|
|
var songNameList:Array<String> = songList.map(function(songId:String) {
|
|
|
|
return getSongDisplayName(songId, difficulty);
|
2023-05-17 02:09:53 +00:00
|
|
|
});
|
2023-05-17 20:42:58 +00:00
|
|
|
return songNameList;
|
2023-05-17 02:09:53 +00:00
|
|
|
}
|
|
|
|
|
2023-09-08 21:46:44 +00:00
|
|
|
static function getSongDisplayName(songId:String, difficulty:String):String
|
|
|
|
{
|
|
|
|
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
|
|
|
|
if (song == null) return 'Unknown';
|
|
|
|
|
|
|
|
return song.songName;
|
|
|
|
}
|
|
|
|
|
2023-05-17 02:09:53 +00:00
|
|
|
/**
|
|
|
|
* Whether this level is unlocked. If not, it will be greyed out on the menu and have a lock icon.
|
2024-03-05 07:29:44 +00:00
|
|
|
* Override this in a script.
|
|
|
|
* @default `true`
|
|
|
|
* @return Whether this level is unlocked
|
2023-05-17 02:09:53 +00:00
|
|
|
*/
|
|
|
|
public function isUnlocked():Bool
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether this level is visible. If not, it will not be shown on the menu at all.
|
2024-03-05 07:29:44 +00:00
|
|
|
* Override this in a script.
|
|
|
|
* @default `true`
|
|
|
|
* @return Whether this level is visible in the menu
|
2023-05-17 02:09:53 +00:00
|
|
|
*/
|
|
|
|
public function isVisible():Bool
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-06-27 19:21:09 +00:00
|
|
|
/**
|
|
|
|
* Build a sprite for the background of the level.
|
|
|
|
* Can be overriden by ScriptedLevel. Not used if `isBackgroundSimple` returns true.
|
2024-03-05 07:29:44 +00:00
|
|
|
* @return The constructed sprite
|
2023-06-27 19:21:09 +00:00
|
|
|
*/
|
2023-05-17 02:09:53 +00:00
|
|
|
public function buildBackground():FlxSprite
|
|
|
|
{
|
2023-06-27 19:21:09 +00:00
|
|
|
if (!_data.background.startsWith('#'))
|
2023-05-17 02:09:53 +00:00
|
|
|
{
|
|
|
|
// Image specified
|
|
|
|
return new FlxSprite().loadGraphic(Paths.image(_data.background));
|
|
|
|
}
|
2023-06-27 19:21:09 +00:00
|
|
|
|
|
|
|
// 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.
|
2024-03-05 07:29:44 +00:00
|
|
|
* @return Whether the background is a simple color
|
2023-06-27 19:21:09 +00:00
|
|
|
*/
|
|
|
|
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.
|
2024-03-05 07:29:44 +00:00
|
|
|
* @return The background as a simple color. May not be valid if `isBackgroundSimple` returns false.
|
2023-06-27 19:21:09 +00:00
|
|
|
*/
|
|
|
|
public function getBackgroundColor():FlxColor
|
|
|
|
{
|
|
|
|
return FlxColor.fromString(_data.background);
|
2023-05-17 02:09:53 +00:00
|
|
|
}
|
|
|
|
|
2024-03-05 07:29:44 +00:00
|
|
|
/**
|
|
|
|
* The list of difficulties the player can select from for this level.
|
|
|
|
* @return The difficulty IDs.
|
|
|
|
*/
|
2023-05-17 02:09:53 +00:00
|
|
|
public function getDifficulties():Array<String>
|
|
|
|
{
|
|
|
|
var difficulties:Array<String> = [];
|
|
|
|
|
2024-03-05 07:29:44 +00:00
|
|
|
var songList:Array<String> = getSongs();
|
2023-05-17 02:09:53 +00:00
|
|
|
|
|
|
|
var firstSongId:String = songList[0];
|
2023-09-08 21:46:44 +00:00
|
|
|
var firstSong:Song = SongRegistry.instance.fetchEntry(firstSongId);
|
2023-05-17 02:09:53 +00:00
|
|
|
|
2023-05-23 00:55:53 +00:00
|
|
|
if (firstSong != null)
|
2023-05-17 02:09:53 +00:00
|
|
|
{
|
2024-03-05 07:29:44 +00:00
|
|
|
// Don't display alternate characters in Story Mode. Only show `default` and `erect` variations.
|
2024-04-04 01:57:29 +00:00
|
|
|
for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect'], false, false))
|
2023-05-23 00:55:53 +00:00
|
|
|
{
|
|
|
|
difficulties.push(difficulty);
|
|
|
|
}
|
2023-05-17 02:09:53 +00:00
|
|
|
}
|
|
|
|
|
2024-03-05 07:29:44 +00:00
|
|
|
// Sort in a specific order! Fall back to alphabetical.
|
2023-10-17 21:27:11 +00:00
|
|
|
difficulties.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
|
2023-10-17 03:43:59 +00:00
|
|
|
|
2023-05-17 02:09:53 +00:00
|
|
|
// Filter to only include difficulties that are present in all songs
|
2023-05-17 20:42:58 +00:00
|
|
|
for (songIndex in 1...songList.length)
|
2023-05-17 02:09:53 +00:00
|
|
|
{
|
2023-05-17 20:42:58 +00:00
|
|
|
var songId:String = songList[songIndex];
|
2023-09-08 21:46:44 +00:00
|
|
|
var song:Song = SongRegistry.instance.fetchEntry(songId);
|
2023-05-17 02:09:53 +00:00
|
|
|
|
2023-05-22 17:47:01 +00:00
|
|
|
if (song == null) continue;
|
|
|
|
|
2023-05-17 02:09:53 +00:00
|
|
|
for (difficulty in difficulties)
|
|
|
|
{
|
2024-03-05 07:29:44 +00:00
|
|
|
if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, 'erect']))
|
2023-05-17 02:09:53 +00:00
|
|
|
{
|
|
|
|
difficulties.remove(difficulty);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-23 00:55:53 +00:00
|
|
|
if (difficulties.length == 0) difficulties = ['normal'];
|
|
|
|
|
2023-05-17 02:09:53 +00:00
|
|
|
return difficulties;
|
|
|
|
}
|
|
|
|
|
2024-03-05 07:29:44 +00:00
|
|
|
/**
|
|
|
|
* Build the props for display over the colored background.
|
|
|
|
* @param existingProps The existing prop sprites, if any.
|
|
|
|
* @return The constructed prop sprites
|
|
|
|
*/
|
2024-01-16 04:43:05 +00:00
|
|
|
public function buildProps(?existingProps:Array<LevelProp>):Array<LevelProp>
|
2023-05-17 02:09:53 +00:00
|
|
|
{
|
2024-01-16 04:43:05 +00:00
|
|
|
var props:Array<LevelProp> = existingProps == null ? [] : [for (x in existingProps) x];
|
2023-05-17 02:09:53 +00:00
|
|
|
|
|
|
|
if (_data.props.length == 0) return props;
|
|
|
|
|
2024-02-13 10:23:59 +00:00
|
|
|
var hiddenProps:Array<LevelProp> = props.splice(_data.props.length - 1, props.length - 1);
|
|
|
|
for (hiddenProp in hiddenProps)
|
2024-03-05 07:29:44 +00:00
|
|
|
{
|
2024-02-13 10:23:59 +00:00
|
|
|
hiddenProp.visible = false;
|
2024-03-05 07:29:44 +00:00
|
|
|
}
|
2024-02-13 10:23:59 +00:00
|
|
|
|
2023-05-17 02:09:53 +00:00
|
|
|
for (propIndex in 0..._data.props.length)
|
|
|
|
{
|
2024-03-05 07:29:44 +00:00
|
|
|
var propData:LevelPropData = _data.props[propIndex];
|
2023-06-10 06:56:03 +00:00
|
|
|
|
2024-01-16 04:43:05 +00:00
|
|
|
// Attempt to reuse the `LevelProp` object.
|
|
|
|
// This prevents animations from resetting.
|
|
|
|
var existingProp:Null<LevelProp> = props[propIndex];
|
|
|
|
if (existingProp != null)
|
|
|
|
{
|
|
|
|
existingProp.propData = propData;
|
2024-03-05 05:22:29 +00:00
|
|
|
if (existingProp.propData == null)
|
|
|
|
{
|
|
|
|
existingProp.visible = false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
existingProp.visible = true;
|
|
|
|
existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
|
|
|
|
}
|
2024-01-16 04:43:05 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
var propSprite:Null<LevelProp> = LevelProp.build(propData);
|
|
|
|
if (propSprite == null) continue;
|
2023-06-10 06:56:03 +00:00
|
|
|
|
2024-01-16 04:43:05 +00:00
|
|
|
propSprite.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
|
|
|
|
props.push(propSprite);
|
|
|
|
}
|
2023-05-17 02:09:53 +00:00
|
|
|
}
|
2023-05-17 20:42:58 +00:00
|
|
|
|
|
|
|
return props;
|
2023-05-17 02:09:53 +00:00
|
|
|
}
|
|
|
|
|
2024-03-05 07:29:44 +00:00
|
|
|
/**
|
|
|
|
* Called when the level is destroyed.
|
|
|
|
* TODO: Document when this gets called
|
|
|
|
*/
|
2023-05-17 02:09:53 +00:00
|
|
|
public function destroy():Void {}
|
|
|
|
|
|
|
|
public function toString():String
|
|
|
|
{
|
|
|
|
return 'Level($id)';
|
|
|
|
}
|
|
|
|
|
2024-03-05 07:29:44 +00:00
|
|
|
/**
|
|
|
|
* Retrieve and parse the JSON data for a level by ID.
|
|
|
|
* @param id The ID of the level
|
|
|
|
* @return The parsed level data, or null if not found or invalid
|
|
|
|
*/
|
2023-09-08 21:46:44 +00:00
|
|
|
static function _fetchData(id:String):Null<LevelData>
|
2023-05-17 02:09:53 +00:00
|
|
|
{
|
2023-08-22 08:27:30 +00:00
|
|
|
return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id));
|
2023-05-17 02:09:53 +00:00
|
|
|
}
|
|
|
|
}
|