mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-23 08:07:54 -05:00
Merge branch 'master' into feature/split-vocals
This commit is contained in:
commit
2af4a51b15
26 changed files with 1352 additions and 26 deletions
|
@ -130,6 +130,7 @@
|
|||
<haxelib name="polymod" /> <!-- Modding framework -->
|
||||
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
|
||||
<!-- <haxelib name="hxcodec" /> Video playback -->
|
||||
<haxelib name="json2object" /> <!-- JSON parsing -->
|
||||
|
||||
<haxelib name="thx.semver" />
|
||||
|
||||
|
@ -144,6 +145,9 @@
|
|||
<!--Enable this for Nape release builds for a serious peformance improvement-->
|
||||
<haxedef name="NAPE_RELEASE_BUILD" unless="debug" />
|
||||
|
||||
<!-- TODO: REMOVE THIS!!!! -->
|
||||
<haxeflag name="-w" value="-WDeprecated" />
|
||||
|
||||
<!-- _________________________________ Custom _______________________________ -->
|
||||
|
||||
<!-- Disable trace() calls in release builds to bump up performance. -->
|
||||
|
|
17
hmm.json
17
hmm.json
|
@ -11,7 +11,7 @@
|
|||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d6100cc8",
|
||||
"ref": "32cee07",
|
||||
"url": "https://github.com/EliteMasterEric/flixel"
|
||||
},
|
||||
{
|
||||
|
@ -42,14 +42,14 @@
|
|||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "59157d2",
|
||||
"ref": "08fbc9d",
|
||||
"url": "https://github.com/haxeui/haxeui-core/"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d353389",
|
||||
"ref": "999fadd",
|
||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||
},
|
||||
{
|
||||
|
@ -68,13 +68,13 @@
|
|||
"name": "hxcodec",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d74c2aa",
|
||||
"ref": "91adeec",
|
||||
"url": "https://github.com/polybiusproxy/hxCodec"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp",
|
||||
"type": "haxelib",
|
||||
"version": "4.2.1"
|
||||
"version": "4.3.2"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp-debug-server",
|
||||
|
@ -84,13 +84,18 @@
|
|||
{
|
||||
"name": "hxp",
|
||||
"type": "haxelib",
|
||||
"version": "1.2.2"
|
||||
},
|
||||
{
|
||||
"name": "json2object",
|
||||
"type": "haxelib",
|
||||
"version": null
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "afadf5f",
|
||||
"ref": "deecd6c",
|
||||
"url": "https://github.com/openfl/lime"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
@ -153,6 +155,7 @@ class InitState extends FlxTransitionableState
|
|||
|
||||
// TODO: Register custom event callbacks here
|
||||
|
||||
funkin.data.level.LevelRegistry.instance.loadEntries();
|
||||
SongEventParser.loadEventCache();
|
||||
SongDataParser.loadSongCache();
|
||||
StageDataParser.loadStageCache();
|
||||
|
|
|
@ -31,7 +31,7 @@ class LatencyState extends MusicBeatSubstate
|
|||
var offsetsPerBeat:Array<Int> = [];
|
||||
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);
|
||||
|
||||
|
@ -170,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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
167
source/funkin/data/BaseRegistry.hx
Normal file
167
source/funkin/data/BaseRegistry.hx
Normal file
|
@ -0,0 +1,167 @@
|
|||
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 fetchEntry(id:String):Null<T>
|
||||
{
|
||||
return entries.get(id);
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
91
source/funkin/data/level/LevelData.hx
Normal file
91
source/funkin/data/level/LevelData.hx
Normal file
|
@ -0,0 +1,91 @@
|
|||
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(funkin.data.level.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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* The frequency to bop at, in beats.
|
||||
* @default 1 = every beat, 2 = every other beat, etc.
|
||||
*/
|
||||
@:default(1)
|
||||
@:optional
|
||||
var danceEvery:Int;
|
||||
|
||||
/**
|
||||
* The offset on the position to render the prop at.
|
||||
* @default [0.0, 0.0]
|
||||
*/
|
||||
@:default([0, 0])
|
||||
@:optional
|
||||
var offsets: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>;
|
||||
}
|
85
source/funkin/data/level/LevelRegistry.hx
Normal file
85
source/funkin/data/level/LevelRegistry.hx
Normal file
|
@ -0,0 +1,85 @@
|
|||
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 from the base game, in order.
|
||||
* TODO: Should this be hardcoded?
|
||||
*/
|
||||
public function listBaseGameLevelIds():Array<String>
|
||||
{
|
||||
return [
|
||||
"tutorial",
|
||||
"week1",
|
||||
"week2",
|
||||
"week3",
|
||||
"week4",
|
||||
"week5",
|
||||
"week6",
|
||||
"week7",
|
||||
"weekend1"
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all installed story weeks that are not from the base game.
|
||||
*/
|
||||
public function listModdedLevelIds():Array<String>
|
||||
{
|
||||
return listEntryIds().filter(function(id:String):Bool {
|
||||
return listBaseGameLevelIds().indexOf(id) == -1;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -277,6 +277,7 @@ class PolymodHandler
|
|||
|
||||
// TODO: Reload event callbacks
|
||||
|
||||
funkin.data.level.LevelRegistry.instance.loadEntries();
|
||||
SongDataParser.loadSongCache();
|
||||
StageDataParser.loadStageCache();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
|
|
|
@ -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<String>;
|
||||
|
||||
/**
|
||||
* Offset the character's position by this amount when playing this animation.
|
||||
* @default [0, 0]
|
||||
*/
|
||||
@:default([0, 0])
|
||||
@:optional
|
||||
var offsets:Null<Array<Float>>;
|
||||
|
||||
/**
|
||||
* Whether the animation should loop when it finishes.
|
||||
* @default false
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var looped:Null<Bool>;
|
||||
|
||||
/**
|
||||
* Whether the animation's sprites should be flipped horizontally.
|
||||
* @default false
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var flipX:Null<Bool>;
|
||||
|
||||
/**
|
||||
* Whether the animation's sprites should be flipped vertically.
|
||||
* @default false
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var flipY:Null<Bool>;
|
||||
|
||||
/**
|
||||
* The frame rate of the animation.
|
||||
* @default 24
|
||||
*/
|
||||
@:default(24)
|
||||
@:optional
|
||||
var frameRate:Null<Int>;
|
||||
|
||||
/**
|
||||
|
@ -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<Array<Int>>;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ package funkin.play;
|
|||
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.sound.FlxSound;
|
||||
import flixel.system.FlxSound;
|
||||
import funkin.ui.story.StoryMenuState;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
|
|
|
@ -5,6 +5,7 @@ import flixel.sound.FlxSound;
|
|||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.FlxObject;
|
||||
import funkin.ui.story.StoryMenuState;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.FlxState;
|
||||
import flixel.FlxSubState;
|
||||
|
|
|
@ -37,11 +37,14 @@ class Song implements IPlayStateScriptedClass
|
|||
*/
|
||||
public var validScore:Bool = true;
|
||||
|
||||
var difficultyIds:Array<String>;
|
||||
|
||||
public function new(id:String)
|
||||
{
|
||||
this.songId = id;
|
||||
|
||||
variations = [];
|
||||
difficultyIds = [];
|
||||
difficulties = new Map<String, SongDifficulty>();
|
||||
|
||||
_metadata = SongDataParser.parseSongMetadata(songId);
|
||||
|
@ -72,6 +75,8 @@ class Song implements IPlayStateScriptedClass
|
|||
// but all the difficulties in the metadata must be in the chart file.
|
||||
for (diffId in metadata.playData.difficulties)
|
||||
{
|
||||
difficultyIds.push(diffId);
|
||||
|
||||
var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation);
|
||||
|
||||
variations.push(metadata.variation);
|
||||
|
@ -148,6 +153,16 @@ class Song implements IPlayStateScriptedClass
|
|||
return difficulties.get(diffId);
|
||||
}
|
||||
|
||||
public function listDifficulties():Array<String>
|
||||
{
|
||||
return difficultyIds;
|
||||
}
|
||||
|
||||
public function hasDifficulty(diffId:String):Bool
|
||||
{
|
||||
return difficulties.exists(diffId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge the cached chart data for each difficulty of this song.
|
||||
*/
|
||||
|
@ -290,7 +305,8 @@ class SongDifficulty
|
|||
|
||||
public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -320,28 +336,30 @@ class SongDifficulty
|
|||
return [];
|
||||
}
|
||||
|
||||
var suffix:String = variation != null ? '-$variation' : '';
|
||||
|
||||
// Automatically resolve voices by removing suffixes.
|
||||
// For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`.
|
||||
|
||||
var playerId:String = id;
|
||||
var voicePlayer:String = Paths.voices(this.song.songId, '-$id');
|
||||
var voicePlayer:String = Paths.voices(this.song.songId, '-$id$suffix');
|
||||
while (voicePlayer != null && !Assets.exists(voicePlayer))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
// For example, bf-car becomes bf.
|
||||
playerId = playerId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
voicePlayer = playerId == '' ? null : Paths.voices(this.song.songId, '-${playerId}');
|
||||
voicePlayer = playerId == '' ? null : Paths.voices(this.song.songId, '-${playerId}$suffix');
|
||||
}
|
||||
|
||||
var opponentId:String = playableCharData.opponent;
|
||||
var voiceOpponent:String = Paths.voices(this.song.songId, '-${opponentId}');
|
||||
var voiceOpponent:String = Paths.voices(this.song.songId, '-${opponentId}$suffix');
|
||||
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
opponentId = opponentId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.songId, '-${opponentId}');
|
||||
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.songId, '-${opponentId}$suffix');
|
||||
}
|
||||
|
||||
var result:Array<String> = [];
|
||||
|
@ -350,7 +368,7 @@ class SongDifficulty
|
|||
if (voicePlayer == null && voiceOpponent == null)
|
||||
{
|
||||
// Try to use `Voices.ogg` if no other voices are found.
|
||||
if (Assets.exists(Paths.voices(this.song.songId, ''))) result.push(Paths.voices(this.song.songId, ''));
|
||||
if (Assets.exists(Paths.voices(this.song.songId, ''))) result.push(Paths.voices(this.song.songId, '$suffix'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
177
source/funkin/ui/story/Level.hx
Normal file
177
source/funkin/ui/story/Level.hx
Normal file
|
@ -0,0 +1,177 @@
|
|||
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;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
{
|
||||
var songList:Array<String> = getSongs() ?? [];
|
||||
var songNameList:Array<String> = songList.map(function(songId) {
|
||||
return funkin.play.song.SongData.SongDataParser.fetchSong(songId) ?.getDifficulty(difficulty) ?.songName ?? 'Unknown';
|
||||
});
|
||||
return songNameList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
if (firstSong != null)
|
||||
{
|
||||
for (difficulty in firstSong.listDifficulties())
|
||||
{
|
||||
difficulties.push(difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to only include difficulties that are present in all songs
|
||||
for (songIndex in 1...songList.length)
|
||||
{
|
||||
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))
|
||||
{
|
||||
difficulties.remove(difficulty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (difficulties.length == 0) difficulties = ['normal'];
|
||||
|
||||
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;
|
||||
props.push(propSprite);
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
public function destroy():Void {}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Level($id)';
|
||||
}
|
||||
|
||||
public function _fetchData(id:String):Null<LevelData>
|
||||
{
|
||||
return LevelRegistry.instance.parseEntryData(id);
|
||||
}
|
||||
}
|
63
source/funkin/ui/story/LevelProp.hx
Normal file
63
source/funkin/ui/story/LevelProp.hx
Normal file
|
@ -0,0 +1,63 @@
|
|||
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)
|
||||
{
|
||||
super(danceEvery);
|
||||
}
|
||||
|
||||
public function playConfirm():Void
|
||||
{
|
||||
playAnimation('confirm', true, true);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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];
|
||||
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]);
|
||||
}
|
||||
|
||||
prop.dance();
|
||||
prop.animation.paused = true;
|
||||
|
||||
return prop;
|
||||
}
|
||||
}
|
90
source/funkin/ui/story/LevelTitle.hx
Normal file
90
source/funkin/ui/story/LevelTitle.hx
Normal file
|
@ -0,0 +1,90 @@
|
|||
package funkin.ui.story;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.CoolUtil;
|
||||
|
||||
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;
|
||||
|
||||
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??
|
||||
// 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, 0.17);
|
||||
|
||||
if (isFlashing) flashingInt += 1;
|
||||
if (flashingInt % fakeFramerate >= Math.floor(fakeFramerate / 2)) title.color = 0xFF33ffff;
|
||||
else
|
||||
title.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 {}
|
549
source/funkin/ui/story/StoryMenuState.hx
Normal file
549
source/funkin/ui/story/StoryMenuState.hx
Normal file
|
@ -0,0 +1,549 @@
|
|||
package funkin.ui.story;
|
||||
|
||||
import openfl.utils.Assets;
|
||||
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");
|
||||
static final BACKGROUND_HEIGHT:Int = 400;
|
||||
|
||||
var currentDifficultyId:String = 'normal';
|
||||
|
||||
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 selectedLevel:Bool = false;
|
||||
|
||||
var displayingModdedLevels: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 titles of the levels 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;
|
||||
|
||||
var difficultySprites:Map<String, FlxSprite>;
|
||||
|
||||
var stickerSubState:StickerSubState;
|
||||
|
||||
public function new(?stickers:StickerSubState = null)
|
||||
{
|
||||
super();
|
||||
|
||||
if (stickers != null)
|
||||
{
|
||||
stickerSubState = stickers;
|
||||
}
|
||||
}
|
||||
|
||||
override function create():Void
|
||||
{
|
||||
super.create();
|
||||
|
||||
difficultySprites = new Map<String, FlxSprite>();
|
||||
|
||||
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;
|
||||
|
||||
updateData();
|
||||
|
||||
// Explicitly define the background color.
|
||||
this.bgColor = FlxColor.BLACK;
|
||||
|
||||
levelTitles = new FlxTypedGroup<LevelTitle>();
|
||||
add(levelTitles);
|
||||
|
||||
updateBackground();
|
||||
|
||||
levelProps = new FlxTypedGroup<LevelProp>();
|
||||
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, levelBackground.x + levelBackground.height + 100, 0, "Tracks", 32);
|
||||
tracklistText.setFormat("VCR OSD Mono", 32);
|
||||
tracklistText.alignment = CENTER;
|
||||
tracklistText.color = 0xFFe55777;
|
||||
add(tracklistText);
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
add(difficultySprite);
|
||||
|
||||
updateText();
|
||||
changeDifficulty();
|
||||
changeLevel();
|
||||
refresh();
|
||||
|
||||
#if discord_rpc
|
||||
// Updating Discord Rich Presence
|
||||
DiscordClient.changePresence("In the Menus", null);
|
||||
#end
|
||||
}
|
||||
|
||||
function updateData():Void
|
||||
{
|
||||
currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId);
|
||||
isLevelUnlocked = currentLevel == null ? false : currentLevel.isUnlocked();
|
||||
}
|
||||
|
||||
function buildDifficultySprite():Void
|
||||
{
|
||||
remove(difficultySprite);
|
||||
difficultySprite = difficultySprites.get(currentDifficultyId);
|
||||
if (difficultySprite == null)
|
||||
{
|
||||
difficultySprite = new FlxSprite(leftDifficultyArrow.x + leftDifficultyArrow.width + 10, leftDifficultyArrow.y);
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
function buildLevelTitles():Void
|
||||
{
|
||||
levelTitles.clear();
|
||||
|
||||
var levelIds:Array<String> = displayingModdedLevels ? LevelRegistry.instance.listModdedLevelIds() : LevelRegistry.instance.listBaseGameLevelIds();
|
||||
if (levelIds.length == 0) levelIds = ['tutorial']; // Make sure there's at least one level to display.
|
||||
|
||||
for (levelIndex in 0...levelIds.length)
|
||||
{
|
||||
var levelId:String = levelIds[levelIndex];
|
||||
var level:Level = LevelRegistry.instance.fetchEntry(levelId);
|
||||
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)
|
||||
{
|
||||
Conductor.update();
|
||||
|
||||
highScoreLerp = Std.int(CoolUtil.coolLerp(highScoreLerp, highScore, 0.5));
|
||||
|
||||
scoreText.text = 'LEVEL SCORE: ${Math.round(highScoreLerp)}';
|
||||
|
||||
levelTitleText.text = currentLevel.getTitle();
|
||||
levelTitleText.x = FlxG.width - (levelTitleText.width + 10); // Right align.
|
||||
|
||||
handleKeyPresses();
|
||||
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
||||
function handleKeyPresses():Void
|
||||
{
|
||||
if (!exitingMenu)
|
||||
{
|
||||
if (!selectedLevel)
|
||||
{
|
||||
if (controls.UI_UP_P)
|
||||
{
|
||||
changeLevel(-1);
|
||||
changeDifficulty(0);
|
||||
}
|
||||
|
||||
if (controls.UI_DOWN_P)
|
||||
{
|
||||
changeLevel(1);
|
||||
changeDifficulty(0);
|
||||
}
|
||||
|
||||
if (controls.UI_RIGHT)
|
||||
{
|
||||
rightDifficultyArrow.animation.play('press');
|
||||
}
|
||||
else
|
||||
{
|
||||
rightDifficultyArrow.animation.play('idle');
|
||||
}
|
||||
|
||||
if (controls.UI_LEFT)
|
||||
{
|
||||
leftDifficultyArrow.animation.play('press');
|
||||
}
|
||||
else
|
||||
{
|
||||
leftDifficultyArrow.animation.play('idle');
|
||||
}
|
||||
|
||||
if (controls.UI_RIGHT_P)
|
||||
{
|
||||
changeDifficulty(1);
|
||||
}
|
||||
|
||||
if (controls.UI_LEFT_P)
|
||||
{
|
||||
changeDifficulty(-1);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.TAB)
|
||||
{
|
||||
switchMode(!displayingModdedLevels);
|
||||
}
|
||||
}
|
||||
|
||||
if (controls.ACCEPT)
|
||||
{
|
||||
selectLevel();
|
||||
}
|
||||
}
|
||||
|
||||
if (controls.BACK && !exitingMenu && !selectedLevel)
|
||||
{
|
||||
FlxG.sound.play(Paths.sound('cancelMenu'));
|
||||
exitingMenu = true;
|
||||
FlxG.switchState(new MainMenuState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the selected level.
|
||||
* @param change +1 (down), -1 (up)
|
||||
*/
|
||||
function changeLevel(change:Int = 0):Void
|
||||
{
|
||||
var levelList:Array<String> = displayingModdedLevels ? LevelRegistry.instance.listModdedLevelIds() : LevelRegistry.instance.listBaseGameLevelIds();
|
||||
if (levelList.length == 0) levelList = ['tutorial'];
|
||||
|
||||
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<String> = 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;
|
||||
|
||||
var hasChanged:Bool = currentDifficultyId != difficultyList[currentIndex];
|
||||
currentDifficultyId = difficultyList[currentIndex];
|
||||
|
||||
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
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -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