From 021f7a0a1c14c215e27b7a2b57a385e5b492584c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 16 May 2023 22:09:53 -0400 Subject: [PATCH 1/4] Work in progress on story menu data storage rework --- hmm.json | 7 +- source/funkin/StoryMenuState.hx | 4 +- source/funkin/data/BaseRegistry.hx | 162 ++++++++++++++ source/funkin/data/IRegistryEntry.hx | 19 ++ source/funkin/data/level/LevelData.hx | 83 +++++++ source/funkin/data/level/LevelRegistry.hx | 75 +++++++ source/funkin/play/song/Song.hx | 5 + source/funkin/ui/story/Level.hx | 162 ++++++++++++++ source/funkin/ui/story/LevelProp.hx | 51 +++++ source/funkin/ui/story/LevelTitle.hx | 78 +++++++ source/funkin/ui/story/ScriptedLevel.hx | 9 + source/funkin/ui/story/StoryMenuState.hx | 258 ++++++++++++++++++++++ source/funkin/util/tools/MapTools.hx | 16 +- 13 files changed, 925 insertions(+), 4 deletions(-) create mode 100644 source/funkin/data/BaseRegistry.hx create mode 100644 source/funkin/data/IRegistryEntry.hx create mode 100644 source/funkin/data/level/LevelData.hx create mode 100644 source/funkin/data/level/LevelRegistry.hx create mode 100644 source/funkin/ui/story/Level.hx create mode 100644 source/funkin/ui/story/LevelProp.hx create mode 100644 source/funkin/ui/story/LevelTitle.hx create mode 100644 source/funkin/ui/story/ScriptedLevel.hx create mode 100644 source/funkin/ui/story/StoryMenuState.hx 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(); } } From a599dbea11d34386ffce57883f9342c10e58fbc8 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 May 2023 16:42:58 -0400 Subject: [PATCH 2/4] Reworked Story Menu (data driven) --- Project.xml | 4 + source/funkin/Conductor.hx | 10 +- source/funkin/InitState.hx | 1 + source/funkin/MainMenuState.hx | 1 + source/funkin/data/BaseRegistry.hx | 7 +- source/funkin/data/level/LevelData.hx | 18 +- source/funkin/data/level/LevelRegistry.hx | 16 +- source/funkin/modding/PolymodHandler.hx | 1 + source/funkin/play/AnimationData.hx | 14 + source/funkin/play/GameOverSubstate.hx | 10 +- source/funkin/play/PlayState.hx | 1 + source/funkin/play/song/Song.hx | 12 +- source/funkin/play/stage/Bopper.hx | 1 + source/funkin/ui/StickerSubState.hx | 1 + source/funkin/ui/story/Level.hx | 16 +- source/funkin/ui/story/LevelProp.hx | 16 +- source/funkin/ui/story/LevelTitle.hx | 28 +- source/funkin/ui/story/StoryMenuState.hx | 339 +++++++++++++++++++--- 18 files changed, 416 insertions(+), 80 deletions(-) diff --git a/Project.xml b/Project.xml index 393248698..972749939 100644 --- a/Project.xml +++ b/Project.xml @@ -130,6 +130,7 @@ + @@ -144,6 +145,9 @@ + + + diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index d1176fa03..a0493869b 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -207,9 +207,15 @@ class Conductor } // FlxSignals are really cool. - if (currentStep != oldStep) stepHit.dispatch(); + if (currentStep != oldStep) + { + stepHit.dispatch(); + } - if (currentBeat != oldBeat) beatHit.dispatch(); + if (currentBeat != oldBeat) + { + beatHit.dispatch(); + } } @:deprecated // Switch to TimeChanges instead. diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index fea8899d2..e21cfda69 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -153,6 +153,7 @@ class InitState extends FlxTransitionableState // TODO: Register custom event callbacks here + funkin.data.level.LevelRegistry.instance.loadEntries(); SongEventParser.loadEventCache(); SongDataParser.loadSongCache(); StageDataParser.loadStageCache(); diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx index 85c91db93..e4bdfbe35 100644 --- a/source/funkin/MainMenuState.hx +++ b/source/funkin/MainMenuState.hx @@ -21,6 +21,7 @@ import funkin.shaderslmfao.ScreenWipeShader; import funkin.ui.AtlasMenuList; import funkin.ui.MenuList.MenuItem; import funkin.ui.MenuList; +import funkin.ui.story.StoryMenuState; import funkin.ui.OptionsState; import funkin.ui.PreferencesMenu; import funkin.ui.Prompt; diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 96eb43965..0864fddd9 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -66,7 +66,7 @@ abstract class BaseRegistry & Constructible = DataAssets.listDataFilesInPath(dataFilePath); + var entryIdList:Array = DataAssets.listDataFilesInPath('${dataFilePath}/'); var unscriptedEntryIds:Array = entryIdList.filter(function(entryId:String):Bool { return !entries.exists(entryId); }); @@ -101,6 +101,11 @@ abstract class BaseRegistry & Constructible + { + return entries.get(id); + } + public function toString():String { return 'Registry(' + registryId + ', ${countEntries()} entries)'; diff --git a/source/funkin/data/level/LevelData.hx b/source/funkin/data/level/LevelData.hx index abee74689..0342c3d39 100644 --- a/source/funkin/data/level/LevelData.hx +++ b/source/funkin/data/level/LevelData.hx @@ -13,7 +13,7 @@ typedef LevelData = * 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) + @:default(funkin.data.level.LevelRegistry.LEVEL_DATA_VERSION) var version:String; /** @@ -50,6 +50,14 @@ typedef LevelPropData = @:optional var scale:Float; + /** + * The opacity to render the prop at. + * @default 1.0 + */ + @:default(1.0) + @:optional + var alpha:Float; + /** * If true, the prop is a pixel sprite, and will be rendered without smoothing. */ @@ -59,11 +67,11 @@ typedef LevelPropData = /** * The frequency to bop at, in beats. - * @default 1.0 = every beat + * @default 1 = every beat, 2 = every other beat, etc. */ - @:default(1.0) + @:default(1) @:optional - var danceEvery:Float; + var danceEvery:Int; /** * The offset on the position to render the prop at. @@ -71,7 +79,7 @@ typedef LevelPropData = */ @:default([0, 0]) @:optional - var offset:Array; + var offsets:Array; /** * A set of animations to play on the prop. diff --git a/source/funkin/data/level/LevelRegistry.hx b/source/funkin/data/level/LevelRegistry.hx index 7447c467f..54ed81093 100644 --- a/source/funkin/data/level/LevelRegistry.hx +++ b/source/funkin/data/level/LevelRegistry.hx @@ -55,10 +55,10 @@ class LevelRegistry extends BaseRegistry } /** - * A list of all the story weeks, in order. + * A list of all the story weeks from the base game, in order. * TODO: Should this be hardcoded? */ - public function listDefaultLevelIds():String + public function listBaseGameLevelIds():Array { return [ "tutorial", @@ -70,6 +70,16 @@ class LevelRegistry extends BaseRegistry "week6", "week7", "weekend1" - ] + ]; + } + + /** + * A list of all installed story weeks that are not from the base game. + */ + public function listModdedLevelIds():Array + { + return listEntryIds().filter(function(id:String):Bool { + return listBaseGameLevelIds().indexOf(id) == -1; + }); } } diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 08b070835..a8e4a6109 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -277,6 +277,7 @@ class PolymodHandler // TODO: Reload event callbacks + funkin.data.level.LevelRegistry.instance.loadEntries(); SongDataParser.loadSongCache(); StageDataParser.loadStageCache(); CharacterDataParser.loadCharacterCache(); diff --git a/source/funkin/play/AnimationData.hx b/source/funkin/play/AnimationData.hx index 87bc10102..e512bb757 100644 --- a/source/funkin/play/AnimationData.hx +++ b/source/funkin/play/AnimationData.hx @@ -21,36 +21,48 @@ typedef AnimationData = * ONLY for use by MultiSparrow characters. * @default The assetPath of the parent sprite */ + @:default(null) + @:optional var assetPath:Null; /** * Offset the character's position by this amount when playing this animation. * @default [0, 0] */ + @:default([0, 0]) + @:optional var offsets:Null>; /** * Whether the animation should loop when it finishes. * @default false */ + @:default(false) + @:optional var looped:Null; /** * Whether the animation's sprites should be flipped horizontally. * @default false */ + @:default(false) + @:optional var flipX:Null; /** * Whether the animation's sprites should be flipped vertically. * @default false */ + @:default(false) + @:optional var flipY:Null; /** * The frame rate of the animation. * @default 24 */ + @:default(24) + @:optional var frameRate:Null; /** @@ -59,5 +71,7 @@ typedef AnimationData = * @example [0, 1, 2, 3] (use only the first four frames) * @default [] (all frames) */ + @:default([]) + @:optional var frameIndices:Null>; } diff --git a/source/funkin/play/GameOverSubstate.hx b/source/funkin/play/GameOverSubstate.hx index ddedf257c..3d5470324 100644 --- a/source/funkin/play/GameOverSubstate.hx +++ b/source/funkin/play/GameOverSubstate.hx @@ -3,6 +3,7 @@ package funkin.play; import flixel.FlxObject; import flixel.FlxSprite; import flixel.system.FlxSound; +import funkin.ui.story.StoryMenuState; import flixel.util.FlxColor; import flixel.util.FlxTimer; import funkin.modding.events.ScriptEvent; @@ -208,11 +209,9 @@ class GameOverSubstate extends MusicBeatSubstate boyfriend.playAnimation('deathConfirm' + animationSuffix, true); // After the animation finishes... - new FlxTimer().start(0.7, function(tmr:FlxTimer) - { + new FlxTimer().start(0.7, function(tmr:FlxTimer) { // ...fade out the graphics. Then after that happens... - FlxG.camera.fade(FlxColor.BLACK, 2, false, function() - { + FlxG.camera.fade(FlxColor.BLACK, 2, false, function() { // ...close the GameOverSubstate. FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true); PlayState.needsReset = true; @@ -276,8 +275,7 @@ class GameOverSubstate extends MusicBeatSubstate if (PreferencesMenu.getPref('censor-naughty')) randomCensor = [1, 3, 8, 13, 17, 21]; - FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), 1, false, null, true, function() - { + FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), 1, false, null, true, function() { // Once the quote ends, fade in the game over music. if (!isEnding && gameOverMusic != null) { diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 0d99744a1..afdf48ffd 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2,6 +2,7 @@ package funkin.play; import flixel.FlxCamera; import flixel.FlxObject; +import funkin.ui.story.StoryMenuState; import flixel.FlxSprite; import flixel.FlxState; import flixel.FlxSubState; diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index dde56be23..e1176f3b6 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -29,11 +29,14 @@ class Song // implements IPlayStateScriptedClass final variations:Array; final difficulties:Map; + var difficultyIds:Array; + public function new(id:String) { this.songId = id; variations = []; + difficultyIds = []; difficulties = new Map(); _metadata = SongDataParser.parseSongMetadata(songId); @@ -61,6 +64,8 @@ class Song // implements IPlayStateScriptedClass { for (diffId in metadata.playData.difficulties) { + difficultyIds.push(diffId); + var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation); variations.push(metadata.variation); @@ -138,7 +143,12 @@ class Song // implements IPlayStateScriptedClass public function listDifficulties():Array { - return difficulties.keys().array(); + return difficultyIds; + } + + public function hasDifficulty(diffId:String):Bool + { + return difficulties.exists(diffId); } /** diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 09aa910e0..623660af7 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -256,6 +256,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass var correctName = correctAnimationName(name); if (correctName == null) return; + this.animation.paused = false; this.animation.play(correctName, restart, false, 0); if (ignoreOther) diff --git a/source/funkin/ui/StickerSubState.hx b/source/funkin/ui/StickerSubState.hx index b300640a4..9658275e9 100644 --- a/source/funkin/ui/StickerSubState.hx +++ b/source/funkin/ui/StickerSubState.hx @@ -4,6 +4,7 @@ import flixel.FlxSprite; import haxe.Json; import lime.utils.Assets; // import flxtyped group +import funkin.ui.story.StoryMenuState; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.util.FlxTimer; import flixel.FlxG; diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index 0428fbbc4..3f334a148 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -1,6 +1,8 @@ package funkin.ui.story; import flixel.FlxSprite; +import flixel.util.FlxColor; +import funkin.play.song.Song; import funkin.data.IRegistryEntry; import funkin.data.level.LevelRegistry; import funkin.data.level.LevelData; @@ -66,9 +68,11 @@ class Level implements IRegistryEntry */ public function getSongDisplayNames(difficulty:String):Array { - return getSongs().map(function(songId) { - return funkin.play.song.SongData.SongDataParser.fetchSong(songId).getDifficulty(difficulty).songName; + var songList:Array = getSongs() ?? []; + var songNameList:Array = songList.map(function(songId) { + return funkin.play.song.SongData.SongDataParser.fetchSong(songId) ?.getDifficulty(difficulty) ?.songName ?? 'Unknown'; }); + return songNameList; } /** @@ -112,14 +116,15 @@ class Level implements IRegistryEntry var firstSongId:String = songList[0]; var firstSong:Song = funkin.play.song.SongData.SongDataParser.fetchSong(firstSongId); - for (difficulty in firstSong.getDifficulties()) + for (difficulty in firstSong.listDifficulties()) { difficulties.push(difficulty); } // Filter to only include difficulties that are present in all songs - for (songId in 1...songList.length) + for (songIndex in 1...songList.length) { + var songId:String = songList[songIndex]; var song:Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId); for (difficulty in difficulties) @@ -145,7 +150,10 @@ class Level implements IRegistryEntry var propData = _data.props[propIndex]; var propSprite:LevelProp = LevelProp.build(propData); propSprite.x += FlxG.width * 0.25 * propIndex; + props.push(propSprite); } + + return props; } public function destroy():Void {} diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx index 8f2ad5026..a474b363c 100644 --- a/source/funkin/ui/story/LevelProp.hx +++ b/source/funkin/ui/story/LevelProp.hx @@ -1,5 +1,9 @@ package funkin.ui.story; +import funkin.play.stage.Bopper; +import funkin.util.assets.FlxAnimationUtil; +import funkin.data.level.LevelData; + class LevelProp extends Bopper { public function new(danceEvery:Int) @@ -7,6 +11,11 @@ class LevelProp extends Bopper super(danceEvery); } + public function playConfirm():Void + { + playAnimation('confirm', true, true); + } + public static function build(propData:LevelPropData):Null { var isAnimated:Bool = propData.animations.length > 0; @@ -33,8 +42,8 @@ class LevelProp extends Bopper return null; } - prop.scale.set(propData.scale * (propData.isPixel ? 6 : 1)); - prop.updateHitbox(); + var scale:Float = propData.scale * (propData.isPixel ? 6 : 1); + prop.scale.set(scale, scale); prop.antialiasing = !propData.isPixel; prop.alpha = propData.alpha; prop.x = propData.offsets[0]; @@ -46,6 +55,9 @@ class LevelProp extends Bopper prop.setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]); } + prop.dance(); + prop.animation.paused = true; + return prop; } } diff --git a/source/funkin/ui/story/LevelTitle.hx b/source/funkin/ui/story/LevelTitle.hx index 7318c3e59..e1765d453 100644 --- a/source/funkin/ui/story/LevelTitle.hx +++ b/source/funkin/ui/story/LevelTitle.hx @@ -1,9 +1,10 @@ package funkin.ui.story; -import funkin.CoolUtil; import flixel.FlxSprite; import flixel.graphics.frames.FlxAtlasFrames; import flixel.group.FlxSpriteGroup; +import flixel.util.FlxColor; +import funkin.CoolUtil; class LevelTitle extends FlxSpriteGroup { @@ -24,16 +25,27 @@ class LevelTitle extends FlxSpriteGroup super(x, y); this.level = level; - } - public override function create():Void - { - super.create(); + if (this.level == null) throw "Level cannot be null!"; buildLevelTitle(); buildLevelLock(); } + override function get_width():Float + { + if (length == 0) return 0; + + if (lock.visible) + { + return title.width + lock.width + LOCK_PAD; + } + else + { + return title.width; + } + } + // 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?? @@ -42,12 +54,12 @@ class LevelTitle extends FlxSpriteGroup public override function update(elapsed:Float):Void { - this.y = CoolUtil.coolLerp(y, (targetY * 120) + 480, 0.17); + this.y = CoolUtil.coolLerp(y, targetY, 0.17); if (isFlashing) flashingInt += 1; - if (flashingInt % fakeFramerate >= Math.floor(fakeFramerate / 2)) week.color = 0xFF33ffff; + if (flashingInt % fakeFramerate >= Math.floor(fakeFramerate / 2)) title.color = 0xFF33ffff; else - week.color = FlxColor.WHITE; + title.color = FlxColor.WHITE; } public function showLock():Void diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index c1ce448eb..9a854128b 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -1,5 +1,20 @@ package funkin.ui.story; +import flixel.addons.transition.FlxTransitionableState; +import flixel.FlxSprite; +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.text.FlxText; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import flixel.util.FlxTimer; +import funkin.data.level.LevelRegistry; +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEventDispatcher; +import funkin.play.PlayState; +import funkin.play.song.SongData.SongDataParser; +import funkin.util.Constants; + class StoryMenuState extends MusicBeatState { static final DEFAULT_BACKGROUND_COLOR:FlxColor = FlxColor.fromString("#F9CF51"); @@ -10,12 +25,15 @@ class StoryMenuState extends MusicBeatState var currentLevelId:String = 'tutorial'; var currentLevel:Level; var isLevelUnlocked:Bool; + var currentLevelTitle:LevelTitle; var highScore:Int = 42069420; var highScoreLerp:Int = 12345678; var exitingMenu:Bool = false; - var selectedWeek:Bool = false; + var selectedLevel:Bool = false; + + var displayingModdedLevels:Bool = false; // // RENDER OBJECTS @@ -37,7 +55,7 @@ class StoryMenuState extends MusicBeatState var tracklistText:FlxText; /** - * The title of the week in the middle. + * The titles of the levels in the middle. */ var levelTitles:FlxTypedGroup; @@ -66,18 +84,26 @@ class StoryMenuState extends MusicBeatState */ var difficultySprite:FlxSprite; + var difficultySprites:Map; + + var stickerSubState:StickerSubState; + public function new(?stickers:StickerSubState = null) { + super(); + if (stickers != null) { stickerSubState = stickers; } - - super(); } override function create():Void { + super.create(); + + difficultySprites = new Map(); + transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; @@ -101,92 +127,131 @@ class StoryMenuState extends MusicBeatState persistentUpdate = persistentDraw = true; + updateData(); + // 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); + updateBackground(); levelProps = new FlxTypedGroup(); + levelProps.zIndex = 1000; add(levelProps); + updateProps(); + 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 = new FlxText(FlxG.width * 0.05, levelBackground.x + levelBackground.height + 100, 0, "Tracks", 32); + tracklistText.setFormat("VCR OSD Mono", 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 = new FlxText(FlxG.width * 0.7, 10, 0, 'LEVEL 1'); levelTitleText.setFormat("VCR OSD Mono", 32, FlxColor.WHITE, RIGHT); levelTitleText.alpha = 0.7; add(levelTitleText); - buildLevelTitles(false); + buildLevelTitles(); - 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); + leftDifficultyArrow = new FlxSprite(levelTitles.members[0].x + levelTitles.members[0].width + 10, levelTitles.members[0].y + 10); + leftDifficultyArrow.frames = Paths.getSparrowAtlas('storymenu/ui/arrows'); + leftDifficultyArrow.animation.addByPrefix('idle', 'leftIdle0'); + leftDifficultyArrow.animation.addByPrefix('press', 'leftConfirm0'); + leftDifficultyArrow.animation.play('idle'); + add(leftDifficultyArrow); - 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); + buildDifficultySprite(); + + rightDifficultyArrow = new FlxSprite(difficultySprite.x + difficultySprite.width + 10, leftDifficultyArrow.y); + rightDifficultyArrow.frames = leftDifficultyArrow.frames; + rightDifficultyArrow.animation.addByPrefix('idle', 'rightIdle0'); + rightDifficultyArrow.animation.addByPrefix('press', 'rightConfirm0'); + rightDifficultyArrow.animation.play('idle'); + add(rightDifficultyArrow); - difficultySprite = buildDifficultySprite(); - changeDifficulty(); add(difficultySprite); + updateText(); + changeDifficulty(); + changeLevel(); + refresh(); + #if discord_rpc // Updating Discord Rich Presence DiscordClient.changePresence("In the Menus", null); #end } - function buildDifficultySprite():Void + function updateData():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'); + currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId); + isLevelUnlocked = currentLevel == null ? false : currentLevel.isUnlocked(); } - function buildLevelTitles(moddedLevels:Bool):Void + function buildDifficultySprite():Void + { + remove(difficultySprite); + difficultySprite = difficultySprites.get(currentDifficultyId); + if (difficultySprite == null) + { + difficultySprite = new FlxSprite(leftDifficultyArrow.x + leftDifficultyArrow.width + 10, leftDifficultyArrow.y); + difficultySprite.loadGraphic(Paths.image('storymenu/difficulties/${currentDifficultyId}')); + difficultySprites.set(currentDifficultyId, difficultySprite); + + difficultySprite.x += (difficultySprites.get('normal').width - difficultySprite.width) / 2; + } + difficultySprite.alpha = 0; + difficultySprite.y = leftDifficultyArrow.y - 15; + FlxTween.tween(difficultySprite, {y: leftDifficultyArrow.y + 15, alpha: 1}, 0.07); + add(difficultySprite); + } + + function buildLevelTitles():Void { levelTitles.clear(); - var levelIds:Array = LevelRegistry.instance.getLevelIds(); + var levelIds:Array = displayingModdedLevels ? LevelRegistry.instance.listModdedLevelIds() : LevelRegistry.instance.listBaseGameLevelIds(); + if (levelIds.length == 0) levelIds = ['tutorial']; + 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); + if (level == null) continue; + + var levelTitleItem:LevelTitle = new LevelTitle(0, Std.int(levelBackground.y + levelBackground.height + 10), level); + levelTitleItem.targetY = ((levelTitleItem.height + 20) * levelIndex); + levelTitleItem.screenCenter(X); levelTitles.add(levelTitleItem); } } + function switchMode(moddedLevels:Bool):Void + { + displayingModdedLevels = moddedLevels; + buildLevelTitles(); + + changeLevel(0); + changeDifficulty(0); + } + override function update(elapsed:Float) { - highScoreLerp = CoolUtil.coolLerp(highScoreLerp, highScore, 0.5); + Conductor.update(); - scoreText.text = 'WEEK SCORE: ${Math.round(highScoreLerp)}'; + highScoreLerp = Std.int(CoolUtil.coolLerp(highScoreLerp, highScore, 0.5)); - txtWeekTitle.text = weekNames[curWeek].toUpperCase(); - txtWeekTitle.x = FlxG.width - (txtWeekTitle.width + 10); + scoreText.text = 'LEVEL SCORE: ${Math.round(highScoreLerp)}'; + + levelTitleText.text = currentLevel.getTitle(); + levelTitleText.x = FlxG.width - (levelTitleText.width + 10); // Right align. handleKeyPresses(); @@ -197,7 +262,7 @@ class StoryMenuState extends MusicBeatState { if (!exitingMenu) { - if (!selectedWeek) + if (!selectedLevel) { if (controls.UI_UP_P) { @@ -211,20 +276,20 @@ class StoryMenuState extends MusicBeatState if (controls.UI_RIGHT) { - rightArrow.animation.play('press') + rightDifficultyArrow.animation.play('press'); } else { - rightArrow.animation.play('idle'); + rightDifficultyArrow.animation.play('idle'); } if (controls.UI_LEFT) { - leftArrow.animation.play('press'); + leftDifficultyArrow.animation.play('press'); } else { - leftArrow.animation.play('idle'); + leftDifficultyArrow.animation.play('idle'); } if (controls.UI_RIGHT_P) @@ -236,15 +301,20 @@ class StoryMenuState extends MusicBeatState { changeDifficulty(-1); } + + if (FlxG.keys.justPressed.TAB) + { + switchMode(!displayingModdedLevels); + } } if (controls.ACCEPT) { - selectWeek(); + selectLevel(); } } - if (controls.BACK && !exitingMenu && !selectedWeek) + if (controls.BACK && !exitingMenu && !selectedLevel) { FlxG.sound.play(Paths.sound('cancelMenu')); exitingMenu = true; @@ -252,7 +322,180 @@ class StoryMenuState extends MusicBeatState } } - function changeLevel(change:Int = 0):Void {} + /** + * Changes the selected level. + * @param change +1 (down), -1 (up) + */ + function changeLevel(change:Int = 0):Void + { + var levelList:Array = displayingModdedLevels ? LevelRegistry.instance.listModdedLevelIds() : LevelRegistry.instance.listBaseGameLevelIds(); + if (levelList.length == 0) levelList = ['tutorial']; - function changeDifficulty(change:Int = 0):Void {} + var currentIndex:Int = levelList.indexOf(currentLevelId); + + currentIndex += change; + + // Wrap around + if (currentIndex < 0) currentIndex = levelList.length - 1; + if (currentIndex >= levelList.length) currentIndex = 0; + + currentLevelId = levelList[currentIndex]; + + updateData(); + + for (index in 0...levelTitles.members.length) + { + var item:LevelTitle = levelTitles.members[index]; + + item.targetY = (index - currentIndex) * 120 + 480; + + if (index == currentIndex) + { + currentLevelTitle = item; + item.alpha = 1.0; + } + else if (index > currentIndex) + { + item.alpha = 0.6; + } + else + { + item.alpha = 0.0; + } + } + + updateText(); + updateBackground(); + updateProps(); + refresh(); + } + + /** + * Changes the selected difficulty. + * @param change +1 (right) to increase difficulty, -1 (left) to decrease difficulty + */ + function changeDifficulty(change:Int = 0):Void + { + var difficultyList:Array = currentLevel.getDifficulties(); + var currentIndex:Int = difficultyList.indexOf(currentDifficultyId); + + currentIndex += change; + + // Wrap around + if (currentIndex < 0) currentIndex = difficultyList.length - 1; + if (currentIndex >= difficultyList.length) currentIndex = 0; + + currentDifficultyId = difficultyList[currentIndex]; + + buildDifficultySprite(); + } + + override function dispatchEvent(event:ScriptEvent):Void + { + // super.dispatchEvent(event) dispatches event to module scripts. + super.dispatchEvent(event); + + if ((levelProps?.length ?? 0) > 0) + { + // Dispatch event to props. + for (prop in levelProps) + { + ScriptEventDispatcher.callEvent(prop, event); + } + } + } + + function selectLevel() + { + if (!currentLevel.isUnlocked()) + { + FlxG.sound.play(Paths.sound('cancelMenu')); + return; + } + + if (selectedLevel) return; + + selectedLevel = true; + + FlxG.sound.play(Paths.sound('confirmMenu')); + + currentLevelTitle.isFlashing = true; + + for (prop in levelProps.members) + { + prop.playConfirm(); + } + + PlayState.storyPlaylist = currentLevel.getSongs(); + PlayState.isStoryMode = true; + + PlayState.currentSong = SongLoad.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), PlayState.storyPlaylist[0].toLowerCase()); + PlayState.currentSong_NEW = SongDataParser.fetchSong(PlayState.storyPlaylist[0].toLowerCase()); + + // TODO: Fix this. + PlayState.storyWeek = 0; + PlayState.campaignScore = 0; + + // TODO: Fix this. + PlayState.storyDifficulty = 0; + PlayState.storyDifficulty_NEW = currentDifficultyId; + + SongLoad.curDiff = PlayState.storyDifficulty_NEW; + + new FlxTimer().start(1, function(tmr:FlxTimer) { + LoadingState.loadAndSwitchState(new PlayState(), true); + }); + } + + function updateBackground():Void + { + if (levelBackground != null) + { + var oldBackground:FlxSprite = levelBackground; + + FlxTween.tween(oldBackground, {alpha: 0.0}, 0.6, + { + ease: FlxEase.linear, + onComplete: function(_) { + remove(oldBackground); + } + }); + } + + levelBackground = currentLevel.buildBackground(); + levelBackground.x = 0; + levelBackground.y = 56; + levelBackground.alpha = 0.0; + levelBackground.zIndex = 100; + add(levelBackground); + + FlxTween.tween(levelBackground, {alpha: 1.0}, 0.6, + { + ease: FlxEase.linear + }); + } + + function updateProps():Void + { + levelProps.clear(); + for (prop in currentLevel.buildProps()) + { + prop.zIndex = 1000; + levelProps.add(prop); + } + + refresh(); + } + + function updateText():Void + { + tracklistText.text = 'TRACKS\n\n'; + tracklistText.text += currentLevel.getSongDisplayNames(currentDifficultyId).join('\n'); + + tracklistText.screenCenter(X); + tracklistText.x -= FlxG.width * 0.35; + + // TODO: Fix this. + highScore = Highscore.getWeekScore(0, 0); + } } From 09654170ed98aa1bd48c798619e6e7e0af440761 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 22 May 2023 13:47:01 -0400 Subject: [PATCH 3/4] Erect mixes play properly when selected --- source/funkin/Paths.hx | 4 ++-- source/funkin/play/song/Song.hx | 5 +++-- source/funkin/play/song/SongData.hx | 12 +++++++++--- source/funkin/ui/story/Level.hx | 2 ++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index 3a1c65285..60dcfad38 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -103,9 +103,9 @@ class Paths return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT'; } - inline static public function inst(song:String) + inline static public function inst(song:String, ?suffix:String) { - return 'songs:assets/songs/${song.toLowerCase()}/Inst.$SOUND_EXT'; + return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT'; } inline static public function image(key:String, ?library:String) diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index e1176f3b6..871cdd713 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -253,7 +253,8 @@ class SongDifficulty public inline function playInst(volume:Float = 1.0, looped:Bool = false) { - FlxG.sound.playMusic(Paths.inst(this.song.songId), volume, looped); + var suffix:String = variation == null ? null : '-$variation'; + FlxG.sound.playMusic(Paths.inst(this.song.songId, suffix), volume, looped); } public inline function cacheVocals() @@ -265,7 +266,7 @@ class SongDifficulty { // TODO: Implement. - return [""]; + return [variation == null ? '' : '-$variation']; } public function buildVocals(charId:String = "bf"):VoicesGroup diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index 60ae32ec1..c5a886ba9 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -143,9 +143,15 @@ class SongDataParser for (variation in variations) { - var variationRawJson:String = loadSongMetadataFile(songId, variation); - var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationRawJson, '${songId}_${variation}'); - variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}_${variation}'); + var variationJsonStr:String = loadSongMetadataFile(songId, variation); + var variationJsonData:Dynamic = null; + try + { + variationJsonData = Json.parse(variationJsonStr); + } + catch (e) {} + var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationJsonData, '${songId}-${variation}'); + variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}-${variation}'); if (variationSongMetadata != null) { variationSongMetadata.variation = variation; diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index 3f334a148..f44023690 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -127,6 +127,8 @@ class Level implements IRegistryEntry var songId:String = songList[songIndex]; var song:Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId); + if (song == null) continue; + for (difficulty in difficulties) { if (!song.hasDifficulty(difficulty)) From 482de2a12276ade9a5a4373d55bb17ca3f7dc647 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 22 May 2023 20:55:53 -0400 Subject: [PATCH 4/4] Added Nightmare difficulty (stub) --- hmm.json | 4 +- source/funkin/InitState.hx | 2 + source/funkin/LatencyState.hx | 15 +++---- source/funkin/ui/story/Level.hx | 9 +++- source/funkin/ui/story/StoryMenuState.hx | 56 ++++++++++++++++++++++-- 5 files changed, 69 insertions(+), 17 deletions(-) diff --git a/hmm.json b/hmm.json index 0ef6974ee..572d4d3d3 100644 --- a/hmm.json +++ b/hmm.json @@ -11,7 +11,7 @@ "name": "flixel", "type": "git", "dir": null, - "ref": "d6100cc8", + "ref": "3de38a0", "url": "https://github.com/EliteMasterEric/flixel" }, { @@ -118,4 +118,4 @@ "version": "0.2.2" } ] -} \ No newline at end of file +} diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index e21cfda69..45c2645df 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -78,6 +78,7 @@ class InitState extends FlxTransitionableState } }); + #if FLX_DEBUG FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() { FlxG.game.debugger.vcr.onStep(); @@ -90,6 +91,7 @@ class InitState extends FlxTransitionableState FlxG.sound.music.pause(); FlxG.sound.music.time += FlxG.elapsed * 1000; }); + #end FlxG.sound.muteKeys = [ZERO]; FlxG.game.focusLostFramerate = 60; diff --git a/source/funkin/LatencyState.hx b/source/funkin/LatencyState.hx index 256efde5c..04a14b90a 100644 --- a/source/funkin/LatencyState.hx +++ b/source/funkin/LatencyState.hx @@ -31,7 +31,7 @@ class LatencyState extends MusicBeatSubstate var offsetsPerBeat:Array = []; var swagSong:HomemadeMusic; - #if debug + #if FLX_DEBUG var funnyStatsGraph:CoolStatsGraph; var realStats:CoolStatsGraph; #end @@ -44,7 +44,7 @@ class LatencyState extends MusicBeatSubstate FlxG.sound.music = swagSong; FlxG.sound.music.play(); - #if debug + #if FLX_DEBUG funnyStatsGraph = new CoolStatsGraph(0, Std.int(FlxG.height / 2), FlxG.width, Std.int(FlxG.height / 2), FlxColor.PINK, "time"); FlxG.addChildBelowMouse(funnyStatsGraph); @@ -52,8 +52,7 @@ class LatencyState extends MusicBeatSubstate FlxG.addChildBelowMouse(realStats); #end - FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, key -> - { + FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, key -> { trace(key.charCode); if (key.charCode == 120) generateBeatStuff(); @@ -154,8 +153,7 @@ class LatencyState extends MusicBeatSubstate override function beatHit():Bool { - if (curBeat % 8 == 0) blocks.forEach(blok -> - { + if (curBeat % 8 == 0) blocks.forEach(blok -> { blok.alpha = 0; }); @@ -172,7 +170,7 @@ class LatencyState extends MusicBeatSubstate trace(FlxG.sound.music._channel.position); */ - #if debug + #if FLX_DEBUG funnyStatsGraph.update(FlxG.sound.music.time % 500); realStats.update(swagSong.getTimeWithDiff() % 500); #end @@ -248,8 +246,7 @@ class LatencyState extends MusicBeatSubstate FlxG.resetState(); }*/ - noteGrp.forEach(function(daNote:Note) - { + noteGrp.forEach(function(daNote:Note) { daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.audioOffset) - daNote.data.strumTime) * 0.45); daNote.x = strumLine.x + 30; diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index f44023690..5d24de312 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -116,9 +116,12 @@ class Level implements IRegistryEntry var firstSongId:String = songList[0]; var firstSong:Song = funkin.play.song.SongData.SongDataParser.fetchSong(firstSongId); - for (difficulty in firstSong.listDifficulties()) + if (firstSong != null) { - difficulties.push(difficulty); + for (difficulty in firstSong.listDifficulties()) + { + difficulties.push(difficulty); + } } // Filter to only include difficulties that are present in all songs @@ -138,6 +141,8 @@ class Level implements IRegistryEntry } } + if (difficulties.length == 0) difficulties = ['normal']; + return difficulties; } diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 9a854128b..8a856baf6 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -1,5 +1,6 @@ package funkin.ui.story; +import openfl.utils.Assets; import flixel.addons.transition.FlxTransitionableState; import flixel.FlxSprite; import flixel.group.FlxGroup.FlxTypedGroup; @@ -202,14 +203,29 @@ class StoryMenuState extends MusicBeatState if (difficultySprite == null) { difficultySprite = new FlxSprite(leftDifficultyArrow.x + leftDifficultyArrow.width + 10, leftDifficultyArrow.y); - difficultySprite.loadGraphic(Paths.image('storymenu/difficulties/${currentDifficultyId}')); + + if (Assets.exists(Paths.file('images/storymenu/difficulties/${currentDifficultyId}.xml'))) + { + difficultySprite.frames = Paths.getSparrowAtlas('storymenu/difficulties/${currentDifficultyId}'); + difficultySprite.animation.addByPrefix('idle', 'idle0', 24, true); + difficultySprite.animation.play('idle'); + } + else + { + difficultySprite.loadGraphic(Paths.image('storymenu/difficulties/${currentDifficultyId}')); + } + difficultySprites.set(currentDifficultyId, difficultySprite); difficultySprite.x += (difficultySprites.get('normal').width - difficultySprite.width) / 2; } difficultySprite.alpha = 0; + difficultySprite.y = leftDifficultyArrow.y - 15; - FlxTween.tween(difficultySprite, {y: leftDifficultyArrow.y + 15, alpha: 1}, 0.07); + var targetY:Float = leftDifficultyArrow.y + 10; + targetY -= (difficultySprite.height - difficultySprites.get('normal').height) / 2; + FlxTween.tween(difficultySprite, {y: targetY, alpha: 1}, 0.07); + add(difficultySprite); } @@ -218,7 +234,7 @@ class StoryMenuState extends MusicBeatState levelTitles.clear(); var levelIds:Array = displayingModdedLevels ? LevelRegistry.instance.listModdedLevelIds() : LevelRegistry.instance.listBaseGameLevelIds(); - if (levelIds.length == 0) levelIds = ['tutorial']; + if (levelIds.length == 0) levelIds = ['tutorial']; // Make sure there's at least one level to display. for (levelIndex in 0...levelIds.length) { @@ -267,11 +283,13 @@ class StoryMenuState extends MusicBeatState if (controls.UI_UP_P) { changeLevel(-1); + changeDifficulty(0); } if (controls.UI_DOWN_P) { changeLevel(1); + changeDifficulty(0); } if (controls.UI_RIGHT) @@ -385,9 +403,39 @@ class StoryMenuState extends MusicBeatState if (currentIndex < 0) currentIndex = difficultyList.length - 1; if (currentIndex >= difficultyList.length) currentIndex = 0; + var hasChanged:Bool = currentDifficultyId != difficultyList[currentIndex]; currentDifficultyId = difficultyList[currentIndex]; - buildDifficultySprite(); + if (difficultyList.length <= 1) + { + leftDifficultyArrow.visible = false; + rightDifficultyArrow.visible = false; + } + else + { + leftDifficultyArrow.visible = true; + rightDifficultyArrow.visible = true; + } + + if (hasChanged) + { + buildDifficultySprite(); + funnyMusicThing(); + } + } + + final FADE_OUT_TIME:Float = 1.5; + + function funnyMusicThing():Void + { + if (currentDifficultyId == "nightmare") + { + FlxG.sound.music.fadeOut(FADE_OUT_TIME, 0.0); + } + else + { + FlxG.sound.music.fadeOut(FADE_OUT_TIME, 1.0); + } } override function dispatchEvent(event:ScriptEvent):Void