diff --git a/hmm.json b/hmm.json index 2daf5b1f0..0ef6974ee 100644 --- a/hmm.json +++ b/hmm.json @@ -86,6 +86,11 @@ "type": "haxelib", "version": null }, + { + "name": "json2object", + "type": "haxelib", + "version": null + }, { "name": "lime", "type": "git", @@ -113,4 +118,4 @@ "version": "0.2.2" } ] -} +} \ No newline at end of file diff --git a/source/funkin/StoryMenuState.hx b/source/funkin/StoryMenuState.hx index d9640d620..89d59de1f 100644 --- a/source/funkin/StoryMenuState.hx +++ b/source/funkin/StoryMenuState.hx @@ -122,10 +122,10 @@ class StoryMenuState extends MusicBeatState persistentUpdate = persistentDraw = true; - scoreText = new FlxText(10, 10, 0, "SCORE: 49324858", 36); + scoreText = new FlxText(10, 10, 0, "SCORE: 49324858"); scoreText.setFormat("VCR OSD Mono", 32); - txtWeekTitle = new FlxText(FlxG.width * 0.7, 10, 0, "", 32); + txtWeekTitle = new FlxText(FlxG.width * 0.7, 10, 0, ""); txtWeekTitle.setFormat("VCR OSD Mono", 32, FlxColor.WHITE, RIGHT); txtWeekTitle.alpha = 0.7; diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx new file mode 100644 index 000000000..96eb43965 --- /dev/null +++ b/source/funkin/data/BaseRegistry.hx @@ -0,0 +1,162 @@ +package funkin.data; + +import openfl.Assets; +import funkin.util.assets.DataAssets; +import haxe.Constraints.Constructible; + +/** + * The entry's constructor function must take a single argument, the entry's ID. + */ +typedef EntryConstructorFunction = String->Void; + +/** + * A base type for a Registry, which is an object which handles loading scriptable objects. + * + * @param T The type to construct. Must implement `IRegistryEntry`. + * @param J The type of the JSON data used when constructing. + */ +@:generic +abstract class BaseRegistry & Constructible), J> +{ + public final registryId:String; + + final dataFilePath:String; + + final entries:Map; + + // public abstract static final instance:BaseRegistry = new BaseRegistry<>(); + + /** + * @param registryId A readable ID for this registry, used when logging. + * @param dataFilePath The path (relative to `assets/data`) to search for JSON files. + */ + public function new(registryId:String, dataFilePath:String) + { + this.registryId = registryId; + this.dataFilePath = dataFilePath; + + this.entries = new Map(); + } + + public function loadEntries():Void + { + clearEntries(); + + // + // SCRIPTED ENTRIES + // + var scriptedEntryClassNames:Array = getScriptedClassNames(); + log('Registering ${scriptedEntryClassNames.length} scripted entries...'); + + for (entryCls in scriptedEntryClassNames) + { + var entry:T = createScriptedEntry(entryCls); + + if (entry != null) + { + log('Successfully created scripted entry (${entryCls} = ${entry.id})'); + entries.set(entry.id, entry); + } + else + { + log('Failed to create scripted entry (${entryCls})'); + } + } + + // + // UNSCRIPTED ENTRIES + // + var entryIdList:Array = DataAssets.listDataFilesInPath(dataFilePath); + var unscriptedEntryIds:Array = entryIdList.filter(function(entryId:String):Bool { + return !entries.exists(entryId); + }); + log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...'); + for (entryId in unscriptedEntryIds) + { + try + { + var entry:T = createEntry(entryId); + if (entry != null) + { + trace(' Loaded entry data: ${entry}'); + entries.set(entry.id, entry); + } + } + catch (e:Dynamic) + { + trace(' Failed to load entry data: ${entryId}'); + trace(e); + continue; + } + } + } + + public function listEntryIds():Array + { + return entries.keys().array(); + } + + public function countEntries():Int + { + return entries.size(); + } + + public function toString():String + { + return 'Registry(' + registryId + ', ${countEntries()} entries)'; + } + + function log(message:String):Void + { + trace('[' + registryId + '] ' + message); + } + + function loadEntryFile(id:String):String + { + var entryFilePath:String = Paths.json('${dataFilePath}/${id}'); + var rawJson:String = openfl.Assets.getText(entryFilePath).trim(); + return rawJson; + } + + function clearEntries():Void + { + for (entry in entries) + { + entry.destroy(); + } + + entries.clear(); + } + + // + // FUNCTIONS TO IMPLEMENT + // + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class annd + */ + public abstract function parseEntryData(id:String):Null; + + /** + * Retrieve the list of scripted class names to load. + * @return An array of scripted class names. + */ + abstract function getScriptedClassNames():Array; + + /** + * Create an entry from the given ID. + * @param id + */ + function createEntry(id:String):Null + { + return new T(id); + } + + /** + * Create a entry, attached to a scripted class, from the given class name. + * @param clsName + */ + abstract function createScriptedEntry(clsName:String):Null; +} diff --git a/source/funkin/data/IRegistryEntry.hx b/source/funkin/data/IRegistryEntry.hx new file mode 100644 index 000000000..0fb704b7c --- /dev/null +++ b/source/funkin/data/IRegistryEntry.hx @@ -0,0 +1,19 @@ +package funkin.data; + +/** + * An interface defining the necessary functions for a registry entry. + * A `String->Void` constructor is also mandatory, but enforced elsewhere. + * @param T The JSON data type of the registry entry. + */ +interface IRegistryEntry +{ + public final id:String; + + // public function new(id:String):Void; + public function destroy():Void; + public function toString():String; + + // Can't make an interface field private I guess. + public final _data:T; + public function _fetchData(id:String):Null; +} diff --git a/source/funkin/data/level/LevelData.hx b/source/funkin/data/level/LevelData.hx new file mode 100644 index 000000000..abee74689 --- /dev/null +++ b/source/funkin/data/level/LevelData.hx @@ -0,0 +1,83 @@ +package funkin.data.level; + +import funkin.play.AnimationData; + +/** + * A type definition for the data in a story mode level JSON file. + * @see https://lib.haxe.org/p/json2object/ + */ +typedef LevelData = +{ + /** + * The version number of the level data schema. + * When making changes to the level data format, this should be incremented, + * and a migration function should be added to LevelDataParser to handle old versions. + */ + @:default(LevelRegistry.LEVEL_DATA_VERSION) + var version:String; + + /** + * The title of the week, as seen in the top corner. + */ + var name:String; + + /** + * The graphic for the level, as seen in the scrolling list. + */ + var titleAsset:String; + + @:default([]) + var props:Array; + @:default(["bopeebo"]) + var songs:Array; + @:default("#F9CF51") + @:optional + var background:String; +} + +typedef LevelPropData = +{ + /** + * The image to use for the prop. May optionally be a sprite sheet. + */ + var assetPath:String; + + /** + * The scale to render the prop at. + * @default 1.0 + */ + @:default(1.0) + @:optional + var scale:Float; + + /** + * If true, the prop is a pixel sprite, and will be rendered without smoothing. + */ + @:default(false) + @:optional + var isPixel:Bool; + + /** + * The frequency to bop at, in beats. + * @default 1.0 = every beat + */ + @:default(1.0) + @:optional + var danceEvery:Float; + + /** + * The offset on the position to render the prop at. + * @default [0.0, 0.0] + */ + @:default([0, 0]) + @:optional + var offset:Array; + + /** + * A set of animations to play on the prop. + * If default/empty, the prop will be static. + */ + @:default([]) + @:optional + var animations:Array; +} diff --git a/source/funkin/data/level/LevelRegistry.hx b/source/funkin/data/level/LevelRegistry.hx new file mode 100644 index 000000000..7447c467f --- /dev/null +++ b/source/funkin/data/level/LevelRegistry.hx @@ -0,0 +1,75 @@ +package funkin.data.level; + +import funkin.ui.story.Level; +import funkin.data.level.LevelData; +import funkin.ui.story.ScriptedLevel; + +class LevelRegistry extends BaseRegistry +{ + /** + * The current version string for the stage data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migrateStageData()` function. + */ + public static final LEVEL_DATA_VERSION:String = "1.0.0"; + + public static final instance:LevelRegistry = new LevelRegistry(); + + public function new() + { + super('LEVEL', 'levels'); + } + + /** + * 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(); + var jsonStr:String = loadEntryFile(id); + + parser.fromJson(jsonStr); + + if (parser.errors.length > 0) + { + trace('Failed to parse entry data: ${id}'); + for (error in parser.errors) + { + trace(error); + } + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):Level + { + return ScriptedLevel.init(clsName, "unknown"); + } + + function getScriptedClassNames():Array + { + return ScriptedLevel.listScriptClasses(); + } + + /** + * A list of all the story weeks, in order. + * TODO: Should this be hardcoded? + */ + public function listDefaultLevelIds():String + { + return [ + "tutorial", + "week1", + "week2", + "week3", + "week4", + "week5", + "week6", + "week7", + "weekend1" + ] + } +} diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 853846414..dde56be23 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -136,6 +136,11 @@ class Song // implements IPlayStateScriptedClass return difficulties.get(diffId); } + public function listDifficulties():Array + { + return difficulties.keys().array(); + } + /** * Purge the cached chart data for each difficulty of this song. */ diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx new file mode 100644 index 000000000..0428fbbc4 --- /dev/null +++ b/source/funkin/ui/story/Level.hx @@ -0,0 +1,162 @@ +package funkin.ui.story; + +import flixel.FlxSprite; +import funkin.data.IRegistryEntry; +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 + { + return _data.songs; + } + + /** + * 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 + { + return getSongs().map(function(songId) { + return funkin.play.song.SongData.SongDataParser.fetchSong(songId).getDifficulty(difficulty).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; + } + + public function buildBackground():FlxSprite + { + if (_data.background.startsWith('#')) + { + // Color specified + var color:FlxColor = FlxColor.fromString(_data.background); + return new FlxSprite().makeGraphic(FlxG.width, 400, color); + } + else + { + // Image specified + return new FlxSprite().loadGraphic(Paths.image(_data.background)); + } + } + + public function getDifficulties():Array + { + var difficulties:Array = []; + + var songList = getSongs(); + + var firstSongId:String = songList[0]; + var firstSong:Song = funkin.play.song.SongData.SongDataParser.fetchSong(firstSongId); + + for (difficulty in firstSong.getDifficulties()) + { + difficulties.push(difficulty); + } + + // Filter to only include difficulties that are present in all songs + for (songId in 1...songList.length) + { + var song:Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId); + + for (difficulty in difficulties) + { + if (!song.hasDifficulty(difficulty)) + { + difficulties.remove(difficulty); + } + } + } + + return difficulties; + } + + public function buildProps():Array + { + var props:Array = []; + + if (_data.props.length == 0) return props; + + for (propIndex in 0..._data.props.length) + { + var propData = _data.props[propIndex]; + var propSprite:LevelProp = LevelProp.build(propData); + propSprite.x += FlxG.width * 0.25 * propIndex; + } + } + + public function destroy():Void {} + + public function toString():String + { + return 'Level($id)'; + } + + public function _fetchData(id:String):Null + { + return LevelRegistry.instance.parseEntryData(id); + } +} diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx new file mode 100644 index 000000000..8f2ad5026 --- /dev/null +++ b/source/funkin/ui/story/LevelProp.hx @@ -0,0 +1,51 @@ +package funkin.ui.story; + +class LevelProp extends Bopper +{ + public function new(danceEvery:Int) + { + super(danceEvery); + } + + public static function build(propData:LevelPropData):Null + { + var isAnimated:Bool = propData.animations.length > 0; + var prop:LevelProp = new LevelProp(propData.danceEvery); + + if (isAnimated) + { + // Initalize sprite frames. + // Sparrow atlas only LEL. + prop.frames = Paths.getSparrowAtlas(propData.assetPath); + } + else + { + // Initalize static sprite. + prop.loadGraphic(Paths.image(propData.assetPath)); + + // Disables calls to update() for a performance boost. + prop.active = false; + } + + if (prop.frames == null || prop.frames.numFrames == 0) + { + trace('ERROR: Could not build texture for level prop (${propData.assetPath}).'); + return null; + } + + prop.scale.set(propData.scale * (propData.isPixel ? 6 : 1)); + prop.updateHitbox(); + prop.antialiasing = !propData.isPixel; + prop.alpha = propData.alpha; + prop.x = propData.offsets[0]; + prop.y = propData.offsets[1]; + + FlxAnimationUtil.addAtlasAnimations(prop, propData.animations); + for (propAnim in propData.animations) + { + prop.setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]); + } + + return prop; + } +} diff --git a/source/funkin/ui/story/LevelTitle.hx b/source/funkin/ui/story/LevelTitle.hx new file mode 100644 index 000000000..7318c3e59 --- /dev/null +++ b/source/funkin/ui/story/LevelTitle.hx @@ -0,0 +1,78 @@ +package funkin.ui.story; + +import funkin.CoolUtil; +import flixel.FlxSprite; +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.group.FlxSpriteGroup; + +class LevelTitle extends FlxSpriteGroup +{ + static final LOCK_PAD:Int = 4; + + 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); + + this.level = level; + } + + public override function create():Void + { + super.create(); + + buildLevelTitle(); + buildLevelLock(); + } + + // 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 override function update(elapsed:Float):Void + { + this.y = CoolUtil.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; + } + + public function showLock():Void + { + lock.visible = true; + this.x -= (lock.width + LOCK_PAD) / 2; + } + + public function hideLock():Void + { + lock.visible = false; + this.x += (lock.width + LOCK_PAD) / 2; + } + + function buildLevelTitle():Void + { + title = level.buildTitleGraphic(); + add(title); + } + + function buildLevelLock():Void + { + lock = new FlxSprite(0, 0).loadGraphic(Paths.image('storymenu/ui/lock')); + lock.x = title.x + title.width + LOCK_PAD; + lock.visible = false; + add(lock); + } +} diff --git a/source/funkin/ui/story/ScriptedLevel.hx b/source/funkin/ui/story/ScriptedLevel.hx new file mode 100644 index 000000000..a9921741c --- /dev/null +++ b/source/funkin/ui/story/ScriptedLevel.hx @@ -0,0 +1,9 @@ +package funkin.ui.story; + +/** + * A script that can be tied to a Level, which persists across states. + * Create a scripted class that extends Level to use this. + * This allows you to customize how a specific level appears. + */ +@:hscriptClass +class ScriptedLevel extends funkin.ui.story.Level implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx new file mode 100644 index 000000000..c1ce448eb --- /dev/null +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -0,0 +1,258 @@ +package funkin.ui.story; + +class StoryMenuState extends MusicBeatState +{ + static final DEFAULT_BACKGROUND_COLOR:FlxColor = FlxColor.fromString("#F9CF51"); + static final BACKGROUND_HEIGHT:Int = 400; + + var currentDifficultyId:String = 'normal'; + + var currentLevelId:String = 'tutorial'; + var currentLevel:Level; + var isLevelUnlocked:Bool; + + var highScore:Int = 42069420; + var highScoreLerp:Int = 12345678; + + var exitingMenu:Bool = false; + var selectedWeek:Bool = false; + + // + // RENDER OBJECTS + // + + /** + * The title of the level at the top. + */ + var levelTitleText:FlxText; + + /** + * The score text at the top. + */ + var scoreText:FlxText; + + /** + * The list of songs on the left. + */ + var tracklistText:FlxText; + + /** + * The title of the week in the middle. + */ + var levelTitles:FlxTypedGroup; + + /** + * The props in the center. + */ + var levelProps:FlxTypedGroup; + + /** + * The background behind the props. + */ + var levelBackground:FlxSprite; + + /** + * The left arrow of the difficulty selector. + */ + var leftDifficultyArrow:FlxSprite; + + /** + * The right arrow of the difficulty selector. + */ + var rightDifficultyArrow:FlxSprite; + + /** + * The text of the difficulty selector. + */ + var difficultySprite:FlxSprite; + + public function new(?stickers:StickerSubState = null) + { + if (stickers != null) + { + stickerSubState = stickers; + } + + super(); + } + + override function create():Void + { + transIn = FlxTransitionableState.defaultTransIn; + transOut = FlxTransitionableState.defaultTransOut; + + if (!FlxG.sound.music.playing) + { + FlxG.sound.playMusic(Paths.music('freakyMenu')); + FlxG.sound.music.fadeIn(4, 0, 0.7); + Conductor.forceBPM(Constants.FREAKY_MENU_BPM); + } + + if (stickerSubState != null) + { + this.persistentUpdate = true; + this.persistentDraw = true; + + openSubState(stickerSubState); + stickerSubState.degenStickers(); + + // resetSubState(); + } + + persistentUpdate = persistentDraw = true; + + // Explicitly define the background color. + this.bgColor = FlxColor.BLACK; + + levelTitles = new FlxTypedGroup(); + add(levelTitles); + + levelBackground = new FlxSprite(0, 56).makeGraphic(FlxG.width, BACKGROUND_HEIGHT, DEFAULT_BACKGROUND_COLOR); + add(levelBackground); + + levelProps = new FlxTypedGroup(); + add(levelProps); + + scoreText = new FlxText(10, 10, 0, 'HIGH SCORE: 42069420'); + scoreText.setFormat("VCR OSD Mono", 32); + add(scoreText); + + tracklistText = new FlxText(FlxG.width * 0.05, yellowBG.x + yellowBG.height + 100, 0, "Tracks", 32); + tracklistText.alignment = CENTER; + tracklistText.font = rankText.font; + tracklistText.color = 0xFFe55777; + add(tracklistText); + + levelTitleText = new FlxText(FlxG.width * 0.7, 10, 0, 'WEEK 1'); + levelTitleText.setFormat("VCR OSD Mono", 32, FlxColor.WHITE, RIGHT); + levelTitleText.alpha = 0.7; + add(levelTitleText); + + buildLevelTitles(false); + + leftArrow = new FlxSprite(grpWeekText.members[0].x + grpWeekText.members[0].width + 10, grpWeekText.members[0].y + 10); + leftArrow.frames = Paths.getSparrowAtlas('storymenu/ui/arrows'); + leftArrow.animation.addByPrefix('idle', 'leftIdle0'); + leftArrow.animation.addByPrefix('press', 'leftConfirm0'); + leftArrow.animation.play('idle'); + add(leftArrow); + + rightArrow = new FlxSprite(sprDifficulty.x + sprDifficulty.width + 50, leftArrow.y); + rightArrow.frames = leftArrow.frames; + rightArrow.animation.addByPrefix('idle', 'rightIdle0'); + rightArrow.animation.addByPrefix('press', 'rightConfirm0'); + rightArrow.animation.play('idle'); + add(rightArrow); + + difficultySprite = buildDifficultySprite(); + changeDifficulty(); + add(difficultySprite); + + #if discord_rpc + // Updating Discord Rich Presence + DiscordClient.changePresence("In the Menus", null); + #end + } + + function buildDifficultySprite():Void + { + difficultySprite = new FlxSprite(leftArrow.x + 130, leftArrow.y); + difficultySprite.frames = ui_tex; + difficultySprite.animation.addByPrefix('easy', 'EASY'); + difficultySprite.animation.addByPrefix('normal', 'NORMAL'); + difficultySprite.animation.addByPrefix('hard', 'HARD'); + difficultySprite.animation.play('easy'); + } + + function buildLevelTitles(moddedLevels:Bool):Void + { + levelTitles.clear(); + + var levelIds:Array = LevelRegistry.instance.getLevelIds(); + for (levelIndex in 0...levelIds.length) + { + var levelId:String = levelIds[levelIndex]; + var level:Level = LevelRegistry.instance.fetchEntry(levelId); + var levelTitleItem:LevelTitle = new LevelTitle(0, yellowBG.y + yellowBG.height + 10, level); + levelTitleItem.targetY = ((weekThing.height + 20) * levelIndex); + levelTitles.add(levelTitleItem); + } + } + + override function update(elapsed:Float) + { + highScoreLerp = CoolUtil.coolLerp(highScoreLerp, highScore, 0.5); + + scoreText.text = 'WEEK SCORE: ${Math.round(highScoreLerp)}'; + + txtWeekTitle.text = weekNames[curWeek].toUpperCase(); + txtWeekTitle.x = FlxG.width - (txtWeekTitle.width + 10); + + handleKeyPresses(); + + super.update(elapsed); + } + + function handleKeyPresses():Void + { + if (!exitingMenu) + { + if (!selectedWeek) + { + if (controls.UI_UP_P) + { + changeLevel(-1); + } + + if (controls.UI_DOWN_P) + { + changeLevel(1); + } + + if (controls.UI_RIGHT) + { + rightArrow.animation.play('press') + } + else + { + rightArrow.animation.play('idle'); + } + + if (controls.UI_LEFT) + { + leftArrow.animation.play('press'); + } + else + { + leftArrow.animation.play('idle'); + } + + if (controls.UI_RIGHT_P) + { + changeDifficulty(1); + } + + if (controls.UI_LEFT_P) + { + changeDifficulty(-1); + } + } + + if (controls.ACCEPT) + { + selectWeek(); + } + } + + if (controls.BACK && !exitingMenu && !selectedWeek) + { + FlxG.sound.play(Paths.sound('cancelMenu')); + exitingMenu = true; + FlxG.switchState(new MainMenuState()); + } + } + + function changeLevel(change:Int = 0):Void {} + + function changeDifficulty(change:Int = 0):Void {} +} diff --git a/source/funkin/util/tools/MapTools.hx b/source/funkin/util/tools/MapTools.hx index daedb4aab..430b7bc81 100644 --- a/source/funkin/util/tools/MapTools.hx +++ b/source/funkin/util/tools/MapTools.hx @@ -9,13 +9,27 @@ package funkin.util.tools; */ class MapTools { + /** + * Return the quantity of keys in the map. + */ + public static function size(map:Map):Int + { + return map.keys().array().length; + } + + /** + * Return a list of values from the map, as an array. + */ public static function values(map:Map):Array { return [for (i in map.iterator()) i]; } + /** + * Return a list of keys from the map (as an array, rather than an iterator). + */ public static function keyValues(map:Map):Array { - return [for (i in map.keys()) i]; + return map.keys().array(); } }