mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-07-19 10:42:30 +00:00
Work in progress on story menu data storage rework
This commit is contained in:
parent
03cab4a326
commit
021f7a0a1c
5
hmm.json
5
hmm.json
|
@ -86,6 +86,11 @@
|
||||||
"type": "haxelib",
|
"type": "haxelib",
|
||||||
"version": null
|
"version": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "json2object",
|
||||||
|
"type": "haxelib",
|
||||||
|
"version": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "lime",
|
"name": "lime",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -122,10 +122,10 @@ class StoryMenuState extends MusicBeatState
|
||||||
|
|
||||||
persistentUpdate = persistentDraw = true;
|
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);
|
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.setFormat("VCR OSD Mono", 32, FlxColor.WHITE, RIGHT);
|
||||||
txtWeekTitle.alpha = 0.7;
|
txtWeekTitle.alpha = 0.7;
|
||||||
|
|
||||||
|
|
162
source/funkin/data/BaseRegistry.hx
Normal file
162
source/funkin/data/BaseRegistry.hx
Normal file
|
@ -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<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J>
|
||||||
|
{
|
||||||
|
public final registryId:String;
|
||||||
|
|
||||||
|
final dataFilePath:String;
|
||||||
|
|
||||||
|
final entries:Map<String, T>;
|
||||||
|
|
||||||
|
// public abstract static final instance:BaseRegistry<T, J> = 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<String, T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadEntries():Void
|
||||||
|
{
|
||||||
|
clearEntries();
|
||||||
|
|
||||||
|
//
|
||||||
|
// SCRIPTED ENTRIES
|
||||||
|
//
|
||||||
|
var scriptedEntryClassNames:Array<String> = 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<String> = DataAssets.listDataFilesInPath(dataFilePath);
|
||||||
|
var unscriptedEntryIds:Array<String> = 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<String>
|
||||||
|
{
|
||||||
|
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<J>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list of scripted class names to load.
|
||||||
|
* @return An array of scripted class names.
|
||||||
|
*/
|
||||||
|
abstract function getScriptedClassNames():Array<String>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an entry from the given ID.
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
function createEntry(id:String):Null<T>
|
||||||
|
{
|
||||||
|
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<T>;
|
||||||
|
}
|
19
source/funkin/data/IRegistryEntry.hx
Normal file
19
source/funkin/data/IRegistryEntry.hx
Normal file
|
@ -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<T>
|
||||||
|
{
|
||||||
|
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<T>;
|
||||||
|
}
|
83
source/funkin/data/level/LevelData.hx
Normal file
83
source/funkin/data/level/LevelData.hx
Normal file
|
@ -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<LevelPropData>;
|
||||||
|
@:default(["bopeebo"])
|
||||||
|
var songs:Array<String>;
|
||||||
|
@: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<Float>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of animations to play on the prop.
|
||||||
|
* If default/empty, the prop will be static.
|
||||||
|
*/
|
||||||
|
@:default([])
|
||||||
|
@:optional
|
||||||
|
var animations:Array<AnimationData>;
|
||||||
|
}
|
75
source/funkin/data/level/LevelRegistry.hx
Normal file
75
source/funkin/data/level/LevelRegistry.hx
Normal file
|
@ -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<Level, LevelData>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 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<LevelData>
|
||||||
|
{
|
||||||
|
// JsonParser does not take type parameters,
|
||||||
|
// otherwise this function would be in BaseRegistry.
|
||||||
|
var parser = new json2object.JsonParser<LevelData>();
|
||||||
|
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<String>
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -136,6 +136,11 @@ class Song // implements IPlayStateScriptedClass
|
||||||
return difficulties.get(diffId);
|
return difficulties.get(diffId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function listDifficulties():Array<String>
|
||||||
|
{
|
||||||
|
return difficulties.keys().array();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purge the cached chart data for each difficulty of this song.
|
* Purge the cached chart data for each difficulty of this song.
|
||||||
*/
|
*/
|
||||||
|
|
162
source/funkin/ui/story/Level.hx
Normal file
162
source/funkin/ui/story/Level.hx
Normal file
|
@ -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<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>
|
||||||
|
{
|
||||||
|
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<String>
|
||||||
|
*/
|
||||||
|
public function getSongDisplayNames(difficulty:String):Array<String>
|
||||||
|
{
|
||||||
|
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<String>
|
||||||
|
{
|
||||||
|
var difficulties:Array<String> = [];
|
||||||
|
|
||||||
|
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<LevelProp>
|
||||||
|
{
|
||||||
|
var props:Array<LevelProp> = [];
|
||||||
|
|
||||||
|
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<LevelData>
|
||||||
|
{
|
||||||
|
return LevelRegistry.instance.parseEntryData(id);
|
||||||
|
}
|
||||||
|
}
|
51
source/funkin/ui/story/LevelProp.hx
Normal file
51
source/funkin/ui/story/LevelProp.hx
Normal file
|
@ -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<LevelProp>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
78
source/funkin/ui/story/LevelTitle.hx
Normal file
78
source/funkin/ui/story/LevelTitle.hx
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
9
source/funkin/ui/story/ScriptedLevel.hx
Normal file
9
source/funkin/ui/story/ScriptedLevel.hx
Normal file
|
@ -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 {}
|
258
source/funkin/ui/story/StoryMenuState.hx
Normal file
258
source/funkin/ui/story/StoryMenuState.hx
Normal file
|
@ -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<LevelTitle>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props in the center.
|
||||||
|
*/
|
||||||
|
var levelProps:FlxTypedGroup<LevelProp>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<LevelTitle>();
|
||||||
|
add(levelTitles);
|
||||||
|
|
||||||
|
levelBackground = new FlxSprite(0, 56).makeGraphic(FlxG.width, BACKGROUND_HEIGHT, DEFAULT_BACKGROUND_COLOR);
|
||||||
|
add(levelBackground);
|
||||||
|
|
||||||
|
levelProps = new FlxTypedGroup<LevelProp>();
|
||||||
|
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<String> = 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 {}
|
||||||
|
}
|
|
@ -9,13 +9,27 @@ package funkin.util.tools;
|
||||||
*/
|
*/
|
||||||
class MapTools
|
class MapTools
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Return the quantity of keys in the map.
|
||||||
|
*/
|
||||||
|
public static function size<K, T>(map:Map<K, T>):Int
|
||||||
|
{
|
||||||
|
return map.keys().array().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of values from the map, as an array.
|
||||||
|
*/
|
||||||
public static function values<K, T>(map:Map<K, T>):Array<T>
|
public static function values<K, T>(map:Map<K, T>):Array<T>
|
||||||
{
|
{
|
||||||
return [for (i in map.iterator()) i];
|
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<K, T>(map:Map<K, T>):Array<K>
|
public static function keyValues<K, T>(map:Map<K, T>):Array<K>
|
||||||
{
|
{
|
||||||
return [for (i in map.keys()) i];
|
return map.keys().array();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue