mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-14 19:25:16 -05:00
Work in progress on story menu data storage rework
This commit is contained in:
parent
03cab4a326
commit
021f7a0a1c
13 changed files with 925 additions and 4 deletions
5
hmm.json
5
hmm.json
|
@ -86,6 +86,11 @@
|
|||
"type": "haxelib",
|
||||
"version": null
|
||||
},
|
||||
{
|
||||
"name": "json2object",
|
||||
"type": "haxelib",
|
||||
"version": null
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "git",
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
public function listDifficulties():Array<String>
|
||||
{
|
||||
return difficulties.keys().array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
/**
|
||||
* 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>
|
||||
{
|
||||
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>
|
||||
{
|
||||
return [for (i in map.keys()) i];
|
||||
return map.keys().array();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue