diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml
index bf81e0d6d..6b565bfa2 100644
--- a/.github/actions/setup-haxeshit/action.yml
+++ b/.github/actions/setup-haxeshit/action.yml
@@ -24,5 +24,5 @@ runs:
haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git
haxelib version
haxelib --global install hmm
- haxelib --global run hmm install
+ haxelib --global run hmm install --quiet
shell: bash
diff --git a/Project.xml b/Project.xml
index 4ffb0355c..f34c9bc06 100644
--- a/Project.xml
+++ b/Project.xml
@@ -181,6 +181,14 @@
+
diff --git a/hmm.json b/hmm.json
index 348e261df..74a1f57a1 100644
--- a/hmm.json
+++ b/hmm.json
@@ -4,21 +4,21 @@
"name": "discord_rpc",
"type": "git",
"dir": null,
- "ref": "2d83fa8",
+ "ref": "2d83fa863ef0c1eace5f1cf67c3ac315d1a3a8a5",
"url": "https://github.com/Aidan63/linc_discord-rpc"
},
{
"name": "flixel",
"type": "git",
"dir": null,
- "ref": "32cee07",
+ "ref": "32cee07a0e5f21e590a4b21234603b2cd5898b10",
"url": "https://github.com/EliteMasterEric/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
- "ref": "f107166",
+ "ref": "f107166de3e830648e8fbf3da5526d4b94aa7dfc",
"url": "https://github.com/EliteMasterEric/flixel-addons"
},
{
@@ -30,7 +30,7 @@
"name": "flxanimate",
"type": "git",
"dir": null,
- "ref": "a913635",
+ "ref": "a9136359271cae6ea3016b7fd9023c5c42562933",
"url": "https://github.com/ninjamuffin99/flxanimate"
},
{
@@ -42,23 +42,16 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
- "ref": "4b927f5",
+ "ref": "3590c94858fc6dbcf9b4d522cd644ad571269677",
"url": "https://github.com/haxeui/haxeui-core/"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
- "ref": "999fadd",
+ "ref": "999faddf862d8a1584ae3794d932c55e94fc65cc",
"url": "https://github.com/haxeui/haxeui-flixel"
},
- {
- "name": "hmm",
- "type": "git",
- "dir": null,
- "ref": "3ef9522",
- "url": "https://github.com/steviegt6/hmm"
- },
{
"name": "hscript",
"type": "haxelib",
@@ -66,8 +59,10 @@
},
{
"name": "hxCodec",
- "type": "haxelib",
- "version": "3.0.1"
+ "type": "git",
+ "dir": null,
+ "ref": "c8c47e706ad82a423783006ed901b6d93c89a421",
+ "url": "https://github.com/polybiusproxy/hxCodec"
},
{
"name": "hxcpp",
@@ -93,21 +88,21 @@
"name": "lime",
"type": "git",
"dir": null,
- "ref": "2447ae6",
- "url": "https://github.com/elitemastereric/lime"
+ "ref": "acb0334c59bd4618f3c0277584d524ed0b288b5f",
+ "url": "https://github.com/EliteMasterEric/lime"
},
{
"name": "openfl",
"type": "git",
"dir": null,
- "ref": "d33d489",
+ "ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08",
"url": "https://github.com/EliteMasterEric/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
- "ref": "6594dd8",
+ "ref": "631a3637f30997e47cd37bbab3cb6a75636a4b2a",
"url": "https://github.com/larsiusprime/polymod"
},
{
@@ -118,7 +113,7 @@
{
"name": "tink_json",
"type": "haxelib",
- "version": null
+ "version": "0.11.0"
}
]
}
diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx
index 608898a5f..9cc68c462 100644
--- a/source/funkin/FreeplayState.hx
+++ b/source/funkin/FreeplayState.hx
@@ -901,6 +901,7 @@ class FreeplayState extends MusicBeatSubState
}
}
+ @:haxe.warning("-WDeprecated")
override function switchTo(nextState:FlxState):Bool
{
clearDaCache(songs[curSelected].songName);
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index bed63d1d8..1ff2c0caa 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -20,7 +20,7 @@ class PolymodHandler
{
/**
* The API version that mods should comply with.
- * Format this with Semantic Versioning; ...
+ * Format this with Semantic Versioning; ...
* Bug fixes increment the patch version, new features increment the minor version.
* Changes that break old mods increment the major version.
*/
@@ -29,7 +29,9 @@ class PolymodHandler
/**
* Where relative to the executable that mods are located.
*/
- static final MOD_FOLDER = "mods";
+ static final MOD_FOLDER:String = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../example_mods" #elseif REDIRECT_ASSETS_FOLDER "../../../../example_mods" #else "mods" #end;
+
+ static final CORE_FOLDER:Null = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../assets" #elseif REDIRECT_ASSETS_FOLDER "../../../../assets" #else null #end;
public static function createModRoot()
{
@@ -202,9 +204,10 @@ class PolymodHandler
{
return {
assetLibraryPaths: [
- "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
- "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
- ]
+ "default" => "preload", "shared" => "", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2", "week3" => "week3",
+ "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
+ ],
+ coreAssetRedirect: CORE_FOLDER,
}
}
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index c0705bd96..3e7325ae4 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -639,6 +639,12 @@ class PlayState extends MusicBeatState
currentStage.resetStage();
+ playerStrumline.vwooshNotes();
+ opponentStrumline.vwooshNotes();
+
+ playerStrumline.clean();
+ opponentStrumline.clean();
+
// Delete all notes and reset the arrays.
regenNoteData();
@@ -966,6 +972,7 @@ class PlayState extends MusicBeatState
* This function is called whenever Flixel switches switching to a new FlxState.
* @return Whether to actually switch to the new state.
*/
+ @:haxe.warning("-WDeprecated")
override function switchTo(nextState:FlxState):Bool
{
var result:Bool = super.switchTo(nextState);
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index 6d67cfbbd..bcb73d543 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -58,6 +58,7 @@ class BaseCharacter extends Bopper
*/
public var dropNoteCounts(default, null):Array;
+ @:allow(funkin.ui.animDebugShit.DebugBoundingState)
final _data:CharacterData;
final singTimeSec:Float;
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 454ec13e1..ab9cfdec5 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -53,6 +53,9 @@ class Strumline extends FlxSpriteGroup
var noteSplashes:FlxTypedSpriteGroup;
var noteHoldCovers:FlxTypedSpriteGroup;
+ var notesVwoosh:FlxTypedSpriteGroup;
+ var holdNotesVwoosh:FlxTypedSpriteGroup;
+
final noteStyle:NoteStyle;
var noteData:Array = [];
@@ -76,10 +79,18 @@ class Strumline extends FlxSpriteGroup
this.holdNotes.zIndex = 20;
this.add(this.holdNotes);
+ this.holdNotesVwoosh = new FlxTypedSpriteGroup();
+ this.holdNotesVwoosh.zIndex = 21;
+ this.add(this.holdNotesVwoosh);
+
this.notes = new FlxTypedSpriteGroup();
this.notes.zIndex = 30;
this.add(this.notes);
+ this.notesVwoosh = new FlxTypedSpriteGroup();
+ this.notesVwoosh.zIndex = 31;
+ this.add(this.notesVwoosh);
+
this.noteHoldCovers = new FlxTypedSpriteGroup(0, 0, 4);
this.noteHoldCovers.zIndex = 40;
this.add(this.noteHoldCovers);
@@ -201,6 +212,54 @@ class Strumline extends FlxSpriteGroup
return null;
}
+ /**
+ * Call this when resetting the playstate.
+ */
+ public function vwooshNotes():Void
+ {
+ for (note in notes.members)
+ {
+ if (note == null) continue;
+ if (!note.alive) continue;
+
+ notes.remove(note);
+ notesVwoosh.add(note);
+
+ var targetY:Float = FlxG.height + note.y;
+ if (PreferencesMenu.getPref('downscroll')) targetY = 0 - note.height;
+ FlxTween.tween(note, {y: targetY}, 0.5,
+ {
+ ease: FlxEase.expoIn,
+ onComplete: function(twn) {
+ note.kill();
+ notesVwoosh.remove(note, true);
+ note.destroy();
+ }
+ });
+ }
+
+ for (holdNote in holdNotes.members)
+ {
+ if (holdNote == null) continue;
+ if (!holdNote.alive) continue;
+
+ holdNotes.remove(holdNote);
+ holdNotesVwoosh.add(holdNote);
+
+ var targetY:Float = FlxG.height + holdNote.y;
+ if (PreferencesMenu.getPref('downscroll')) targetY = 0 - holdNote.height;
+ FlxTween.tween(holdNote, {y: targetY}, 0.5,
+ {
+ ease: FlxEase.expoIn,
+ onComplete: function(twn) {
+ holdNote.kill();
+ holdNotesVwoosh.remove(holdNote, true);
+ holdNote.destroy();
+ }
+ });
+ }
+ }
+
/**
* For a note's strumTime, calculate its Y position relative to the strumline.
* NOTE: Assumes Conductor and PlayState are both initialized.
@@ -396,6 +455,38 @@ class Strumline extends FlxSpriteGroup
return heldKeys[dir];
}
+ /**
+ * Called when the song is reset.
+ * Removes any special animations and the like.
+ * Doesn't reset the notes from the chart, that's handled by the PlayState.
+ */
+ public function clean():Void
+ {
+ for (note in notes.members)
+ {
+ if (note == null) continue;
+ killNote(note);
+ }
+
+ for (holdNote in holdNotes.members)
+ {
+ if (holdNote == null) continue;
+ holdNote.kill();
+ }
+
+ for (splash in noteSplashes)
+ {
+ if (splash == null) continue;
+ splash.kill();
+ }
+
+ for (cover in noteHoldCovers)
+ {
+ if (cover == null) continue;
+ cover.kill();
+ }
+ }
+
public function applyNoteData(data:Array):Void
{
this.notes.clear();
@@ -423,6 +514,7 @@ class Strumline extends FlxSpriteGroup
public function killNote(note:NoteSprite):Void
{
+ if (note == null) return;
note.visible = false;
notes.remove(note, false);
note.kill();
diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index fdd613667..72d22191b 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -289,6 +289,20 @@ class SustainTrail extends FlxSprite
missedNote = false;
}
+ public override function revive():Void
+ {
+ super.revive();
+
+ strumTime = 0;
+ noteDirection = 0;
+ sustainLength = 0;
+ fullSustainLength = 0;
+ noteData = null;
+
+ hitNote = false;
+ missedNote = false;
+ }
+
override public function destroy():Void
{
vertices = null;
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 4cbf1ade3..8f8e24a71 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -47,7 +47,7 @@ class Song implements IPlayStateScriptedClass
difficultyIds = [];
difficulties = new Map();
- _metadata = SongDataParser.parseSongMetadata(songId);
+ _metadata = SongDataParser.loadSongMetadata(songId);
if (_metadata == null || _metadata.length == 0)
{
throw 'Could not find song data for songId: $songId';
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
index c2a701ce9..c44180b20 100644
--- a/source/funkin/play/song/SongData.hx
+++ b/source/funkin/play/song/SongData.hx
@@ -1,7 +1,7 @@
package funkin.play.song;
-import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
+import funkin.modding.events.ScriptEvent;
import flixel.util.typeLimit.OneOfTwo;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
@@ -120,12 +120,21 @@ class SongDataParser
}
}
+ /**
+ * A list of all the song IDs available to the game.
+ * @return The list of song IDs.
+ */
public static function listSongIds():Array
{
return songCache.keys().array();
}
- public static function parseSongMetadata(songId:String):Array
+ /**
+ * Loads the song metadata for a particular song.
+ * @param songId The ID of the song to load.
+ * @return The song metadata for each variation, or an empty array if the song was not found.
+ */
+ public static function loadSongMetadata(songId:String):Array
{
var result:Array = [];
@@ -147,19 +156,13 @@ class SongDataParser
result.push(songMetadata);
- var variations = songMetadata.playData.songVariations;
+ var variations:Array = songMetadata.playData.songVariations;
for (variation in variations)
{
- 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}');
+ var variationRawJson:String = loadSongMetadataFile(songId, variation);
+ var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationRawJson, '${songId}_${variation}');
+ variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}_${variation}');
if (variationSongMetadata != null)
{
variationSongMetadata.variation = variation;
@@ -176,7 +179,7 @@ class SongDataParser
var rawJson:String = Assets.getText(songMetadataFilePath).trim();
- while (!rawJson.endsWith("}"))
+ while (!rawJson.endsWith('}') && rawJson.length > 0)
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
@@ -214,7 +217,7 @@ class SongDataParser
return rawJson;
}
- public static function parseSongChartData(songId:String, variation:String = ""):SongChartData
+ public static function parseSongChartData(songId:String, variation:String = ''):SongChartData
{
var rawJson:String = loadSongChartDataFile(songId, variation);
var jsonData:Dynamic = null;
@@ -222,7 +225,11 @@ class SongDataParser
{
jsonData = Json.parse(rawJson);
}
- catch (e) {}
+ catch (e)
+ {
+ trace('Failed to parse song chart data: ${songId} (${variation})');
+ trace(e);
+ }
var songChartData:SongChartData = SongMigrator.migrateSongChartData(jsonData, songId);
songChartData = SongValidator.validateSongChartData(songChartData, songId);
@@ -242,7 +249,7 @@ class SongDataParser
var rawJson:String = Assets.getText(songChartDataFilePath).trim();
- while (!rawJson.endsWith("}"))
+ while (!rawJson.endsWith('}') && rawJson.length > 0)
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
@@ -310,7 +317,7 @@ abstract SongMetadata(RawSongMetadata)
public function clone(?newVariation:String = null):SongMetadata
{
- var result = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
+ var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
result.version = this.version;
result.timeFormat = this.timeFormat;
result.divisions = this.divisions;
@@ -391,12 +398,12 @@ abstract SongNoteData(RawSongNoteData)
*/
public var time(get, set):Float;
- public function get_time():Float
+ function get_time():Float
{
return this.t;
}
- public function set_time(value:Float):Float
+ function set_time(value:Float):Float
{
return this.t = value;
}
@@ -406,7 +413,7 @@ abstract SongNoteData(RawSongNoteData)
*/
public var stepTime(get, never):Float;
- public function get_stepTime():Float
+ function get_stepTime():Float
{
return Conductor.getTimeInSteps(abstract.time);
}
@@ -416,12 +423,12 @@ abstract SongNoteData(RawSongNoteData)
*/
public var data(get, set):Int;
- public function get_data():Int
+ function get_data():Int
{
return this.d;
}
- public function set_data(value:Int):Int
+ function set_data(value:Int):Int
{
return this.d = value;
}
@@ -524,7 +531,7 @@ abstract SongNoteData(RawSongNoteData)
return this.k;
}
- public function set_kind(value:String):String
+ function set_kind(value:String):String
{
if (value == 'normal' || value == '') value = null;
return this.k = value;
@@ -628,55 +635,55 @@ abstract SongEventData(RawSongEventData)
public var time(get, set):Float;
- public function get_time():Float
+ function get_time():Float
{
return this.t;
}
- public function set_time(value:Float):Float
+ function set_time(value:Float):Float
{
return this.t = value;
}
public var stepTime(get, never):Float;
- public function get_stepTime():Float
+ function get_stepTime():Float
{
return Conductor.getTimeInSteps(abstract.time);
}
public var event(get, set):String;
- public function get_event():String
+ function get_event():String
{
return this.e;
}
- public function set_event(value:String):String
+ function set_event(value:String):String
{
return this.e = value;
}
public var value(get, set):Dynamic;
- public function get_value():Dynamic
+ function get_value():Dynamic
{
return this.v;
}
- public function set_value(value:Dynamic):Dynamic
+ function set_value(value:Dynamic):Dynamic
{
return this.v = value;
}
public var activated(get, set):Bool;
- public function get_activated():Bool
+ function get_activated():Bool
{
return this.a;
}
- public function set_activated(value:Bool):Bool
+ function set_activated(value:Bool):Bool
{
return this.a = value;
}
@@ -755,7 +762,7 @@ abstract SongEventData(RawSongEventData)
abstract SongPlayableChar(RawSongPlayableChar)
{
- public function new(girlfriend:String, opponent:String, inst:String = "")
+ public function new(girlfriend:String, opponent:String, inst:String = '')
{
this =
{
@@ -767,36 +774,36 @@ abstract SongPlayableChar(RawSongPlayableChar)
public var girlfriend(get, set):String;
- public function get_girlfriend():String
+ function get_girlfriend():String
{
return this.g;
}
- public function set_girlfriend(value:String):String
+ function set_girlfriend(value:String):String
{
return this.g = value;
}
public var opponent(get, set):String;
- public function get_opponent():String
+ function get_opponent():String
{
return this.o;
}
- public function set_opponent(value:String):String
+ function set_opponent(value:String):String
{
return this.o = value;
}
public var inst(get, set):String;
- public function get_inst():String
+ function get_inst():String
{
return this.i;
}
- public function set_inst(value:String):String
+ function set_inst(value:String):String
{
return this.i = value;
}
@@ -842,6 +849,35 @@ abstract SongChartData(RawSongChartData)
return (result == 0.0) ? 1.0 : result;
}
+
+ public function setScrollSpeed(value:Float, diff:String = 'default'):Float
+ {
+ return this.scrollSpeed.set(diff, value);
+ }
+
+ public function getNotes(diff:String):Array
+ {
+ var result:Array = this.notes.get(diff);
+
+ if (result == null && diff != 'normal') return getNotes('normal');
+
+ return (result == null) ? [] : result;
+ }
+
+ public function setNotes(value:Array, diff:String):Array
+ {
+ return this.notes.set(diff, value);
+ }
+
+ public function getEvents():Array
+ {
+ return this.events;
+ }
+
+ public function setEvents(value:Array):Array
+ {
+ return this.events = value;
+ }
}
typedef RawSongTimeChange =
@@ -902,12 +938,12 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange
public var timeStamp(get, set):Float;
- public function get_timeStamp():Float
+ function get_timeStamp():Float
{
return this.t;
}
- public function set_timeStamp(value:Float):Float
+ function set_timeStamp(value:Float):Float
{
return this.t = value;
}
@@ -926,43 +962,43 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange
public var bpm(get, set):Float;
- public function get_bpm():Float
+ function get_bpm():Float
{
return this.bpm;
}
- public function set_bpm(value:Float):Float
+ function set_bpm(value:Float):Float
{
return this.bpm = value;
}
public var timeSignatureNum(get, set):Int;
- public function get_timeSignatureNum():Int
+ function get_timeSignatureNum():Int
{
return this.n;
}
- public function set_timeSignatureNum(value:Int):Int
+ function set_timeSignatureNum(value:Int):Int
{
return this.n = value;
}
public var timeSignatureDen(get, set):Int;
- public function get_timeSignatureDen():Int
+ function get_timeSignatureDen():Int
{
return this.d;
}
- public function set_timeSignatureDen(value:Int):Int
+ function set_timeSignatureDen(value:Int):Int
{
return this.d = value;
}
public var beatTuplets(get, set):Array;
- public function get_beatTuplets():Array
+ function get_beatTuplets():Array
{
if (Std.isOfType(this.bt, Int))
{
@@ -974,7 +1010,7 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange
}
}
- public function set_beatTuplets(value:Array):Array
+ function set_beatTuplets(value:Array):Array
{
return this.bt = value;
}
@@ -982,7 +1018,7 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange
enum abstract SongTimeFormat(String) from String to String
{
- var TICKS = "ticks";
- var FLOAT = "float";
- var MILLISECONDS = "ms";
+ var TICKS = 'ticks';
+ var FLOAT = 'float';
+ var MILLISECONDS = 'ms';
}
diff --git a/source/funkin/play/song/SongDataUtils.hx b/source/funkin/play/song/SongDataUtils.hx
index f75972ee7..750d5f54b 100644
--- a/source/funkin/play/song/SongDataUtils.hx
+++ b/source/funkin/play/song/SongDataUtils.hx
@@ -101,9 +101,11 @@ class SongDataUtils
*
* Offset the provided array of notes such that the first note is at 0 milliseconds.
*/
- public static function buildNoteClipboard(notes:Array):Array
+ public static function buildNoteClipboard(notes:Array, ?timeOffset:Int = null):Array
{
- return offsetSongNoteData(sortNotes(notes), -Std.int(notes[0].time));
+ if (notes.length == 0) return notes;
+ if (timeOffset == null) timeOffset = -Std.int(notes[0].time);
+ return offsetSongNoteData(sortNotes(notes), timeOffset);
}
/**
@@ -111,9 +113,11 @@ class SongDataUtils
*
* Offset the provided array of events such that the first event is at 0 milliseconds.
*/
- public static function buildEventClipboard(events:Array):Array
+ public static function buildEventClipboard(events:Array, ?timeOffset:Int = null):Array
{
- return offsetSongEventData(sortEvents(events), -Std.int(events[0].time));
+ if (events.length == 0) return events;
+ if (timeOffset == null) timeOffset = -Std.int(events[0].time);
+ return offsetSongEventData(sortEvents(events), timeOffset);
}
/**
diff --git a/source/funkin/play/song/SongMigrator.hx b/source/funkin/play/song/SongMigrator.hx
index 1872585d0..f561c4d3e 100644
--- a/source/funkin/play/song/SongMigrator.hx
+++ b/source/funkin/play/song/SongMigrator.hx
@@ -1,7 +1,11 @@
package funkin.play.song;
+import funkin.play.song.formats.FNFLegacy;
import funkin.play.song.SongData.SongChartData;
+import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata;
+import funkin.play.song.SongData.SongNoteData;
+import funkin.play.song.SongData.SongPlayableChar;
import funkin.util.VersionUtil;
class SongMigrator
@@ -11,13 +15,22 @@ class SongMigrator
* Handle breaking changes by incrementing this value
* and adding migration to the SongMigrator class.
*/
- public static final CHART_VERSION:String = "2.0.0";
+ public static final CHART_VERSION:String = '2.0.0';
- public static final CHART_VERSION_RULE:String = "2.0.x";
+ /**
+ * Version rule for which chart versions are compatible with the current version.
+ */
+ public static final CHART_VERSION_RULE:String = '2.0.x';
+ /**
+ * Migrate song data from an older chart version to the current version.
+ * @param jsonData The song metadata to migrate.
+ * @param songId The ID of the song (only used for error reporting).
+ * @return The migrated song metadata, or null if the migration failed.
+ */
public static function migrateSongMetadata(jsonData:Dynamic, songId:String):SongMetadata
{
- if (jsonData.version)
+ if (jsonData.version != null)
{
if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE))
{
@@ -32,10 +45,11 @@ class SongMigrator
trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.');
switch (jsonData.version)
{
- // TODO: Add migration functions as cases here.
+ case '1.0.0':
+ return migrateSongMetadataFromLegacy(jsonData);
default:
- // Unknown version.
- trace('Song (${songId}) unknown metadata version: ${jsonData.version}');
+ trace('Song (${songId}) has unknown metadata version (${jsonData.version}), assuming FNF Legacy.');
+ return migrateSongMetadataFromLegacy(jsonData);
}
}
}
@@ -46,6 +60,12 @@ class SongMigrator
return null;
}
+ /**
+ * Migrate song chart data from an older chart version to the current version.
+ * @param jsonData The song chart data to migrate.
+ * @param songId The ID of the song (only used for error reporting).
+ * @return The migrated song chart data, or null if the migration failed.
+ */
public static function migrateSongChartData(jsonData:Dynamic, songId:String):SongChartData
{
if (jsonData.version)
@@ -76,4 +96,161 @@ class SongMigrator
}
return null;
}
+
+ /**
+ * Migrate song metadata from FNF Legacy chart version to the current version.
+ * @param jsonData The song metadata to migrate.
+ * @param songId The ID of the song (only used for error reporting).
+ * @return The migrated song metadata, or null if the migration failed.
+ */
+ public static function migrateSongMetadataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongMetadata
+ {
+ trace('Migrating song metadata from FNF Legacy.');
+
+ var songData:FNFLegacy = cast jsonData;
+
+ var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default');
+
+ var hadError:Bool = false;
+
+ // Set generatedBy string for debugging.
+ songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)';
+
+ try
+ {
+ // Set the song's BPM.
+ songMetadata.timeChanges[0].bpm = songData.song.bpm;
+ }
+ catch (e)
+ {
+ trace("Couldn't parse BPM!");
+ hadError = true;
+ }
+
+ try
+ {
+ // Set the song's stage.
+ songMetadata.playData.stage = songData.song.stageDefault;
+ }
+ catch (e)
+ {
+ trace("Couldn't parse stage!");
+ hadError = true;
+ }
+
+ try
+ {
+ // Set's the song's name.
+ songMetadata.songName = songData.song.song;
+ }
+ catch (e)
+ {
+ trace("Couldn't parse song name!");
+ hadError = true;
+ }
+
+ songMetadata.playData.difficulties = [];
+ if (songData.song != null && songData.song.notes != null)
+ {
+ if (Std.isOfType(songData.song.notes, Array))
+ {
+ // One difficulty of notes.
+ songMetadata.playData.difficulties.push(difficulty);
+ }
+ else
+ {
+ // Multiple difficulties of notes.
+ var songNoteDataDynamic:haxe.DynamicAccess = cast songData.song.notes;
+ for (difficultyKey in songNoteDataDynamic.keys())
+ {
+ songMetadata.playData.difficulties.push(difficultyKey);
+ }
+ }
+ }
+ else
+ {
+ trace("Couldn't parse difficulties!");
+ hadError = true;
+ }
+
+ songMetadata.playData.songVariations = [];
+
+ // Set the song's song variations.
+ songMetadata.playData.playableChars = {};
+ try
+ {
+ Reflect.setField(songMetadata.playData.playableChars, songData.song.player1, new SongPlayableChar('', songData.song.player2));
+ }
+ catch (e)
+ {
+ trace("Couldn't parse characters!");
+ hadError = true;
+ }
+
+ return songMetadata;
+ }
+
+ /**
+ * Migrate song chart data from FNF Legacy chart version to the current version.
+ * @param jsonData The song data to migrate.
+ * @param songId The ID of the song (only used for error reporting).
+ * @param difficulty The difficulty to migrate.
+ * @return The migrated song chart data, or null if the migration failed.
+ */
+ public static function migrateSongChartDataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongChartData
+ {
+ trace('Migrating song chart data from FNF Legacy.');
+
+ var songData:FNFLegacy = cast jsonData;
+
+ var songChartData:SongChartData = new SongChartData(1.0, [], []);
+
+ var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0;
+ if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes));
+ songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes), difficulty);
+ songChartData.setScrollSpeed(songData.song.speed, difficulty);
+
+ return songChartData;
+ }
+
+ static function migrateSongNoteDataFromLegacy(sections:Array):Array
+ {
+ var songNotes:Array = [];
+
+ for (section in sections)
+ {
+ // Skip empty sections.
+ if (section.sectionNotes.length == 0) continue;
+
+ for (note in section.sectionNotes)
+ {
+ songNotes.push(new SongNoteData(note.time, note.getData(section.mustHitSection), note.length, note.kind));
+ }
+ }
+
+ return songNotes;
+ }
+
+ static function migrateSongEventDataFromLegacy(sections:Array):Array
+ {
+ var songEvents:Array = [];
+
+ var lastSectionWasMustHit:Null = null;
+ for (section in sections)
+ {
+ // Skip empty sections.
+ if (section.sectionNotes.length == 0) continue;
+
+ if (section.mustHitSection != lastSectionWasMustHit)
+ {
+ lastSectionWasMustHit = section.mustHitSection;
+
+ var firstNote:LegacyNote = section.sectionNotes[0];
+
+ songEvents.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1}));
+ }
+ }
+
+ return songEvents;
+ }
}
diff --git a/source/funkin/play/song/SongSerializer.hx b/source/funkin/play/song/SongSerializer.hx
index 968a7a1f5..a08b722da 100644
--- a/source/funkin/play/song/SongSerializer.hx
+++ b/source/funkin/play/song/SongSerializer.hx
@@ -82,9 +82,9 @@ class SongSerializer
* Save a SongChartData object as a JSON file to an automatically generated path.
* Works great on HTML5 and desktop.
*/
- public static function exportSongChartData(data:SongChartData)
+ public static function exportSongChartData(data:SongChartData, songId:String)
{
- var path = 'chart.json';
+ var path = '${songId}-chart.json';
exportSongChartDataAs(path, data);
}
@@ -92,9 +92,9 @@ class SongSerializer
* Save a SongMetadata object as a JSON file to an automatically generated path.
* Works great on HTML5 and desktop.
*/
- public static function exportSongMetadata(data:SongMetadata)
+ public static function exportSongMetadata(data:SongMetadata, songId:String)
{
- var path = 'metadata.json';
+ var path = '${songId}-metadata.json';
exportSongMetadataAs(path, data);
}
diff --git a/source/funkin/play/song/formats/FNFLegacy.hx b/source/funkin/play/song/formats/FNFLegacy.hx
new file mode 100644
index 000000000..a64e461bd
--- /dev/null
+++ b/source/funkin/play/song/formats/FNFLegacy.hx
@@ -0,0 +1,131 @@
+package funkin.play.song.formats;
+
+typedef FNFLegacy =
+{
+ var song:LegacySongData;
+}
+
+typedef LegacySongData =
+{
+ var player1:String; // Boyfriend
+ var player2:String; // Opponent
+
+ var speed:Float;
+ var stageDefault:String;
+ var bpm:Float;
+ var notes:Array;
+ var song:String; // Song name
+};
+
+typedef LegacyScrollSpeeds =
+{
+ var easy:Float;
+ var normal:Float;
+ var hard:Float;
+};
+
+typedef LegacyNoteData =
+{
+ /**
+ * The easy difficulty.
+ */
+ var ?easy:Array;
+
+ /**
+ * The normal difficulty.
+ */
+ var ?normal:Array;
+
+ /**
+ * The hard difficulty.
+ */
+ var ?hard:Array;
+};
+
+typedef LegacyNoteSection =
+{
+ /**
+ * Whether the section is a must-hit section.
+ * If true, 0-3 are boyfriends notes, 4-7 are opponents notes.
+ * If false, 0-3 are opponents notes, 4-7 are boyfriends notes.
+ */
+ var mustHitSection:Bool;
+
+ /**
+ * Array of note data:
+ * - Direction
+ * - Time (ms)
+ * - Sustain Duration (ms)
+ * - Note kind (true = "alt", or string)
+ */
+ var sectionNotes:Array;
+
+ var typeOfSection:Int;
+ var lengthInSteps:Int;
+}
+
+/**
+ * Notes in the old format are stored as an Array
+ */
+abstract LegacyNote(Array)
+{
+ public var time(get, set):Float;
+
+ function get_time():Float
+ {
+ return this[0];
+ }
+
+ function set_time(value:Float):Float
+ {
+ return this[0] = value;
+ }
+
+ public var data(get, set):Int;
+
+ function get_data():Int
+ {
+ return this[1];
+ }
+
+ function set_data(value:Int):Int
+ {
+ return this[1] = value;
+ }
+
+ public function getData(mustHitSection:Bool):Int
+ {
+ if (mustHitSection) return this[1];
+
+ return (this[1] + 4) % 8;
+ }
+
+ public var length(get, set):Float;
+
+ function get_length():Float
+ {
+ if (this.length < 3) return 0.0;
+ return this[2];
+ }
+
+ function set_length(value:Float):Float
+ {
+ return this[2] = value;
+ }
+
+ public var kind(get, set):String;
+
+ function get_kind():String
+ {
+ if (this.length < 4) return 'normal';
+
+ if (Std.isOfType(this[3], Bool)) return this[3] ? 'alt' : 'normal';
+
+ return this[3];
+ }
+
+ function set_kind(value:String):String
+ {
+ return this[3] = value;
+ }
+}
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index 26c3a0ff2..5fb1022fe 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -85,6 +85,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
return globalOffsets = value;
}
+ @:allow(funkin.ui.animDebugShit.DebugBoundingState)
var animOffsets(default, set):Array = [0, 0];
public var originalPosition:FlxPoint = new FlxPoint(0, 0);
diff --git a/source/funkin/ui/animDebugShit/DebugBoundingState.hx b/source/funkin/ui/animDebugShit/DebugBoundingState.hx
index 5a7e555de..4e3d1dbf4 100644
--- a/source/funkin/ui/animDebugShit/DebugBoundingState.hx
+++ b/source/funkin/ui/animDebugShit/DebugBoundingState.hx
@@ -1,5 +1,7 @@
package funkin.ui.animDebugShit;
+import funkin.util.SerializerUtil;
+import funkin.play.character.CharacterData;
import flixel.FlxCamera;
import flixel.FlxSprite;
import flixel.FlxState;
@@ -32,6 +34,9 @@ import openfl.net.FileReference;
import openfl.net.URLLoader;
import openfl.net.URLRequest;
import openfl.utils.ByteArray;
+import funkin.input.Cursor;
+import funkin.play.character.CharacterData.CharacterDataParser;
+import funkin.util.SortUtil;
using flixel.util.FlxSpriteUtil;
@@ -71,7 +76,7 @@ class DebugBoundingState extends FlxState
{
Paths.setCurrentLevel('week1');
- var str = Paths.xml('ui/offset-editor-view');
+ var str = Paths.xml('ui/animation-editor/offset-editor-view');
uiStuff = RuntimeComponentBuilder.fromAsset(str);
// uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET;
@@ -109,6 +114,8 @@ class DebugBoundingState extends FlxState
initSpritesheetView();
initOffsetView();
+ Cursor.show();
+
uiStuff.cameras = [hudCam];
add(uiStuff);
@@ -124,7 +131,7 @@ class DebugBoundingState extends FlxState
spriteSheetView = new FlxGroup();
add(spriteSheetView);
- var tex = Paths.getSparrowAtlas('characters/temp');
+ var tex = Paths.getSparrowAtlas('characters/BOYFRIEND');
// tex.frames[0].uv
bf = new FlxSprite();
@@ -238,11 +245,15 @@ class DebugBoundingState extends FlxState
txtOffsetShit.cameras = [hudCam];
offsetView.add(txtOffsetShit);
- animDropDownMenu = new FlxUIDropDownMenu(630, 20, FlxUIDropDownMenu.makeStrIdLabelArray(['weed'], true));
+ animDropDownMenu = new FlxUIDropDownMenu(0, 0, FlxUIDropDownMenu.makeStrIdLabelArray(['weed'], true));
animDropDownMenu.cameras = [hudCam];
+ // Move to bottom right corner
+ animDropDownMenu.x = FlxG.width - animDropDownMenu.width - 20;
+ animDropDownMenu.y = FlxG.height - animDropDownMenu.height - 20;
offsetView.add(animDropDownMenu);
- var characters:Array = CoolUtil.coolTextFile(Paths.txt('characterList'));
+ var characters:Array = CharacterDataParser.listCharacterIds();
+ characters.sort(SortUtil.alphabetically);
var charDropdown:DropDown = cast uiStuff.findComponent('characterDropdown');
for (char in characters)
@@ -264,19 +275,16 @@ class DebugBoundingState extends FlxState
{
if (FlxG.mouse.justPressed)
{
- mouseOffset.set(FlxG.mouse.x - -swagChar.offset.x, FlxG.mouse.y - -swagChar.offset.y);
- // oldPos.set(swagChar.offset.x, swagChar.offset.y);
- // oldPos.set(FlxG.mouse.x, FlxG.mouse.y);
+ mouseOffset.set(FlxG.mouse.x - -swagChar.animOffsets[0], FlxG.mouse.y - -swagChar.animOffsets[1]);
}
if (FlxG.mouse.pressed)
{
- swagChar.offset.x = (FlxG.mouse.x - mouseOffset.x) * -1;
- swagChar.offset.y = (FlxG.mouse.y - mouseOffset.y) * -1;
+ swagChar.animOffsets = [(FlxG.mouse.x - mouseOffset.x) * -1, (FlxG.mouse.y - mouseOffset.y) * -1];
- swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, [Std.int(swagChar.offset.x), Std.int(swagChar.offset.y)]);
+ swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, swagChar.animOffsets);
- txtOffsetShit.text = 'Offset: ' + swagChar.offset;
+ txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
}
}
}
@@ -291,6 +299,11 @@ class DebugBoundingState extends FlxState
swagText.text = str + ": " + Std.string(value);
}
+ function clearInfo()
+ {
+ txtGrp.clear();
+ }
+
function checkLibrary(library:String)
{
trace(Assets.hasLibrary(library));
@@ -320,7 +333,7 @@ class DebugBoundingState extends FlxState
{
var lv:DropDown = cast uiStuff.findComponent("swapper");
lv.selectedIndex = 1;
- curView = OFFSETSHIT;
+ curView = ANIMATIONS;
if (swagChar != null)
{
FlxG.camera.focusOn(swagChar.getMidpoint());
@@ -334,7 +347,7 @@ class DebugBoundingState extends FlxState
spriteSheetView.visible = true;
offsetView.visible = false;
offsetView.active = false;
- case OFFSETSHIT:
+ case ANIMATIONS:
spriteSheetView.visible = false;
offsetView.visible = true;
offsetView.active = true;
@@ -344,6 +357,8 @@ class DebugBoundingState extends FlxState
if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible;
+ if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState());
+
CoolUtil.mouseCamDrag();
CoolUtil.mouseWheelZoom();
@@ -364,14 +379,14 @@ class DebugBoundingState extends FlxState
+ 1);
else
animDropDownMenu.selectedId = Std.string(0);
- animDropDownMenu.callback(animDropDownMenu.selectedId);
+ playCharacterAnimation(animDropDownMenu.selectedId, true);
}
if (FlxG.keys.justPressed.LBRACKET || FlxG.keys.justPressed.Q)
{
if (Std.parseInt(animDropDownMenu.selectedId) - 1 >= 0) animDropDownMenu.selectedId = Std.string(Std.parseInt(animDropDownMenu.selectedId) - 1);
else
animDropDownMenu.selectedId = Std.string(animDropDownMenu.length - 1);
- animDropDownMenu.callback(animDropDownMenu.selectedId);
+ playCharacterAnimation(animDropDownMenu.selectedId, true);
}
// Keyboards controls for general WASD "movement"
@@ -379,16 +394,29 @@ class DebugBoundingState extends FlxState
// and then it's just played and updated from the animDropDownMenu callback, which is set in the loadAnimShit() function probabbly
if (FlxG.keys.justPressed.W || FlxG.keys.justPressed.S || FlxG.keys.justPressed.D || FlxG.keys.justPressed.A)
{
- var missShit:String = '';
+ var suffix:String = '';
+ var targetLabel:String = '';
- if (FlxG.keys.pressed.SHIFT) missShit = 'miss';
+ if (FlxG.keys.pressed.SHIFT) suffix = 'miss';
- if (FlxG.keys.justPressed.W) animDropDownMenu.selectedLabel = 'singUP' + missShit;
- if (FlxG.keys.justPressed.S) animDropDownMenu.selectedLabel = 'singDOWN' + missShit;
- if (FlxG.keys.justPressed.A) animDropDownMenu.selectedLabel = 'singLEFT' + missShit;
- if (FlxG.keys.justPressed.D) animDropDownMenu.selectedLabel = 'singRIGHT' + missShit;
+ if (FlxG.keys.justPressed.W) targetLabel = 'singUP$suffix';
+ if (FlxG.keys.justPressed.S) targetLabel = 'singDOWN$suffix';
+ if (FlxG.keys.justPressed.A) targetLabel = 'singLEFT$suffix';
+ if (FlxG.keys.justPressed.D) targetLabel = 'singRIGHT$suffix';
- animDropDownMenu.callback(animDropDownMenu.selectedId);
+ if (targetLabel != animDropDownMenu.selectedLabel)
+ {
+ // Play the new animation if the IDs are the different.
+ // Override the onion skin.
+ animDropDownMenu.selectedLabel = targetLabel;
+ playCharacterAnimation(animDropDownMenu.selectedId, true);
+ }
+ else
+ {
+ // Replay the current animation if the IDs are the same.
+ // Don't override the onion skin.
+ playCharacterAnimation(animDropDownMenu.selectedId, false);
+ }
}
if (FlxG.keys.justPressed.F)
@@ -400,16 +428,16 @@ class DebugBoundingState extends FlxState
if (FlxG.keys.justPressed.SPACE)
{
animDropDownMenu.selectedLabel = 'idle';
- animDropDownMenu.callback(animDropDownMenu.selectedId);
+ playCharacterAnimation(animDropDownMenu.selectedId, true);
}
// Playback the animation
- if (FlxG.keys.justPressed.ENTER) animDropDownMenu.callback(animDropDownMenu.selectedId);
+ if (FlxG.keys.justPressed.ENTER) playCharacterAnimation(animDropDownMenu.selectedId, false);
if (FlxG.keys.justPressed.RIGHT || FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.UP || FlxG.keys.justPressed.DOWN)
{
var animName = animDropDownMenu.selectedLabel;
- var coolValues:Array = swagChar.animationOffsets.get(animName);
+ var coolValues:Array = swagChar.animationOffsets.get(animName).copy();
var multiplier:Int = 5;
@@ -432,18 +460,38 @@ class DebugBoundingState extends FlxState
if (FlxG.keys.justPressed.ESCAPE)
{
- var outputString:String = "";
-
- for (i in swagChar.animationOffsets.keys())
- {
- outputString += i + " " + swagChar.animationOffsets.get(i)[0] + " " + swagChar.animationOffsets.get(i)[1] + "\n";
- }
-
- outputString.trim();
- saveOffsets(outputString);
+ var outputString = FlxG.keys.pressed.CONTROL ? buildOutputStringOld() : buildOutputStringNew();
+ saveOffsets(outputString, FlxG.keys.pressed.CONTROL ? swagChar.characterId + "Offsets.txt" : swagChar.characterId + ".json");
}
}
+ function buildOutputStringOld():String
+ {
+ var outputString:String = "";
+
+ for (i in swagChar.animationOffsets.keys())
+ {
+ outputString += i + " " + swagChar.animationOffsets.get(i)[0] + " " + swagChar.animationOffsets.get(i)[1] + "\n";
+ }
+
+ outputString.trim();
+
+ return outputString;
+ }
+
+ function buildOutputStringNew():String
+ {
+ var charData:CharacterData = Reflect.copy(swagChar._data);
+
+ for (charDataAnim in charData.animations)
+ {
+ var animName:String = charDataAnim.name;
+ charDataAnim.offsets = swagChar.animationOffsets.get(animName);
+ }
+
+ return SerializerUtil.toJSON(charData, true);
+ }
+
var swagChar:BaseCharacter;
/*
@@ -466,35 +514,51 @@ class DebugBoundingState extends FlxState
generateOutlines(swagChar.frames.frames);
bf.pixels = swagChar.pixels;
- var animThing:Array = [];
+ clearInfo();
+ addInfo(swagChar._data.assetPath, "");
+ addInfo('Width', bf.width);
+ addInfo('Height', bf.height);
+
+ characterAnimNames = [];
for (i in swagChar.animationOffsets.keys())
{
- animThing.push(i);
+ characterAnimNames.push(i);
trace(i);
trace(swagChar.animationOffsets[i]);
}
- animDropDownMenu.setData(FlxUIDropDownMenu.makeStrIdLabelArray(animThing, true));
+ animDropDownMenu.setData(FlxUIDropDownMenu.makeStrIdLabelArray(characterAnimNames, true));
animDropDownMenu.callback = function(str:String) {
+ playCharacterAnimation(str, true);
+ };
+ txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
+ dropDownSetup = true;
+ }
+
+ private var characterAnimNames:Array;
+
+ function playCharacterAnimation(str:String, setOnionSkin:Bool = true)
+ {
+ if (setOnionSkin)
+ {
// clears the canvas
onionSkinChar.pixels.fillRect(new Rectangle(0, 0, FlxG.width * 2, FlxG.height * 2), 0x00000000);
- onionSkinChar.stamp(swagChar, Std.int(swagChar.x - swagChar.offset.x), Std.int(swagChar.y - swagChar.offset.y));
+ onionSkinChar.stamp(swagChar, Std.int(swagChar.x), Std.int(swagChar.y));
onionSkinChar.alpha = 0.6;
+ }
- var animName = animThing[Std.parseInt(str)];
- swagChar.playAnimation(animName, true); // trace();
- trace(swagChar.animationOffsets.get(animName));
+ var animName = characterAnimNames[Std.parseInt(str)];
+ swagChar.playAnimation(animName, true); // trace();
+ trace(swagChar.animationOffsets.get(animName));
- txtOffsetShit.text = 'Offset: ' + swagChar.offset;
- };
- dropDownSetup = true;
+ txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
}
var _file:FileReference;
- function saveOffsets(saveString:String)
+ function saveOffsets(saveString:String, fileName:String)
{
if ((saveString != null) && (saveString.length > 0))
{
@@ -502,7 +566,7 @@ class DebugBoundingState extends FlxState
_file.addEventListener(Event.COMPLETE, onSaveComplete);
_file.addEventListener(Event.CANCEL, onSaveCancel);
_file.addEventListener(IOErrorEvent.IO_ERROR, onSaveError);
- _file.save(saveString, swagChar.characterId + "Offsets.txt");
+ _file.save(saveString,);
}
}
@@ -542,5 +606,5 @@ class DebugBoundingState extends FlxState
enum abstract ANIMDEBUGVIEW(String)
{
var SPRITESHEET;
- var OFFSETSHIT;
+ var ANIMATIONS;
}
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 9453c8c94..bb08e8d6b 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -1,14 +1,22 @@
package funkin.ui.debug.charting;
+import funkin.play.character.CharacterData;
+import funkin.util.Constants;
+import funkin.util.SerializerUtil;
+import funkin.play.song.SongData.SongChartData;
+import funkin.play.song.SongData.SongMetadata;
import flixel.util.FlxTimer;
import funkin.util.SortUtil;
import funkin.input.Cursor;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song;
+import funkin.play.song.SongMigrator;
+import funkin.play.song.SongValidator;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.song.SongData.SongTimeChange;
+import funkin.util.FileUtil;
import haxe.io.Path;
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
@@ -41,6 +49,9 @@ class ChartEditorDialogHandler
static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata-chargroup');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
+ static final CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart');
+ static final CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-entry');
+ static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart');
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
/**
@@ -72,42 +83,31 @@ class ChartEditorDialogHandler
//
// Create Song Wizard
//
+ openCreateSongWizard(state, false);
+ }
- // Step 1. Upload Instrumental
- var uploadInstDialog:Dialog = openUploadInstDialog(state, false);
- uploadInstDialog.onDialogClosed = function(_event) {
- state.isHaxeUIDialogOpen = false;
- if (_event.button == DialogButton.APPLY)
- {
- // Step 2. Song Metadata
- var songMetadataDialog:Dialog = openSongMetadataDialog(state);
- songMetadataDialog.onDialogClosed = function(_event) {
- state.isHaxeUIDialogOpen = false;
- if (_event.button == DialogButton.APPLY)
- {
- // Step 3. Upload Vocals
- // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
- openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog
- }
- else
- {
- // User cancelled the wizard! Back to the welcome dialog.
- openWelcomeDialog(state);
- }
- };
- }
- else
- {
- // User cancelled the wizard! Back to the welcome dialog.
- openWelcomeDialog(state);
- }
- };
+ var linkImportChartLegacy:Link = dialog.findComponent('splashImportChartLegacy', Link);
+ linkImportChartLegacy.onClick = function(_event) {
+ // Hide the welcome dialog
+ dialog.hideDialog(DialogButton.CANCEL);
+
+ // Open the "Import Chart" dialog
+ openImportChartWizard(state, 'legacy', false);
+ };
+
+ var buttonBrowse:Button = dialog.findComponent('splashBrowse', Button);
+ buttonBrowse.onClick = function(_event) {
+ // Hide the welcome dialog
+ dialog.hideDialog(DialogButton.CANCEL);
+
+ // Open the "Open Chart" dialog
+ openBrowseWizard(state, false);
}
var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
var songList:Array = SongDataParser.listSongIds();
- songList.sort(SortUtil.alphabetical);
+ songList.sort(SortUtil.alphabetically);
for (targetSongId in songList)
{
@@ -132,6 +132,120 @@ class ChartEditorDialogHandler
return dialog;
}
+ /**
+ * Open the wizard for opening an existing chart from individual files.
+ * @param state
+ * @param closable
+ */
+ public static function openBrowseWizard(state:ChartEditorState, closable:Bool):Void
+ {
+ // Open the "Open Chart" wizard
+ // Step 1. Open Chart
+ var openChartDialog:Dialog = openChartDialog(state);
+ openChartDialog.onDialogClosed = function(_event) {
+ state.isHaxeUIDialogOpen = false;
+ if (_event.button == DialogButton.APPLY)
+ {
+ // Step 2. Upload instrumental
+ var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
+ uploadInstDialog.onDialogClosed = function(_event) {
+ state.isHaxeUIDialogOpen = false;
+ if (_event.button == DialogButton.APPLY)
+ {
+ // Step 3. Upload Vocals
+ // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
+ var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
+ uploadVocalsDialog.onDialogClosed = function(_event) {
+ state.isHaxeUIDialogOpen = false;
+ state.postLoadInstrumental();
+ }
+ }
+ else
+ {
+ // User cancelled the wizard! Back to the welcome dialog.
+ openWelcomeDialog(state);
+ }
+ };
+ }
+ else
+ {
+ // User cancelled the wizard! Back to the welcome dialog.
+ openWelcomeDialog(state);
+ }
+ };
+ }
+
+ public static function openImportChartWizard(state:ChartEditorState, format:String, closable:Bool):Void
+ {
+ // Open the "Open Chart" wizard
+ // Step 1. Open Chart
+ var openChartDialog:Dialog = openImportChartDialog(state, format);
+ openChartDialog.onDialogClosed = function(_event) {
+ state.isHaxeUIDialogOpen = false;
+ if (_event.button == DialogButton.APPLY)
+ {
+ // Step 2. Upload instrumental
+ var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
+ uploadInstDialog.onDialogClosed = function(_event) {
+ state.isHaxeUIDialogOpen = false;
+ if (_event.button == DialogButton.APPLY)
+ {
+ // Step 3. Upload Vocals
+ // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
+ var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
+ uploadVocalsDialog.onDialogClosed = function(_event) {
+ state.isHaxeUIDialogOpen = false;
+ state.postLoadInstrumental();
+ }
+ }
+ else
+ {
+ // User cancelled the wizard! Back to the welcome dialog.
+ openWelcomeDialog(state);
+ }
+ };
+ }
+ else
+ {
+ // User cancelled the wizard! Back to the welcome dialog.
+ openWelcomeDialog(state);
+ }
+ };
+ }
+
+ public static function openCreateSongWizard(state:ChartEditorState, closable:Bool):Void
+ {
+ // Step 1. Upload Instrumental
+ var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
+ uploadInstDialog.onDialogClosed = function(_event) {
+ state.isHaxeUIDialogOpen = false;
+ if (_event.button == DialogButton.APPLY)
+ {
+ // Step 2. Song Metadata
+ var songMetadataDialog:Dialog = openSongMetadataDialog(state);
+ songMetadataDialog.onDialogClosed = function(_event) {
+ state.isHaxeUIDialogOpen = false;
+ if (_event.button == DialogButton.APPLY)
+ {
+ // Step 3. Upload Vocals
+ // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
+ openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog
+ }
+ else
+ {
+ // User cancelled the wizard! Back to the welcome dialog.
+ openWelcomeDialog(state);
+ }
+ };
+ }
+ else
+ {
+ // User cancelled the wizard! Back to the welcome dialog.
+ openWelcomeDialog(state);
+ }
+ };
+ }
+
/**
* Builds and opens a dialog where the user uploads an instrumental for the current song.
* @param state The current chart editor state.
@@ -216,11 +330,20 @@ class ChartEditorDialogHandler
}
else
{
+ var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
+ {
+ 'File format (${path.ext}) not supported for instrumental track (${path.file}.${path.ext})';
+ }
+ else
+ {
+ 'Failed to load instrumental track (${path.file}.${path.ext})';
+ }
+
// Tell the user the load was successful.
NotificationManager.instance.addNotification(
{
title: 'Failure',
- body: 'Failed to load instrumental track (${path.file}.${path.ext})',
+ body: message,
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@@ -420,12 +543,6 @@ class ChartEditorDialogHandler
moveCharGroup(event.data.id);
};
- if (key == null)
- {
- // Find the next available player character.
- trace(charGroupPlayer.dataSource.data);
- }
-
var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown);
charGroupOpponent.onChange = function(event:UIEvent) {
charData.opponent = event.data.id;
@@ -483,8 +600,8 @@ class ChartEditorDialogHandler
for (charKey in charIdsForVocals)
{
trace('Adding vocal upload for character ${charKey}');
- var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey);
- var charName:String = charMetadata.characterName;
+ var charMetadata:CharacterData = CharacterDataParser.fetchCharacterData(charKey);
+ var charName:String = charMetadata != null ? charMetadata.name : charKey;
var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
@@ -511,11 +628,20 @@ class ChartEditorDialogHandler
}
else
{
+ var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
+ {
+ 'File format (${path.ext}) not supported for vocal track (${path.file}.${path.ext})';
+ }
+ else
+ {
+ 'Failed to load vocal track (${path.file}.${path.ext})';
+ }
+
// Vocals failed to load.
NotificationManager.instance.addNotification(
{
title: 'Failure',
- body: 'Failed to load vocal track (${path.file}.${path.ext})',
+ body: message,
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@@ -552,6 +678,284 @@ class ChartEditorDialogHandler
return dialog;
}
+ /**
+ * Builds and opens a dialog where the user upload the JSON files for a song.
+ * @param state The current chart editor state.
+ * @param closable Whether the dialog can be closed by the user.
+ * @return The dialog that was opened.
+ */
+ @:haxe.warning('-WVarInit')
+ public static function openChartDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
+ {
+ var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable);
+
+ var buttonCancel:Button = dialog.findComponent('dialogCancel', Button);
+ buttonCancel.onClick = function(_event) {
+ dialog.hideDialog(DialogButton.CANCEL);
+ }
+
+ var chartContainerA:Component = dialog.findComponent('chartContainerA');
+ var chartContainerB:Component = dialog.findComponent('chartContainerB');
+
+ var songMetadata:Map = [];
+ var songChartData:Map = [];
+
+ var buttonContinue:Button = dialog.findComponent('dialogContinue', Button);
+ buttonContinue.onClick = function(_event) {
+ state.loadSong(songMetadata, songChartData);
+
+ dialog.hideDialog(DialogButton.APPLY);
+ }
+
+ var onDropFileMetadataVariation:String->Label->String->Void;
+ var onClickMetadataVariation:String->Label->UIEvent->Void;
+ var onDropFileChartDataVariation:String->Label->String->Void;
+ var onClickChartDataVariation:String->Label->UIEvent->Void;
+
+ var constructVariationEntries:Array->Void = function(variations:Array) {
+ // Clear the chart container.
+ while (chartContainerB.getComponentAt(0) != null)
+ {
+ chartContainerB.removeComponent(chartContainerB.getComponentAt(0));
+ }
+
+ // Build an entry for -chart.json.
+ var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
+ var songDefaultChartDataEntryLabel:Label = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label);
+ songDefaultChartDataEntryLabel.text = 'Drag and drop -chart.json file, or click to browse.';
+
+ songDefaultChartDataEntry.onClick = onClickChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel);
+ addDropHandler(songDefaultChartDataEntry, onDropFileChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel));
+ chartContainerB.addComponent(songDefaultChartDataEntry);
+
+ for (variation in variations)
+ {
+ // Build entries for -metadata-.json.
+ var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
+ var songVariationMetadataEntryLabel:Label = songVariationMetadataEntry.findComponent('chartEntryLabel', Label);
+ songVariationMetadataEntryLabel.text = 'Drag and drop -metadata-${variation}.json file, or click to browse.';
+
+ songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
+ addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel));
+ chartContainerB.addComponent(songVariationMetadataEntry);
+
+ // Build entries for -chart-.json.
+ var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
+ var songVariationChartDataEntryLabel:Label = songVariationChartDataEntry.findComponent('chartEntryLabel', Label);
+ songVariationChartDataEntryLabel.text = 'Drag and drop -chart-${variation}.json file, or click to browse.';
+
+ songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel);
+ addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel));
+ chartContainerB.addComponent(songVariationChartDataEntry);
+ }
+ }
+
+ onDropFileMetadataVariation = function(variation:String, label:Label, pathStr:String) {
+ var path:Path = new Path(pathStr);
+ trace('Dropped JSON file (${path})');
+
+ var songMetadataJson:Dynamic = FileUtil.readJSONFromPath(path.toString());
+ var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import');
+ songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import');
+
+ songMetadata.set(variation, songMetadataVariation);
+
+ // Tell the user the load was successful.
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Success',
+ body: 'Loaded metadata file (${path.file}.${path.ext})',
+ type: NotificationType.Success,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+
+ label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
+
+ if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations);
+ };
+
+ onClickMetadataVariation = function(variation:String, label:Label, _event:UIEvent) {
+ Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [
+ {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) {
+ if (selectedFile != null)
+ {
+ trace('Selected file: ' + selectedFile.name);
+
+ var songMetadataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes);
+ var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import');
+ songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import');
+ songMetadataVariation.variation = variation;
+
+ songMetadata.set(variation, songMetadataVariation);
+
+ // Tell the user the load was successful.
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Success',
+ body: 'Loaded metadata file (${selectedFile.name})',
+ type: NotificationType.Success,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+
+ label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
+
+ if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations);
+ }
+ });
+ }
+
+ onDropFileChartDataVariation = function(variation:String, label:Label, pathStr:String) {
+ var path:Path = new Path(pathStr);
+ trace('Dropped JSON file (${path})');
+
+ var songChartDataJson:Dynamic = FileUtil.readJSONFromPath(path.toString());
+ var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import');
+ songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import');
+
+ songChartData.set(variation, songChartDataVariation);
+
+ // Tell the user the load was successful.
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Success',
+ body: 'Loaded chart data file (${path.file}.${path.ext})',
+ type: NotificationType.Success,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+
+ label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
+ };
+
+ onClickChartDataVariation = function(variation:String, label:Label, _event:UIEvent) {
+ Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [
+ {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) {
+ if (selectedFile != null)
+ {
+ trace('Selected file: ' + selectedFile.name);
+
+ var songChartDataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes);
+ var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import');
+ songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import');
+
+ songChartData.set(variation, songChartDataVariation);
+
+ // Tell the user the load was successful.
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Success',
+ body: 'Loaded chart data file (${selectedFile.name})',
+ type: NotificationType.Success,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+
+ label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
+ }
+ });
+ }
+
+ var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
+ var metadataEntryLabel:Label = metadataEntry.findComponent('chartEntryLabel', Label);
+ metadataEntryLabel.text = 'Drag and drop -metadata.json file, or click to browse.';
+
+ metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel);
+ addDropHandler(metadataEntry, onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel));
+
+ chartContainerA.addComponent(metadataEntry);
+
+ return dialog;
+ }
+
+ /**
+ * Builds and opens a dialog where the user can import a chart from an existing file format.
+ * @param state The current chart editor state.
+ * @param format The format to import from.
+ * @param closable
+ * @return Dialog
+ */
+ public static function openImportChartDialog(state:ChartEditorState, format:String, ?closable:Bool = true):Dialog
+ {
+ var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable);
+
+ var prettyFormat:String = switch (format)
+ {
+ case 'legacy': 'FNF Legacy';
+ default: 'Unknown';
+ }
+
+ var fileFilter = switch (format)
+ {
+ case 'legacy': {label: 'JSON Data File (.json)', extension: 'json'};
+ default: null;
+ }
+
+ dialog.title = 'Import Chart - ${prettyFormat}';
+
+ var buttonCancel:Button = dialog.findComponent('dialogCancel', Button);
+
+ buttonCancel.onClick = function(_event) {
+ dialog.hideDialog(DialogButton.CANCEL);
+ }
+
+ var importBox:Box = dialog.findComponent('importBox', Box);
+
+ importBox.onMouseOver = function(_event) {
+ importBox.swapClass('upload-bg', 'upload-bg-hover');
+ Cursor.cursorMode = Pointer;
+ }
+
+ importBox.onMouseOut = function(_event) {
+ importBox.swapClass('upload-bg-hover', 'upload-bg');
+ Cursor.cursorMode = Default;
+ }
+
+ var onDropFile:String->Void;
+
+ importBox.onClick = function(_event) {
+ Dialogs.openBinaryFile('Import Chart - ${prettyFormat}', [fileFilter], function(selectedFile:SelectedFileInfo) {
+ if (selectedFile != null)
+ {
+ trace('Selected file: ' + selectedFile.fullPath);
+ var selectedFileJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes);
+ var songMetadata:SongMetadata = SongMigrator.migrateSongMetadataFromLegacy(selectedFileJson);
+ var songChartData:SongChartData = SongMigrator.migrateSongChartDataFromLegacy(selectedFileJson);
+
+ state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
+
+ dialog.hideDialog(DialogButton.APPLY);
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Success',
+ body: 'Loaded chart file (${selectedFile.name})',
+ type: NotificationType.Success,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+ }
+ });
+ }
+
+ onDropFile = function(pathStr:String) {
+ var path:Path = new Path(pathStr);
+ var selectedFileJson:Dynamic = FileUtil.readJSONFromPath(path.toString());
+ var songMetadata:SongMetadata = SongMigrator.migrateSongMetadataFromLegacy(selectedFileJson);
+ var songChartData:SongChartData = SongMigrator.migrateSongChartDataFromLegacy(selectedFileJson);
+
+ state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
+
+ dialog.hideDialog(DialogButton.APPLY);
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Success',
+ body: 'Loaded chart file (${path.file}.${path.ext})',
+ type: NotificationType.Success,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+ };
+
+ addDropHandler(importBox, onDropFile);
+
+ return dialog;
+ }
+
/**
* Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor.
*
@@ -571,6 +975,8 @@ class ChartEditorDialogHandler
static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Dialog
{
var dialog:Dialog = cast state.buildComponent(key);
+ if (dialog == null) return null;
+
dialog.destroyOnClose = true;
dialog.closable = closable;
dialog.showDialog(modal);
diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
index bc68709c5..768e0be52 100644
--- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
@@ -31,7 +31,7 @@ class ChartEditorEventSprite extends FlxSprite
/**
* The image used for all song events. Cached for performance.
*/
- var eventGraphic:BitmapData;
+ static var eventSpriteBasic:BitmapData;
public function new(parent:ChartEditorState)
{
diff --git a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx
new file mode 100644
index 000000000..27951f079
--- /dev/null
+++ b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx
@@ -0,0 +1,144 @@
+package funkin.ui.debug.charting;
+
+import funkin.play.song.SongData.SongEventData;
+import funkin.play.song.SongData.SongNoteData;
+import flixel.math.FlxMath;
+import flixel.FlxSprite;
+import flixel.util.FlxColor;
+import flixel.util.FlxSpriteUtil;
+
+/**
+ * Handles the note scrollbar preview in the chart editor.
+ */
+class ChartEditorNotePreview extends FlxSprite
+{
+ //
+ // Constants
+ //
+ static final NOTE_WIDTH:Int = 5;
+ static final NOTE_HEIGHT:Int = 1;
+ static final WIDTH:Int = NOTE_WIDTH * 9;
+
+ static final BG_COLOR:FlxColor = FlxColor.GRAY;
+ static final LEFT_COLOR:FlxColor = 0xFFFF22AA;
+ static final DOWN_COLOR:FlxColor = 0xFF00EEFF;
+ static final UP_COLOR:FlxColor = 0xFF00CC00;
+ static final RIGHT_COLOR:FlxColor = 0xFFCC1111;
+ static final EVENT_COLOR:FlxColor = 0xFF111111;
+
+ var previewHeight:Int;
+
+ public function new(height:Int)
+ {
+ super(0, 0);
+ this.previewHeight = height;
+ buildBackground();
+ }
+
+ /**
+ * Build the initial sprite for the preview.
+ */
+ function buildBackground():Void
+ {
+ makeGraphic(WIDTH, 0, BG_COLOR);
+ }
+
+ /**
+ * Erase all notes from the preview.
+ */
+ public function erase():Void
+ {
+ drawRect(0, 0, WIDTH, previewHeight, BG_COLOR);
+ }
+
+ /**
+ * Add a single note to the preview.
+ * @param note The data for the note.
+ * @param songLengthInMs The total length of the song in milliseconds.
+ */
+ public function addNote(note:SongNoteData, songLengthInMs:Int):Void
+ {
+ var noteDir:Int = note.getDirection();
+ var mustHit:Bool = note.getStrumlineIndex() == 0;
+ drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs);
+ }
+
+ /**
+ * Add a song event to the preview.
+ * @param event The data for the event.
+ * @param songLengthInMs The total length of the song in milliseconds.
+ */
+ public function addEvent(event:SongEventData, songLengthInMs:Int):Void
+ {
+ drawNote(-1, false, Std.int(event.time), songLengthInMs);
+ }
+
+ /**
+ * Add an array of notes to the preview.
+ * @param notes The data for the notes.
+ * @param songLengthInMs The total length of the song in milliseconds.
+ */
+ public function addNotes(notes:Array, songLengthInMs:Int):Void
+ {
+ for (note in notes)
+ {
+ addNote(note, songLengthInMs);
+ }
+ }
+
+ /**
+ * Add an array of events to the preview.
+ * @param events The data for the events.
+ * @param songLengthInMs The total length of the song in milliseconds.
+ */
+ public function addEvents(events:Array, songLengthInMs:Int):Void
+ {
+ for (event in events)
+ {
+ addEvent(event, songLengthInMs);
+ }
+ }
+
+ /**
+ * Draws a note on the preview.
+ * @param dir Note data.
+ * @param mustHit False if opponent, true if player.
+ * @param strumTimeInMs Time in milliseconds to strum the note.
+ * @param songLengthInMs Length of the song in milliseconds.
+ */
+ function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int):Void
+ {
+ var color:FlxColor = switch (dir)
+ {
+ case 0: LEFT_COLOR;
+ case 1: DOWN_COLOR;
+ case 2: UP_COLOR;
+ case 3: RIGHT_COLOR;
+ default: EVENT_COLOR;
+ };
+
+ var noteX:Float = NOTE_WIDTH * dir;
+ if (mustHit) noteX += NOTE_WIDTH * 4;
+ if (dir == -1) noteX = NOTE_WIDTH * 8;
+
+ var noteY:Float = FlxMath.remapToRange(strumTimeInMs, 0, songLengthInMs, 0, previewHeight);
+
+ drawRect(noteX, noteY, NOTE_WIDTH, NOTE_HEIGHT, color);
+ }
+
+ function eraseNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int):Void
+ {
+ var noteX:Float = NOTE_WIDTH * dir;
+ if (mustHit) noteX += NOTE_WIDTH * 4;
+ if (dir == -1) noteX = NOTE_WIDTH * 8;
+
+ var noteY:Float = FlxMath.remapToRange(strumTimeInMs, 0, songLengthInMs, 0, previewHeight);
+
+ drawRect(noteX, noteY, NOTE_WIDTH, NOTE_HEIGHT, BG_COLOR);
+ }
+
+ inline function drawRect(noteX:Float, noteY:Float, width:Int, height:Int, color:FlxColor):Void
+ {
+ FlxSpriteUtil.drawRect(this, noteX, noteY, width, height, color);
+ }
+}
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 2238fff3f..e1a55f947 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -7,16 +7,21 @@ import flixel.group.FlxSpriteGroup;
import flixel.input.keyboard.FlxKey;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
-import flixel.sound.FlxSound;
+import flixel.system.FlxSound;
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+import flixel.tweens.misc.VarTween;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
import flixel.util.FlxTimer;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VoicesGroup;
import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.data.notestyle.NoteStyleRegistry;
import funkin.input.Cursor;
import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent;
+import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.HealthIcon;
import funkin.play.notes.NoteSprite;
import funkin.play.notes.Strumline;
@@ -26,22 +31,26 @@ import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongNoteData;
+import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.song.SongDataUtils;
import funkin.ui.debug.charting.ChartEditorCommand;
+import funkin.ui.debug.charting.ChartEditorCommand;
import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState;
+import funkin.util.Constants;
import funkin.util.DateUtil;
import funkin.util.FileUtil;
import funkin.util.SerializerUtil;
import funkin.util.SortUtil;
import funkin.util.WindowUtil;
import haxe.DynamicAccess;
+import haxe.io.Bytes;
import haxe.io.Path;
import haxe.ui.components.Label;
import haxe.ui.components.Slider;
-import haxe.ui.containers.dialogs.Dialog;
+import haxe.ui.containers.dialogs.CollapsibleDialog;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
@@ -51,6 +60,7 @@ import haxe.ui.events.DragEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
+import openfl.Assets;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
@@ -65,6 +75,7 @@ using Lambda;
* @author MasterEric
*/
// Give other classes access to private instance fields
+
@:allow(funkin.ui.debug.charting.ChartEditorCommand)
@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
@:allow(funkin.ui.debug.charting.ChartEditorThemeHandler)
@@ -76,19 +87,19 @@ class ChartEditorState extends HaxeUIState
*/
// ==============================
// XML Layouts
- static final CHART_EDITOR_LAYOUT = Paths.ui('chart-editor/main-view');
+ static final CHART_EDITOR_LAYOUT:String = Paths.ui('chart-editor/main-view');
- static final CHART_EDITOR_NOTIFBAR_LAYOUT = Paths.ui('chart-editor/components/notifbar');
- static final CHART_EDITOR_PLAYBARHEAD_LAYOUT = Paths.ui('chart-editor/components/playbar-head');
+ static final CHART_EDITOR_NOTIFBAR_LAYOUT:String = Paths.ui('chart-editor/components/notifbar');
+ static final CHART_EDITOR_PLAYBARHEAD_LAYOUT:String = Paths.ui('chart-editor/components/playbar-head');
- static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT = Paths.ui('chart-editor/toolbox/tools');
- static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT = Paths.ui('chart-editor/toolbox/notedata');
- static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT = Paths.ui('chart-editor/toolbox/eventdata');
- static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT = Paths.ui('chart-editor/toolbox/metadata');
- static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT = Paths.ui('chart-editor/toolbox/difficulty');
- static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT = Paths.ui('chart-editor/toolbox/characters');
- static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/player-preview');
- static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/opponent-preview');
+ static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:String = Paths.ui('chart-editor/toolbox/tools');
+ static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata');
+ static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
+ static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
+ static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty');
+ static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:String = Paths.ui('chart-editor/toolbox/characters');
+ static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/player-preview');
+ static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview');
// Validation
static final SUPPORTED_MUSIC_FORMATS:Array = ['ogg'];
@@ -107,7 +118,7 @@ class ChartEditorState extends HaxeUIState
/**
* Number of notes in each player's strumline.
*/
- public static final STRUMLINE_SIZE = 4;
+ public static final STRUMLINE_SIZE:Int = 4;
/**
* The height of the menu bar in the layout.
@@ -134,10 +145,10 @@ class ChartEditorState extends HaxeUIState
*/
static final NOTIFICATION_DISMISS_TIME:Int = 5000;
- // Start performing rapid undo after this many seconds.
- static final RAPID_UNDO_DELAY:Float = 0.4;
- // Perform a rapid undo every this many seconds.
- static final RAPID_UNDO_INTERVAL:Float = 0.1;
+ /**
+ * Duration, in seconds, for the scroll easing animation.
+ */
+ static final SCROLL_EASE_DURATION:Float = 0.2;
// UI Element Colors
// Background color tint.
@@ -380,6 +391,11 @@ class ChartEditorState extends HaxeUIState
*/
var currentOpponentCharacterPlayer:CharacterPlayer = null;
+ /**
+ * The currently selected live input style.
+ */
+ var currentLiveInputStyle:LiveInputStyle = LiveInputStyle.None;
+
/**
* Whether the current view is in downscroll mode.
*/
@@ -473,6 +489,22 @@ class ChartEditorState extends HaxeUIState
return selectedDifficulty;
}
+ /**
+ * The character ID for the character which is currently selected.
+ */
+ var selectedCharacter(default, set):String = Constants.DEFAULT_CHARACTER;
+
+ function set_selectedCharacter(value:String):String
+ {
+ selectedCharacter = value;
+
+ // Make sure view is updated when the character changes.
+ noteDisplayDirty = true;
+ notePreviewDirty = true;
+
+ return selectedCharacter;
+ }
+
/**
* Whether the user is currently in Pattern Mode.
* This overrides the chart editor's normal behavior.
@@ -548,6 +580,18 @@ class ChartEditorState extends HaxeUIState
*/
var characterSelectDirty:Bool = true;
+ /**
+ * Whether the player preview toolbox have been modified and need to be updated.
+ * This happens when we switch characters.
+ */
+ var playerPreviewDirty:Bool = true;
+
+ /**
+ * Whether the opponent preview toolbox have been modified and need to be updated.
+ * This happens when we switch characters.
+ */
+ var opponentPreviewDirty:Bool = true;
+
var isInPlaytestMode:Bool = false;
/**
@@ -626,7 +670,7 @@ class ChartEditorState extends HaxeUIState
* The Dialog components representing the currently available tool windows.
* Dialogs are retained here even when collapsed or hidden.
*/
- var activeToolboxes:Map = new Map();
+ var activeToolboxes:Map = new Map();
/**
* AUDIO AND SOUND DATA
@@ -638,6 +682,11 @@ class ChartEditorState extends HaxeUIState
*/
var audioInstTrack:FlxSound;
+ /**
+ * The raw byte data for the instrumental audio track.
+ */
+ var audioInstTrackData:Bytes = null;
+
/**
* The audio track for the vocals.
*/
@@ -650,7 +699,7 @@ class ChartEditorState extends HaxeUIState
*
* When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
*/
- var audioVocalTracks:Map = new Map();
+ var audioVocalTrackData:Map = [];
/**
* CHART DATA
@@ -685,7 +734,7 @@ class ChartEditorState extends HaxeUIState
function get_currentSongMetadata():SongMetadata
{
- var result = songMetadata.get(selectedVariation);
+ var result:SongMetadata = songMetadata.get(selectedVariation);
if (result == null)
{
result = new SongMetadata('Dad Battle', 'Kawai Sprite', selectedVariation);
@@ -707,7 +756,7 @@ class ChartEditorState extends HaxeUIState
function get_currentSongChartData():SongChartData
{
- var result = songChartData.get(selectedVariation);
+ var result:SongChartData = songChartData.get(selectedVariation);
if (result == null)
{
result = new SongChartData(1.0, [], []);
@@ -729,7 +778,7 @@ class ChartEditorState extends HaxeUIState
function get_currentSongChartScrollSpeed():Float
{
- var result = currentSongChartData.scrollSpeed.get(selectedDifficulty);
+ var result:Null = currentSongChartData.scrollSpeed.get(selectedDifficulty);
if (result == null)
{
// Initialize to the default value if not set.
@@ -752,11 +801,12 @@ class ChartEditorState extends HaxeUIState
function get_currentSongChartNoteData():Array
{
- var result = currentSongChartData.notes.get(selectedDifficulty);
+ var result:Array = currentSongChartData.notes.get(selectedDifficulty);
if (result == null)
{
// Initialize to the default value if not set.
result = [];
+ trace('Initializing blank note data for difficulty ' + selectedDifficulty);
currentSongChartData.notes.set(selectedDifficulty, result);
return result;
}
@@ -864,6 +914,59 @@ class ChartEditorState extends HaxeUIState
return currentSongMetadata.artist = value;
}
+ var currentSongPlayableCharacters(get, null):Array;
+
+ function get_currentSongPlayableCharacters():Array
+ {
+ return currentSongMetadata.playData.playableChars.keys().array();
+ }
+
+ var currentSongCharacterPlayer(get, set):String;
+
+ function get_currentSongCharacterPlayer():String
+ {
+ // Validate selected character before returning it.
+ if (!currentSongPlayableCharacters.contains(selectedCharacter))
+ {
+ trace('Invalid character selected: ' + selectedCharacter);
+ selectedCharacter = currentSongPlayableCharacters[0];
+ }
+
+ return selectedCharacter;
+ }
+
+ function set_currentSongCharacterPlayer(value:String):String
+ {
+ if (!currentSongPlayableCharacters.contains(value))
+ {
+ trace('Invalid character selected: ' + value);
+ return value;
+ }
+
+ return selectedCharacter = value;
+ }
+
+ var currentSongCharacterOpponent(get, set):String;
+
+ function get_currentSongCharacterOpponent():String
+ {
+ // Validate selected character before returning it.
+ if (!currentSongPlayableCharacters.contains(selectedCharacter))
+ {
+ trace('Invalid character selected: ' + selectedCharacter);
+ selectedCharacter = currentSongPlayableCharacters[0];
+ }
+
+ var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter);
+ return playableCharData.opponent;
+ }
+
+ function set_currentSongCharacterOpponent(value:String):String
+ {
+ var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter);
+ return playableCharData.opponent = value;
+ }
+
/**
* RENDER OBJECTS
*/
@@ -911,17 +1014,11 @@ class ChartEditorState extends HaxeUIState
*/
var gridSpectrogram:PolygonSpectogram;
- /**
- * The rectangle used for the note preview area.
- * Should span the full height of the song. We scribble on this to draw the preview.
- */
- var notePreviewBitmap:BitmapData;
-
/**
* The sprite used to display the note preview area.
* We move this up and down to scroll the preview.
*/
- var notePreviewSprite:FlxSprite;
+ var notePreview:ChartEditorNotePreview;
/**
* The rectangular sprite used for rendering the selection box.
@@ -987,6 +1084,8 @@ class ChartEditorState extends HaxeUIState
currentTheme = ChartEditorTheme.Light;
buildGrid();
+ // buildSpectrogram(audioInstTrack);
+ buildNotePreview();
buildSelectionBox();
// Add the HaxeUI components after the grid so they're on top.
@@ -1048,7 +1147,7 @@ class ChartEditorState extends HaxeUIState
gridGhostEvent = new ChartEditorEventSprite(this);
gridGhostEvent.alpha = 0.6;
- gridGhostEvent.eventData = new SongEventData(-1, "", {});
+ gridGhostEvent.eventData = new SongEventData(-1, '', {});
gridGhostEvent.visible = false;
add(gridGhostEvent);
@@ -1062,15 +1161,15 @@ class ChartEditorState extends HaxeUIState
gridPlayhead = new FlxSpriteGroup();
add(gridPlayhead);
- var playheadWidth = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2);
- var playheadBaseYPos = MENU_BAR_HEIGHT + GRID_TOP_PAD;
+ var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2);
+ var playheadBaseYPos:Float = MENU_BAR_HEIGHT + GRID_TOP_PAD;
gridPlayhead.setPosition(gridTiledSprite.x, playheadBaseYPos);
- var playheadSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
+ var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH;
playheadSprite.y = 0;
gridPlayhead.add(playheadSprite);
- var playheadBlock = ChartEditorThemeHandler.buildPlayheadBlock();
+ var playheadBlock:FlxSprite = ChartEditorThemeHandler.buildPlayheadBlock();
playheadBlock.x = -PLAYHEAD_SCROLL_AREA_WIDTH;
playheadBlock.y = -PLAYHEAD_HEIGHT / 2;
gridPlayhead.add(playheadBlock);
@@ -1100,7 +1199,7 @@ class ChartEditorState extends HaxeUIState
setSelectionBoxBounds();
}
- function setSelectionBoxBounds(?bounds:FlxRect = null):Void
+ function setSelectionBoxBounds(bounds:FlxRect = null):Void
{
if (bounds == null)
{
@@ -1118,17 +1217,21 @@ class ChartEditorState extends HaxeUIState
}
}
+ function buildNotePreview():Void
+ {
+ var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - 200;
+ notePreview = new ChartEditorNotePreview(height);
+ notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
+ add(notePreview);
+ }
+
function buildSpectrogram(target:FlxSound):Void
{
- gridSpectrogram = new PolygonSpectogram(target, SPECTROGRAM_COLOR, FlxG.height / 2, Math.floor(FlxG.height / 2));
- // Halfway through the grid.
- // gridSpectrogram.x = gridTiledSprite.x + STRUMLINE_SIZE * GRID_SIZE;
- // gridSpectrogram.y = gridTiledSprite.y;
- gridSpectrogram.x = 200;
- gridSpectrogram.y = 200;
- gridSpectrogram.visType = STATIC; // We move the spectrogram manually.
+ gridSpectrogram = new PolygonSpectogram(FlxG.sound.music, FlxColor.RED, FlxG.height / 2, Math.floor(FlxG.height / 2));
+ gridSpectrogram.x += 170;
+ gridSpectrogram.scrollFactor.set();
gridSpectrogram.waveAmplitude = 50;
- gridSpectrogram.scrollFactor.set(0, 0);
+ gridSpectrogram.visType = UPDATED;
add(gridSpectrogram);
}
@@ -1169,7 +1272,7 @@ class ChartEditorState extends HaxeUIState
playbarHead.allowFocus = false;
playbarHead.width = FlxG.width;
playbarHead.height = 10;
- playbarHead.styleString = "padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;";
+ playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;';
playbarHead.onDragStart = function(_:DragEvent) {
playbarHeadDragging = true;
@@ -1198,11 +1301,17 @@ class ChartEditorState extends HaxeUIState
if (playbarHeadDraggingWasPlaying)
{
playbarHeadDraggingWasPlaying = false;
- startAudioPlayback();
+ // Disabled code to resume song playback on drag.
+ // startAudioPlayback();
}
}
add(playbarHeadLayout);
+
+ // Setup notifications.
+ @:privateAccess
+ // NotificationManager.GUTTER_SIZE = 56;
+ NotificationManager.GUTTER_SIZE = 20;
}
/**
@@ -1221,8 +1330,10 @@ class ChartEditorState extends HaxeUIState
// Add functionality to the menu items.
addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
+ addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseWizard(this, true));
addUIClickListener('menubarItemSaveChartAs', _ -> exportAllSongData());
addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
+ addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true));
addUIClickListener('menubarItemUndo', _ -> undoLastCommand());
@@ -1230,10 +1341,21 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('menubarItemCopy', function(_) {
// Doesn't use a command because it's not undoable.
+
+ // Calculate a single time offset for all the notes and events.
+ var timeOffset:Null = currentNoteSelection.length > 0 ? Std.int(currentNoteSelection[0].time) : null;
+ if (currentEventSelection.length > 0)
+ {
+ if (timeOffset == null || currentEventSelection[0].time < timeOffset)
+ {
+ timeOffset = Std.int(currentEventSelection[0].time);
+ }
+ }
+
SongDataUtils.writeItemsToClipboard(
{
- notes: SongDataUtils.buildNoteClipboard(currentNoteSelection),
- events: SongDataUtils.buildEventClipboard(currentEventSelection),
+ notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset),
+ events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset),
});
});
@@ -1271,6 +1393,10 @@ class ChartEditorState extends HaxeUIState
// addUIClickListener('menubarItemSelectBeforeCursor', _ -> doSomething());
// addUIClickListener('menubarItemSelectAfterCursor', _ -> doSomething());
+ addUIChangeListener('menubarItemInputStyleGroup', function(event:UIEvent) {
+ trace('Change input style: ${event.target}');
+ });
+
addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this));
addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this));
@@ -1314,11 +1440,13 @@ class ChartEditorState extends HaxeUIState
var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label);
addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) {
var pitch:Float = event.value * 2.0 / 100.0;
+ pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
#if FLX_PITCH
if (audioInstTrack != null) audioInstTrack.pitch = pitch;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
#end
- playbackSpeedLabel.text = 'Playback Speed - ${Std.int(pitch * 100) / 100}x';
+ var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places.
+ playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x';
});
addUIChangeListener('menubarItemToggleToolboxTools',
@@ -1427,21 +1555,7 @@ class ChartEditorState extends HaxeUIState
// DEBUG
#if debug
- if (FlxG.keys.justPressed.F)
- {
- NotificationManager.instance.addNotification(
- {
- title: 'This is a Notification',
- body: 'Hello, world!',
- type: NotificationType.Info,
- expiryMs: NOTIFICATION_DISMISS_TIME
- // styleNames: 'cssStyleName',
- // icon: 'assetPath',
- // actions: ['action1', 'action2']
- });
- }
-
- if (FlxG.keys.justPressed.E)
+ if (FlxG.keys.justPressed.E && !isHaxeUIDialogOpen)
{
currentSongMetadata.timeChanges[0].timeSignatureNum = (currentSongMetadata.timeChanges[0].timeSignatureNum == 4 ? 3 : 4);
}
@@ -1450,9 +1564,9 @@ class ChartEditorState extends HaxeUIState
// Right align the BF health icon.
// Base X position to the right of the grid.
- var baseHealthIconXPos = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15;
+ var baseHealthIconXPos:Float = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15;
// Will be 0 when not bopping. When bopping, will increase to push the icon left.
- var healthIconOffset = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
+ var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
healthIconBF.x = baseHealthIconXPos - healthIconOffset;
}
@@ -1486,8 +1600,9 @@ class ChartEditorState extends HaxeUIState
healthIconBF.onStepHit(Conductor.currentStep);
}
- // if (shouldPlayMetronome)
- // playMetronomeTick(false);
+ // Updating these every step keeps it more accurate.
+ // playerPreviewDirty = true;
+ // opponentPreviewDirty = true;
return true;
}
@@ -1500,29 +1615,36 @@ class ChartEditorState extends HaxeUIState
// Don't scroll when the cursor is over the UI.
if (isCursorOverHaxeUI) return;
- // Amount to scroll the grid.
- var scrollAmount:Float = 0;
- // Amount to scroll the playhead relative to the grid.
- var playheadAmount:Float = 0;
- var shouldPause:Bool = false;
+ var scrollAmount:Float = 0; // Amount to scroll the grid.
+ var playheadAmount:Float = 0; // Amount to scroll the playhead relative to the grid.
+ var shouldPause:Bool = false; // Whether to pause the song when scrolling.
+ var shouldEase:Bool = false; // Whether to ease the scroll.
// Up Arrow = Scroll Up
- if (upKeyHandler.activated)
+ if (upKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD)
{
scrollAmount = -GRID_SIZE * 0.25 * 5.0;
shouldPause = true;
}
// Down Arrow = Scroll Down
- if (downKeyHandler.activated)
+ if (downKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD)
{
scrollAmount = GRID_SIZE * 0.25 * 5.0;
shouldPause = true;
}
- // PAGE UP = Jump Up 1 Measure
+ // PAGE UP = Jump up to nearest measure
if (pageUpKeyHandler.activated)
{
- scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure;
+ var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure;
+ var targetScrollPosition:Float = Math.floor(scrollPositionInPixels / measureHeight) * measureHeight;
+ // If we would move less than one grid, instead move to the top of the previous measure.
+ if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE)
+ {
+ targetScrollPosition -= GRID_SIZE * 4 * Conductor.beatsPerMeasure;
+ }
+ scrollAmount = targetScrollPosition - scrollPositionInPixels;
+
shouldPause = true;
}
if (playbarButtonPressed == 'playbarBack')
@@ -1532,10 +1654,18 @@ class ChartEditorState extends HaxeUIState
shouldPause = true;
}
- // PAGE DOWN = Jump Down 1 Measure
+ // PAGE DOWN = Jump down to nearest measure
if (pageDownKeyHandler.activated)
{
- scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure;
+ var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure;
+ var targetScrollPosition:Float = Math.ceil(scrollPositionInPixels / measureHeight) * measureHeight;
+ // If we would move less than one grid, instead move to the top of the next measure.
+ if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE)
+ {
+ targetScrollPosition += GRID_SIZE * 4 * Conductor.beatsPerMeasure;
+ }
+ scrollAmount = targetScrollPosition - scrollPositionInPixels;
+
shouldPause = true;
}
if (playbarButtonPressed == 'playbarForward')
@@ -1613,12 +1743,26 @@ class ChartEditorState extends HaxeUIState
shouldPause = true;
}
- // Apply the scroll amount.
- this.scrollPositionInPixels += scrollAmount;
- this.playheadPositionInPixels += playheadAmount;
+ if (Math.abs(scrollAmount) > GRID_SIZE * 8)
+ {
+ shouldEase = true;
+ }
// Resync the conductor and audio tracks.
- if (scrollAmount != 0 || playheadAmount != 0) moveSongToScrollPosition();
+ if (scrollAmount != 0 || playheadAmount != 0)
+ {
+ this.playheadPositionInPixels += playheadAmount;
+ if (shouldEase)
+ {
+ easeSongToScrollPosition(this.scrollPositionInPixels + scrollAmount);
+ }
+ else
+ {
+ // Apply the scroll amount.
+ this.scrollPositionInPixels += scrollAmount;
+ moveSongToScrollPosition();
+ }
+ }
if (shouldPause) stopAudioPlayback();
}
@@ -1664,8 +1808,8 @@ class ChartEditorState extends HaxeUIState
function handleCursor():Void
{
// Note: If a menu is open in HaxeUI, don't handle cursor behavior.
- var shouldHandleCursor = !isCursorOverHaxeUI || (selectionBoxStartPos != null);
- var eventColumn = (STRUMLINE_SIZE * 2 + 1) - 1;
+ var shouldHandleCursor:Bool = !isCursorOverHaxeUI || (selectionBoxStartPos != null);
+ var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1;
if (shouldHandleCursor)
{
@@ -1675,7 +1819,7 @@ class ChartEditorState extends HaxeUIState
var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x;
var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y;
- var overlapsSelectionBorder = overlapsGrid
+ var overlapsSelectionBorder:Bool = overlapsGrid
&& (cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2)
|| (cursorX % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2))
|| (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2));
@@ -1690,6 +1834,13 @@ class ChartEditorState extends HaxeUIState
{
selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
}
+ else
+ {
+ trace('Clicked outside grid, deselecting all items.');
+
+ // Deselect all items.
+ performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
+ }
}
if (gridPlayheadScrollAreaPressed)
@@ -1823,6 +1974,13 @@ class ChartEditorState extends HaxeUIState
else
{
// We made a selection box, but it didn't select anything.
+
+ if (!FlxG.keys.pressed.CONTROL)
+ {
+ trace('Clicked and dragged outside grid, deselecting all items.');
+ // Deselect all items.
+ performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
+ }
}
}
else
@@ -1836,8 +1994,26 @@ class ChartEditorState extends HaxeUIState
}
else
{
+ // Scroll the screen if the mouse is above or below the grid.
+ if (FlxG.mouse.screenY < MENU_BAR_HEIGHT)
+ {
+ // Scroll up.
+ var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY;
+ scrollPositionInPixels -= diff * 0.5; // Too fast!
+ trace('Scroll up: ' + diff);
+ moveSongToScrollPosition();
+ }
+ else if (FlxG.mouse.screenY > playbarHeadLayout.y)
+ {
+ // Scroll down.
+ var diff:Float = FlxG.mouse.screenY - playbarHeadLayout.y;
+ scrollPositionInPixels += diff * 0.5; // Too fast!
+ trace('Scroll down: ' + diff);
+ moveSongToScrollPosition();
+ }
+
// Render the selection box.
- var selectionRect = new FlxRect();
+ var selectionRect:FlxRect = new FlxRect();
selectionRect.x = Math.min(FlxG.mouse.screenX, selectionBoxStartPos.x);
selectionRect.y = Math.min(FlxG.mouse.screenY, selectionBoxStartPos.y);
selectionRect.width = Math.abs(FlxG.mouse.screenX - selectionBoxStartPos.x);
@@ -1921,7 +2097,14 @@ class ChartEditorState extends HaxeUIState
}
else
{
- // If we clicked and released outside the grid, do nothing.
+ // If we clicked and released outside the grid.
+
+ if (!FlxG.keys.pressed.CONTROL)
+ {
+ trace('Clicked outside grid, deselecting all items.');
+ // Deselect all items.
+ performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
+ }
}
}
}
@@ -2413,8 +2596,8 @@ class ChartEditorState extends HaxeUIState
playbarHeadLayout.x = 4;
playbarHeadLayout.y = FlxG.height - 48 - 8;
- var songPos = Conductor.songPosition;
- var songRemaining = songLengthInMs - songPos;
+ var songPos:Float = Conductor.songPosition;
+ var songRemaining:Float = Math.max(songLengthInMs - songPos, 0.0);
// Move the playhead to match the song position, if we aren't dragging it.
if (!playbarHeadDragging)
@@ -2559,7 +2742,7 @@ class ChartEditorState extends HaxeUIState
difficultySelectDirty = false;
// Manage the Select Difficulty tree view.
- var difficultyToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+ var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
if (difficultyToolbox == null) return;
var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree');
@@ -2568,30 +2751,28 @@ class ChartEditorState extends HaxeUIState
// Clear the tree view so we can rebuild it.
treeView.clearNodes();
- var treeSong = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: "haxeui-core/styles/default/haxeui_tiny.png"});
+ var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: 'haxeui-core/styles/default/haxeui_tiny.png'});
treeSong.expanded = true;
for (curVariation in availableVariations)
{
var variationMetadata:SongMetadata = songMetadata.get(curVariation);
- var treeVariation = treeSong.addNode(
+ var treeVariation:TreeViewNode = treeSong.addNode(
{
id: 'stv_variation_$curVariation',
- text: 'V: ${curVariation.toTitleCase()}',
- // icon: "haxeui-core/styles/default/haxeui_tiny.png"
+ text: 'V: ${curVariation.toTitleCase()}'
});
treeVariation.expanded = true;
- var difficultyList = variationMetadata.playData.difficulties;
+ var difficultyList:Array = variationMetadata.playData.difficulties;
for (difficulty in difficultyList)
{
- var treeDifficulty = treeVariation.addNode(
+ var _treeDifficulty:TreeViewNode = treeVariation.addNode(
{
id: 'stv_difficulty_${curVariation}_$difficulty',
- text: 'D: ${difficulty.toTitleCase()}',
- // icon: "haxeui-core/styles/default/haxeui_tiny.png"
+ text: 'D: ${difficulty.toTitleCase()}'
});
}
}
@@ -2604,25 +2785,71 @@ class ChartEditorState extends HaxeUIState
function handlePlayerPreviewToolbox():Void
{
// Manage the Select Difficulty tree view.
- var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
+ var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
if (charPreviewToolbox == null) return;
var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer');
if (charPlayer == null) return;
currentPlayerCharacterPlayer = charPlayer;
+
+ if (playerPreviewDirty)
+ {
+ playerPreviewDirty = false;
+
+ if (currentSongCharacterPlayer != charPlayer.charId)
+ {
+ healthIconBF.characterId = currentSongCharacterPlayer;
+
+ charPlayer.loadCharacter(currentSongCharacterPlayer);
+ charPlayer.characterType = CharacterType.BF;
+ charPlayer.flip = true;
+ charPlayer.targetScale = 0.5;
+
+ charPreviewToolbox.title = 'Player Preview - ${charPlayer.charName}';
+ }
+
+ if (charPreviewToolbox != null && !charPreviewToolbox.minimized)
+ {
+ charPreviewToolbox.width = charPlayer.width + 32;
+ charPreviewToolbox.height = charPlayer.height + 64;
+ }
+ }
}
function handleOpponentPreviewToolbox():Void
{
// Manage the Select Difficulty tree view.
- var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
+ var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
if (charPreviewToolbox == null) return;
var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer');
if (charPlayer == null) return;
currentOpponentCharacterPlayer = charPlayer;
+
+ if (opponentPreviewDirty)
+ {
+ opponentPreviewDirty = false;
+
+ if (currentSongCharacterOpponent != charPlayer.charId)
+ {
+ healthIconDad.characterId = currentSongCharacterOpponent;
+
+ charPlayer.loadCharacter(currentSongCharacterOpponent);
+ charPlayer.characterType = CharacterType.DAD;
+ charPlayer.flip = false;
+ charPlayer.targetScale = 0.5;
+
+ charPreviewToolbox.title = 'Opponent Preview - ${charPlayer.charName}';
+ }
+
+ if (charPreviewToolbox != null && !charPreviewToolbox.minimized)
+ {
+ charPreviewToolbox.width = charPlayer.width + 32;
+ charPreviewToolbox.height = charPlayer.height + 64;
+ }
+ }
}
public override function dispatchEvent(event:ScriptEvent):Void
@@ -2667,7 +2894,8 @@ class ChartEditorState extends HaxeUIState
if (treeView == null) return null;
- var result = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty', 'id');
+ var result:TreeViewNode = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty',
+ 'id');
if (result == null) return null;
@@ -2691,8 +2919,8 @@ class ChartEditorState extends HaxeUIState
switch (targetNode.data.id.split('_')[1])
{
case 'difficulty':
- var variation = targetNode.data.id.split('_')[2];
- var difficulty = targetNode.data.id.split('_')[3];
+ var variation:String = targetNode.data.id.split('_')[2];
+ var difficulty:String = targetNode.data.id.split('_')[3];
if (variation != null && difficulty != null)
{
@@ -2739,12 +2967,10 @@ class ChartEditorState extends HaxeUIState
{
notePreviewDirty = false;
- var PREVIEW_WIDTH:Int = GRID_SIZE * 2;
- var STEP_HEIGHT:Int = 1;
- var PREVIEW_HEIGHT:Int = Std.int(Conductor.getTimeInSteps(audioInstTrack.length) * STEP_HEIGHT);
-
- notePreviewBitmap = new BitmapData(PREVIEW_WIDTH, PREVIEW_HEIGHT, true);
- notePreviewBitmap.fillRect(new Rectangle(0, 0, PREVIEW_WIDTH, PREVIEW_HEIGHT), PREVIEW_BG_COLOR);
+ // TODO: Only update the notes that have changed.
+ notePreview.erase();
+ notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs));
+ notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs));
}
}
@@ -2773,7 +2999,7 @@ class ChartEditorState extends HaxeUIState
{
// Disable the Undo button.
undoButton.disabled = true;
- undoButton.text = "Undo";
+ undoButton.text = 'Undo';
}
else
{
@@ -2784,7 +3010,7 @@ class ChartEditorState extends HaxeUIState
}
else
{
- trace("undoButton is null");
+ trace('undoButton is null');
}
var redoButton:MenuItem = findComponent('menubarItemRedo', MenuItem);
@@ -2795,7 +3021,7 @@ class ChartEditorState extends HaxeUIState
{
// Disable the Redo button.
redoButton.disabled = true;
- redoButton.text = "Redo";
+ redoButton.text = 'Redo';
}
else
{
@@ -2806,7 +3032,7 @@ class ChartEditorState extends HaxeUIState
}
else
{
- trace("redoButton is null");
+ trace('redoButton is null');
}
}
}
@@ -2822,13 +3048,16 @@ class ChartEditorState extends HaxeUIState
{
// If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat!
- var oldStepTime = Conductor.currentStepTime;
- var oldSongPosition = Conductor.songPosition;
+ var oldStepTime:Float = Conductor.currentStepTime;
+ var oldSongPosition:Float = Conductor.songPosition;
Conductor.update(audioInstTrack.time);
handleHitsounds(oldSongPosition, Conductor.songPosition);
// Resync vocals.
- if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time;
- var diffStepTime = Conductor.currentStepTime - oldStepTime;
+ if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
+ {
+ audioVocalTrackGroup.time = audioInstTrack.time;
+ }
+ var diffStepTime:Float = Conductor.currentStepTime - oldStepTime;
// Move the playhead.
playheadPositionInPixels += diffStepTime * GRID_SIZE;
@@ -2838,12 +3067,14 @@ class ChartEditorState extends HaxeUIState
else
{
// Else, move the entire view.
- var oldSongPosition = Conductor.songPosition;
+ var oldSongPosition:Float = Conductor.songPosition;
Conductor.update(audioInstTrack.time);
handleHitsounds(oldSongPosition, Conductor.songPosition);
// Resync vocals.
- if (audioVocalTrackGroup != null
- && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time;
+ if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
+ {
+ audioVocalTrackGroup.time = audioInstTrack.time;
+ }
// We need time in fractional steps here to allow the song to actually play.
// Also account for a potentially offset playhead.
@@ -2909,7 +3140,6 @@ class ChartEditorState extends HaxeUIState
{
if (audioInstTrack != null) audioInstTrack.play();
if (audioVocalTrackGroup != null) audioVocalTrackGroup.play();
- if (audioVocalTrackGroup != null) audioVocalTrackGroup.play();
setComponentText('playbarPlay', '||');
}
@@ -2918,7 +3148,6 @@ class ChartEditorState extends HaxeUIState
{
if (audioInstTrack != null) audioInstTrack.pause();
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
- if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
setComponentText('playbarPlay', '>');
}
@@ -2940,23 +3169,42 @@ class ChartEditorState extends HaxeUIState
function handlePlayhead():Void
{
// Place notes at the playhead.
- // TODO: Add the ability to switch modes.
- if (true)
+ switch (currentLiveInputStyle)
{
- if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(0);
- if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(1);
- if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(2);
- if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(3);
- if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(4);
- if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(5);
- if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(6);
- if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(7);
+ case LiveInputStyle.WASD:
+ if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(0);
+ if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(1);
+ if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(2);
+ if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(3);
+
+ if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(4);
+ if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(5);
+ if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(6);
+ if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(7);
+ case LiveInputStyle.NumberKeys:
+ if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(0);
+ if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(1);
+ if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(2);
+ if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(3);
+
+ if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(4);
+ if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(5);
+ if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(6);
+ if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(7);
+ case LiveInputStyle.None:
+ // Do nothing.
}
}
function placeNoteAtPlayhead(column:Int):Void
{
- var gridSnappedPlayheadPos = scrollPositionInPixels - (scrollPositionInPixels % GRID_SIZE);
+ var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
+ var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant);
+ var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
+ var playheadPosMs:Float = playheadPosStep * Conductor.stepLengthMs * (16 / noteSnapQuant);
+
+ var newNoteData:SongNoteData = new SongNoteData(playheadPosMs, column, 0, selectedNoteKind);
+ performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
}
function set_scrollPositionInPixels(value:Float):Float
@@ -2967,7 +3215,7 @@ class ChartEditorState extends HaxeUIState
// but the playhead is in the middle, move the playhead up.
if (playheadPositionInPixels > 0)
{
- var amount = scrollPositionInPixels - value;
+ var amount:Float = scrollPositionInPixels - value;
playheadPositionInPixels -= amount;
}
@@ -2978,6 +3226,9 @@ class ChartEditorState extends HaxeUIState
if (value == scrollPositionInPixels) return value;
+ // Difference in pixels.
+ var diff:Float = value - scrollPositionInPixels;
+
this.scrollPositionInPixels = value;
// Move the grid sprite to the correct position.
@@ -2994,12 +3245,9 @@ class ChartEditorState extends HaxeUIState
renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y);
renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y);
- if (gridSpectrogram != null)
- {
- // Move the spectrogram to the correct position.
- gridSpectrogram.y = gridTiledSprite.y;
- gridSpectrogram.setPosition(0, 0);
- }
+
+ // Offset the selection box start position, if we are dragging.
+ if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff;
return this.scrollPositionInPixels;
}
@@ -3036,12 +3284,19 @@ class ChartEditorState extends HaxeUIState
*/
public function loadInstrumentalFromBytes(bytes:haxe.io.Bytes, fileName:String = null):Bool
{
+ if (bytes == null)
+ {
+ return false;
+ }
+
var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
audioInstTrack = FlxG.sound.load(openflSound, 1.0, false);
audioInstTrack.autoDestroy = false;
audioInstTrack.pause();
+ audioInstTrackData = bytes;
+
postLoadInstrumental();
return true;
@@ -3059,6 +3314,8 @@ class ChartEditorState extends HaxeUIState
{
audioInstTrack = instTrack;
+ audioInstTrackData = Assets.getBytes(path);
+
postLoadInstrumental();
return true;
}
@@ -3066,7 +3323,7 @@ class ChartEditorState extends HaxeUIState
return false;
}
- function postLoadInstrumental():Void
+ public function postLoadInstrumental():Void
{
// Prevent the time from skipping back to 0 when the song ends.
audioInstTrack.onComplete = function() {
@@ -3077,11 +3334,8 @@ class ChartEditorState extends HaxeUIState
songLengthInMs = audioInstTrack.length;
gridTiledSprite.height = songLengthInPixels;
- if (gridSpectrogram != null)
- {
- gridSpectrogram.setSound(audioInstTrack);
- gridSpectrogram.generateSection(0, songLengthInMs / 1000);
- }
+
+ buildSpectrogram(audioInstTrack);
scrollPositionInPixels = 0;
playheadPositionInPixels = 0;
@@ -3092,8 +3346,9 @@ class ChartEditorState extends HaxeUIState
* Loads a vocal track from an absolute file path.
* @param path The absolute path to the audio file.
* @param charKey The character to load the vocal track for.
+ * @return Success or failure.
*/
- public function loadVocalsFromPath(path:Path, charKey:String = null):Bool
+ public function loadVocalsFromPath(path:Path, charKey:String = 'default'):Bool
{
#if sys
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
@@ -3114,13 +3369,20 @@ class ChartEditorState extends HaxeUIState
/**
* Load a vocal track for a given song and character and add it to the voices group.
+ *
+ * @param path ID of the asset.
+ * @param charKey Character to load the vocal track for.
+ * @return Success or failure.
*/
- public function loadVocalsFromAsset(path:String, charKey:String = null):Bool
+ public function loadVocalsFromAsset(path:String, charKey:String = 'default'):Bool
{
var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
if (vocalTrack != null)
{
audioVocalTrackGroup.add(vocalTrack);
+
+ audioVocalTrackData.set(charKey, Assets.getBytes(path));
+
return true;
}
return false;
@@ -3131,10 +3393,11 @@ class ChartEditorState extends HaxeUIState
*/
public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = null):Bool
{
- var openflSound = new openfl.media.Sound();
+ var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
audioVocalTrackGroup.add(vocalTrack);
+ audioVocalTrackData.set(charKey, bytes);
return true;
}
@@ -3147,28 +3410,27 @@ class ChartEditorState extends HaxeUIState
if (song == null)
{
- // showNotification('Failed to load song.');
return;
}
// Load the song metadata.
var rawSongMetadata:Array = song.getRawMetadata();
- var songName:String = rawSongMetadata[0].songName;
-
- this.songMetadata = new Map();
+ var songMetadata:Map = [];
+ var songChartData:Map = [];
for (metadata in rawSongMetadata)
{
var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
- this.songMetadata.set(variation, Reflect.copy(metadata));
+ songMetadata.set(variation, Reflect.copy(metadata));
+ songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation));
}
- this.songChartData = new Map();
+ loadSong(songMetadata, songChartData);
- for (metadata in rawSongMetadata)
+ if (audioInstTrack != null)
{
- var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
- this.songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation));
+ audioInstTrack.stop();
+ audioInstTrack = null;
}
Conductor.forceBPM(null); // Disable the forced BPM.
@@ -3189,12 +3451,42 @@ class ChartEditorState extends HaxeUIState
NotificationManager.instance.addNotification(
{
title: 'Success',
- body: 'Loaded song ($songName)',
+ body: 'Loaded song (${rawSongMetadata[0].songName})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
}
+ /**
+ * Loads song metadata and chart data into the editor.
+ * @param newSongMetadata The song metadata to load.
+ * @param newSongChartData The song chart data to load.
+ */
+ public function loadSong(newSongMetadata:Map, newSongChartData:Map):Void
+ {
+ this.songMetadata = newSongMetadata;
+ this.songChartData = newSongChartData;
+
+ Conductor.forceBPM(null); // Disable the forced BPM.
+ Conductor.mapTimeChanges(currentSongMetadata.timeChanges);
+
+ difficultySelectDirty = true;
+ opponentPreviewDirty = true;
+ playerPreviewDirty = true;
+
+ // Remove instrumental and vocal tracks, they will be loaded next.
+ if (audioInstTrack != null)
+ {
+ audioInstTrack.stop();
+ audioInstTrack = null;
+ }
+ if (audioVocalTrackGroup != null)
+ {
+ audioVocalTrackGroup.stop();
+ audioVocalTrackGroup.clear();
+ }
+ }
+
/**
* When setting the scroll position, except when automatically scrolling during song playback,
* we need to update the conductor's current step time and the timestamp of the audio tracks.
@@ -3213,6 +3505,39 @@ class ChartEditorState extends HaxeUIState
noteDisplayDirty = true;
}
+ var currentScrollEase:VarTween;
+
+ function easeSongToScrollPosition(targetScrollPosition:Float):Void
+ {
+ if (currentScrollEase != null) cancelScrollEase(currentScrollEase);
+
+ currentScrollEase = FlxTween.tween(this, {scrollPositionInPixels: targetScrollPosition}, SCROLL_EASE_DURATION,
+ {
+ ease: FlxEase.quintInOut,
+ onUpdate: this.onScrollEaseUpdate,
+ onComplete: this.cancelScrollEase,
+ type: ONESHOT
+ });
+ }
+
+ function onScrollEaseUpdate(_:FlxTween):Void
+ {
+ moveSongToScrollPosition();
+ }
+
+ function cancelScrollEase(_:FlxTween):Void
+ {
+ if (currentScrollEase != null)
+ {
+ @:privateAccess
+ var targetScrollPosition:Float = currentScrollEase._properties.scrollPositionInPixels;
+
+ currentScrollEase.cancel();
+ currentScrollEase = null;
+ this.scrollPositionInPixels = targetScrollPosition;
+ }
+ }
+
/**
* Perform (or redo) a command, then add it to the undo stack.
*
@@ -3331,11 +3656,11 @@ class ChartEditorState extends HaxeUIState
*/
public function exportAllSongData(?force:Bool = false, ?tmp:Bool = false):Void
{
- var zipEntries = [];
+ var zipEntries:Array = [];
for (variation in availableVariations)
{
- var variationId = variation;
+ var variationId:String = variation;
if (variation == '' || variation == 'default' || variation == 'normal')
{
variationId = '';
@@ -3343,21 +3668,25 @@ class ChartEditorState extends HaxeUIState
if (variationId == '')
{
- var variationMetadata = songMetadata.get(variation);
+ var variationMetadata:SongMetadata = songMetadata.get(variation);
zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata)));
- var variationChart = songChartData.get(variation);
+ var variationChart:SongChartData = songChartData.get(variation);
zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart)));
}
else
{
- var variationMetadata = songMetadata.get(variation);
+ var variationMetadata:SongMetadata = songMetadata.get(variation);
zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json', SerializerUtil.toJSON(variationMetadata)));
- var variationChart = songChartData.get(variation);
+ var variationChart:SongChartData = songChartData.get(variation);
zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart)));
}
}
- // TODO: Add audio files to the ZIP.
+ zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData));
+ for (charId in audioVocalTrackData.keys())
+ {
+ zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', audioVocalTrackData.get(charId)));
+ }
trace('Exporting ${zipEntries.length} files to ZIP...');
@@ -3390,3 +3719,10 @@ class ChartEditorState extends HaxeUIState
FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '$currentSongId-chart.zip');
}
}
+
+enum LiveInputStyle
+{
+ None;
+ NumberKeys;
+ WASD;
+}
diff --git a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx
index 7bdf366bf..40c797169 100644
--- a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx
@@ -26,7 +26,7 @@ class ChartEditorThemeHandler
// An enum of typedefs or something?
// ================================
static final BACKGROUND_COLOR_LIGHT:FlxColor = 0xFF673AB7;
- static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF673AB7;
+ static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF361E60;
// Color 1 of the grid pattern. Alternates with Color 2.
static final GRID_COLOR_1_LIGHT:FlxColor = 0xFFE7E6E6;
@@ -43,13 +43,11 @@ class ChartEditorThemeHandler
// Vertical divider between characters.
static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
- // static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = 2;
static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
// Horizontal divider between measures.
static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
- // static final GRID_MEASURE_DIVIDER_WIDTH:Float = 2;
static final GRID_MEASURE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
// Border on the square highlighting selected notes.
@@ -66,6 +64,12 @@ class ChartEditorThemeHandler
static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011;
static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231;
+ static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1;
+
+ /**
+ * When the theme is changed, this function updates all of the UI elements to match the new theme.
+ * @param state The ChartEditorState to update.
+ */
public static function updateTheme(state:ChartEditorState):Void
{
updateBackground(state);
@@ -73,6 +77,10 @@ class ChartEditorThemeHandler
updateSelectionSquare(state);
}
+ /**
+ * Updates the tint of the background sprite to match the current theme.
+ * @param state The ChartEditorState to update.
+ */
static function updateBackground(state:ChartEditorState):Void
{
state.menuBG.color = switch (state.currentTheme)
@@ -85,7 +93,7 @@ class ChartEditorThemeHandler
/**
* Builds the checkerboard background image of the chart editor, and adds dividing lines to it.
- * @param dark Whether to draw the grid in a dark color instead of a light one.
+ * @param state The ChartEditorState to update.
*/
static function updateGridBitmap(state:ChartEditorState):Void
{
@@ -107,8 +115,8 @@ class ChartEditorThemeHandler
// 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
// This gets reused to fill the screen.
- var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1));
- var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * (Conductor.stepsPerMeasure));
+ var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT);
+ var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure);
state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2);
// Selection borders
@@ -143,7 +151,7 @@ class ChartEditorThemeHandler
selectionBorderColor);
// Selection borders across the middle.
- for (i in 1...(ChartEditorState.STRUMLINE_SIZE * 2 + 1))
+ for (i in 1...TOTAL_COLUMN_COUNT)
{
state.gridBitmap.fillRect(new Rectangle((ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0,
ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height),
@@ -167,7 +175,7 @@ class ChartEditorThemeHandler
// Divider at top
state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
// Divider at bottom
- var dividerLineBY = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2);
+ var dividerLineBY:Float = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2);
state.gridBitmap.fillRect(new Rectangle(0, dividerLineBY, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
// Draw dividers between the strumlines.
@@ -180,10 +188,10 @@ class ChartEditorThemeHandler
};
// Divider at 1 * (Strumline Size)
- var dividerLineAX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
+ var dividerLineAX:Float = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
state.gridBitmap.fillRect(new Rectangle(dividerLineAX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor);
// Divider at 2 * (Strumline Size)
- var dividerLineBX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
+ var dividerLineBX:Float = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
state.gridBitmap.fillRect(new Rectangle(dividerLineBX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor);
if (state.gridTiledSprite != null)
diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
index 5a903481e..5cace2ff6 100644
--- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
@@ -1,6 +1,5 @@
package funkin.ui.debug.charting;
-import haxe.ui.data.ArrayDataSource;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.event.SongEvent;
import funkin.play.event.SongEventData;
@@ -12,15 +11,17 @@ import haxe.ui.components.CheckBox;
import haxe.ui.components.DropDown;
import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper;
-import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
import haxe.ui.components.TextField;
-import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.Box;
-import haxe.ui.containers.Frame;
import haxe.ui.containers.Grid;
import haxe.ui.containers.Group;
+import haxe.ui.containers.VBox;
+import haxe.ui.containers.dialogs.CollapsibleDialog;
+import haxe.ui.containers.dialogs.Dialog.DialogButton;
+import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import haxe.ui.core.Component;
+import haxe.ui.data.ArrayDataSource;
import haxe.ui.events.UIEvent;
/**
@@ -32,18 +33,26 @@ enum ChartEditorToolMode
Place;
}
+/**
+ * Static functions which handle building themed UI elements for a provided ChartEditorState.
+ */
class ChartEditorToolboxHandler
{
public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void
{
- if (shown) showToolbox(state, id);
+ if (shown)
+ {
+ showToolbox(state, id);
+ }
else
+ {
hideToolbox(state, id);
+ }
}
- public static function showToolbox(state:ChartEditorState, id:String)
+ public static function showToolbox(state:ChartEditorState, id:String):Void
{
- var toolbox:Dialog = state.activeToolboxes.get(id);
+ var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
if (toolbox == null) toolbox = initToolbox(state, id);
@@ -59,7 +68,7 @@ class ChartEditorToolboxHandler
public static function hideToolbox(state:ChartEditorState, id:String):Void
{
- var toolbox:Dialog = state.activeToolboxes.get(id);
+ var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
if (toolbox == null) toolbox = initToolbox(state, id);
@@ -73,13 +82,27 @@ class ChartEditorToolboxHandler
}
}
- public static function minimizeToolbox(state:ChartEditorState, id:String):Void {}
-
- public static function maximizeToolbox(state:ChartEditorState, id:String):Void {}
-
- public static function initToolbox(state:ChartEditorState, id:String):Dialog
+ public static function minimizeToolbox(state:ChartEditorState, id:String):Void
{
- var toolbox:Dialog = null;
+ var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
+
+ if (toolbox == null) return;
+
+ toolbox.minimized = true;
+ }
+
+ public static function maximizeToolbox(state:ChartEditorState, id:String):Void
+ {
+ var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
+
+ if (toolbox == null) return;
+
+ toolbox.minimized = false;
+ }
+
+ public static function initToolbox(state:ChartEditorState, id:String):CollapsibleDialog
+ {
+ var toolbox:CollapsibleDialog = null;
switch (id)
{
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
@@ -95,9 +118,9 @@ class ChartEditorToolboxHandler
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
toolbox = buildToolboxCharactersLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
- toolbox = buildToolboxPlayerPreviewLayout(state);
+ toolbox = null; // buildToolboxPlayerPreviewLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
- toolbox = buildToolboxOpponentPreviewLayout(state);
+ toolbox = null; // buildToolboxOpponentPreviewLayout(state);
default:
// This happens if you try to load an unknown layout.
trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id');
@@ -114,9 +137,15 @@ class ChartEditorToolboxHandler
return toolbox;
}
- public static function getToolbox(state:ChartEditorState, id:String):Dialog
+ /**
+ * Retrieve a toolbox by its layout's asset ID.
+ * @param state The ChartEditorState instance.
+ * @param id The asset ID of the toolbox layout.
+ * @return The toolbox.
+ */
+ public static function getToolbox(state:ChartEditorState, id:String):CollapsibleDialog
{
- var toolbox:Dialog = state.activeToolboxes.get(id);
+ var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
// Initialize the toolbox without showing it.
if (toolbox == null) toolbox = initToolbox(state, id);
@@ -124,9 +153,9 @@ class ChartEditorToolboxHandler
return toolbox;
}
- static function buildToolboxToolsLayout(state:ChartEditorState):Dialog
+ static function buildToolboxToolsLayout(state:ChartEditorState):CollapsibleDialog
{
- var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT);
+ var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT);
if (toolbox == null) return null;
@@ -134,15 +163,15 @@ class ChartEditorToolboxHandler
toolbox.x = 50;
toolbox.y = 50;
- toolbox.onDialogClosed = (event:DialogEvent) -> {
+ toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxTools', false);
}
- var toolsGroup:Group = toolbox.findComponent("toolboxToolsGroup", Group);
+ var toolsGroup:Group = toolbox.findComponent('toolboxToolsGroup', Group);
if (toolsGroup == null) return null;
- toolsGroup.onChange = (event:UIEvent) -> {
+ toolsGroup.onChange = function(event:UIEvent) {
switch (event.target.id)
{
case 'toolboxToolsGroupSelect':
@@ -157,9 +186,9 @@ class ChartEditorToolboxHandler
return toolbox;
}
- static function buildToolboxNoteDataLayout(state:ChartEditorState):Dialog
+ static function buildToolboxNoteDataLayout(state:ChartEditorState):CollapsibleDialog
{
- var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
+ var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
if (toolbox == null) return null;
@@ -167,16 +196,16 @@ class ChartEditorToolboxHandler
toolbox.x = 75;
toolbox.y = 100;
- toolbox.onDialogClosed = (event:DialogEvent) -> {
+ toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false);
}
- var toolboxNotesNoteKind:DropDown = toolbox.findComponent("toolboxNotesNoteKind", DropDown);
- var toolboxNotesCustomKindLabel:Label = toolbox.findComponent("toolboxNotesCustomKindLabel", Label);
- var toolboxNotesCustomKind:TextField = toolbox.findComponent("toolboxNotesCustomKind", TextField);
+ var toolboxNotesNoteKind:DropDown = toolbox.findComponent('toolboxNotesNoteKind', DropDown);
+ var toolboxNotesCustomKindLabel:Label = toolbox.findComponent('toolboxNotesCustomKindLabel', Label);
+ var toolboxNotesCustomKind:TextField = toolbox.findComponent('toolboxNotesCustomKind', TextField);
- toolboxNotesNoteKind.onChange = (event:UIEvent) -> {
- var isCustom = (event.data.id == '~CUSTOM~');
+ toolboxNotesNoteKind.onChange = function(event:UIEvent) {
+ var isCustom:Bool = (event.data.id == '~CUSTOM~');
if (isCustom)
{
@@ -194,16 +223,16 @@ class ChartEditorToolboxHandler
}
}
- toolboxNotesCustomKind.onChange = (event:UIEvent) -> {
+ toolboxNotesCustomKind.onChange = function(event:UIEvent) {
state.selectedNoteKind = toolboxNotesCustomKind.text;
}
return toolbox;
}
- static function buildToolboxEventDataLayout(state:ChartEditorState):Dialog
+ static function buildToolboxEventDataLayout(state:ChartEditorState):CollapsibleDialog
{
- var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
+ var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
if (toolbox == null) return null;
@@ -211,12 +240,12 @@ class ChartEditorToolboxHandler
toolbox.x = 100;
toolbox.y = 150;
- toolbox.onDialogClosed = (event:DialogEvent) -> {
+ toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false);
}
- var toolboxEventsEventKind:DropDown = toolbox.findComponent("toolboxEventsEventKind", DropDown);
- var toolboxEventsDataGrid:Grid = toolbox.findComponent("toolboxEventsDataGrid", Grid);
+ var toolboxEventsEventKind:DropDown = toolbox.findComponent('toolboxEventsEventKind', DropDown);
+ var toolboxEventsDataGrid:Grid = toolbox.findComponent('toolboxEventsDataGrid', Grid);
toolboxEventsEventKind.dataSource = new ArrayDataSource();
@@ -227,7 +256,7 @@ class ChartEditorToolboxHandler
toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
}
- toolboxEventsEventKind.onChange = (event:UIEvent) -> {
+ toolboxEventsEventKind.onChange = function(event:UIEvent) {
var eventType:String = event.data.value;
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
@@ -281,9 +310,9 @@ class ChartEditorToolboxHandler
numberStepper.value = field.defaultValue;
input = numberStepper;
case BOOL:
- var checkBox = new CheckBox();
+ var checkBox:CheckBox = new CheckBox();
checkBox.id = field.name;
- checkBox.selected = field.defaultValue == true;
+ checkBox.selected = field.defaultValue;
input = checkBox;
case ENUM:
var dropDown:DropDown = new DropDown();
@@ -293,7 +322,7 @@ class ChartEditorToolboxHandler
// Add entries to the dropdown.
for (optionName in field.keys.keys())
{
- var optionValue = field.keys.get(optionName);
+ var optionValue:String = field.keys.get(optionName);
trace('$optionName : $optionValue');
dropDown.dataSource.add({value: optionValue, text: optionName});
}
@@ -314,7 +343,7 @@ class ChartEditorToolboxHandler
target.addComponent(input);
- input.onChange = (event:UIEvent) -> {
+ input.onChange = function(event:UIEvent) {
trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${event.target.value}');
if (event.target.value == null) state.selectedEventData.remove(event.target.id);
@@ -324,9 +353,9 @@ class ChartEditorToolboxHandler
}
}
- static function buildToolboxDifficultyLayout(state:ChartEditorState):Dialog
+ static function buildToolboxDifficultyLayout(state:ChartEditorState):CollapsibleDialog
{
- var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+ var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
if (toolbox == null) return null;
@@ -334,36 +363,36 @@ class ChartEditorToolboxHandler
toolbox.x = 125;
toolbox.y = 200;
- toolbox.onDialogClosed = (event:DialogEvent) -> {
+ toolbox.onDialogClosed = function(event:UIEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
}
- var difficultyToolboxSaveMetadata:Button = toolbox.findComponent("difficultyToolboxSaveMetadata", Button);
- var difficultyToolboxSaveChart:Button = toolbox.findComponent("difficultyToolboxSaveChart", Button);
- var difficultyToolboxSaveAll:Button = toolbox.findComponent("difficultyToolboxSaveAll", Button);
- var difficultyToolboxLoadMetadata:Button = toolbox.findComponent("difficultyToolboxLoadMetadata", Button);
- var difficultyToolboxLoadChart:Button = toolbox.findComponent("difficultyToolboxLoadChart", Button);
+ var difficultyToolboxSaveMetadata:Button = toolbox.findComponent('difficultyToolboxSaveMetadata', Button);
+ var difficultyToolboxSaveChart:Button = toolbox.findComponent('difficultyToolboxSaveChart', Button);
+ var difficultyToolboxSaveAll:Button = toolbox.findComponent('difficultyToolboxSaveAll', Button);
+ var difficultyToolboxLoadMetadata:Button = toolbox.findComponent('difficultyToolboxLoadMetadata', Button);
+ var difficultyToolboxLoadChart:Button = toolbox.findComponent('difficultyToolboxLoadChart', Button);
- difficultyToolboxSaveMetadata.onClick = (event:UIEvent) -> {
- SongSerializer.exportSongMetadata(state.currentSongMetadata);
+ difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) {
+ SongSerializer.exportSongMetadata(state.currentSongMetadata, state.currentSongId);
};
- difficultyToolboxSaveChart.onClick = (event:UIEvent) -> {
- SongSerializer.exportSongChartData(state.currentSongChartData);
+ difficultyToolboxSaveChart.onClick = function(event:UIEvent) {
+ SongSerializer.exportSongChartData(state.currentSongChartData, state.currentSongId);
};
- difficultyToolboxSaveAll.onClick = (event:UIEvent) -> {
+ difficultyToolboxSaveAll.onClick = function(event:UIEvent) {
state.exportAllSongData();
};
- difficultyToolboxLoadMetadata.onClick = (event:UIEvent) -> {
+ difficultyToolboxLoadMetadata.onClick = function(event:UIEvent) {
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata) {
state.currentSongMetadata = songMetadata;
});
};
- difficultyToolboxLoadChart.onClick = (event:UIEvent) -> {
+ difficultyToolboxLoadChart.onClick = function(event:UIEvent) {
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData) {
state.currentSongChartData = songChartData;
@@ -376,9 +405,9 @@ class ChartEditorToolboxHandler
return toolbox;
}
- static function buildToolboxMetadataLayout(state:ChartEditorState):Dialog
+ static function buildToolboxMetadataLayout(state:ChartEditorState):CollapsibleDialog
{
- var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
+ var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
if (toolbox == null) return null;
@@ -386,13 +415,13 @@ class ChartEditorToolboxHandler
toolbox.x = 150;
toolbox.y = 250;
- toolbox.onDialogClosed = (event:DialogEvent) -> {
+ toolbox.onDialogClosed = function(event:UIEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false);
}
var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField);
- inputSongName.onChange = (event:UIEvent) -> {
- var valid = event.target.text != null && event.target.text != "";
+ inputSongName.onChange = function(event:UIEvent) {
+ var valid:Bool = event.target.text != null && event.target.text != '';
if (valid)
{
@@ -404,10 +433,11 @@ class ChartEditorToolboxHandler
state.currentSongMetadata.songName = null;
}
};
+ inputSongName.value = state.currentSongMetadata.songName;
var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField);
- inputSongArtist.onChange = (event:UIEvent) -> {
- var valid = event.target.text != null && event.target.text != "";
+ inputSongArtist.onChange = function(event:UIEvent) {
+ var valid:Bool = event.target.text != null && event.target.text != '';
if (valid)
{
@@ -419,28 +449,31 @@ class ChartEditorToolboxHandler
state.currentSongMetadata.artist = null;
}
};
+ inputSongArtist.value = state.currentSongMetadata.artist;
var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown);
- inputStage.onChange = (event:UIEvent) -> {
- var valid = event.data != null && event.data.id != null;
+ inputStage.onChange = function(event:UIEvent) {
+ var valid:Bool = event.data != null && event.data.id != null;
if (valid)
{
state.currentSongMetadata.playData.stage = event.data.id;
}
};
+ inputStage.value = state.currentSongMetadata.playData.stage;
var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
- inputNoteSkin.onChange = (event:UIEvent) -> {
+ inputNoteSkin.onChange = function(event:UIEvent) {
if (event.data.id == null) return;
state.currentSongMetadata.playData.noteSkin = event.data.id;
};
+ inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin;
var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper);
- inputBPM.onChange = (event:UIEvent) -> {
+ inputBPM.onChange = function(event:UIEvent) {
if (event.value == null || event.value <= 0) return;
- var timeChanges = state.currentSongMetadata.timeChanges;
+ var timeChanges:Array = state.currentSongMetadata.timeChanges;
if (timeChanges == null || timeChanges.length == 0)
{
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
@@ -454,28 +487,30 @@ class ChartEditorToolboxHandler
state.currentSongMetadata.timeChanges = timeChanges;
};
+ inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm;
var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
- inputScrollSpeed.onChange = (event:UIEvent) -> {
- var valid = event.target.value != null && event.target.value > 0;
+ inputScrollSpeed.onChange = function(event:UIEvent) {
+ var valid:Bool = event.target.value != null && event.target.value > 0;
if (valid)
{
inputScrollSpeed.removeClass('invalid-value');
- state.currentSongChartData.scrollSpeed = event.target.value;
+ state.currentSongChartScrollSpeed = event.target.value;
}
else
{
- state.currentSongChartData.scrollSpeed = null;
+ state.currentSongChartScrollSpeed = 1.0;
}
};
+ inputScrollSpeed.value = state.currentSongChartData.scrollSpeed;
return toolbox;
}
- static function buildToolboxCharactersLayout(state:ChartEditorState):Dialog
+ static function buildToolboxCharactersLayout(state:ChartEditorState):CollapsibleDialog
{
- var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
+ var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
if (toolbox == null) return null;
@@ -483,16 +518,16 @@ class ChartEditorToolboxHandler
toolbox.x = 175;
toolbox.y = 300;
- toolbox.onDialogClosed = (event:DialogEvent) -> {
+ toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false);
}
return toolbox;
}
- static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Dialog
+ static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):CollapsibleDialog
{
- var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
+ var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
if (toolbox == null) return null;
@@ -500,23 +535,23 @@ class ChartEditorToolboxHandler
toolbox.x = 200;
toolbox.y = 350;
- toolbox.onDialogClosed = (event:DialogEvent) -> {
+ toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false);
}
var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer');
// TODO: We need to implement character swapping in ChartEditorState.
charPlayer.loadCharacter('bf');
- // charPlayer.setScale(0.5);
- charPlayer.setCharacterType(CharacterType.BF);
+ charPlayer.characterType = CharacterType.BF;
charPlayer.flip = true;
+ charPlayer.targetScale = 0.5;
return toolbox;
}
- static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Dialog
+ static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):CollapsibleDialog
{
- var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
+ var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
if (toolbox == null) return null;
@@ -531,9 +566,9 @@ class ChartEditorToolboxHandler
var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer');
// TODO: We need to implement character swapping in ChartEditorState.
charPlayer.loadCharacter('dad');
- // charPlayer.setScale(0.5);
- charPlayer.setCharacterType(CharacterType.DAD);
+ charPlayer.characterType = CharacterType.DAD;
charPlayer.flip = false;
+ charPlayer.targetScale = 0.5;
return toolbox;
}
diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx
index c6beca123..0e6981535 100644
--- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx
+++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx
@@ -1,25 +1,19 @@
package funkin.ui.haxeui.components;
-import flixel.FlxSprite;
-import flixel.graphics.frames.FlxAtlasFrames;
-import flixel.graphics.frames.FlxFramesCollection;
-import flixel.math.FlxRect;
-import funkin.modding.events.ScriptEvent;
-import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
+import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent;
+import funkin.modding.events.ScriptEvent.NoteScriptEvent;
+import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
+import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
+import haxe.ui.core.IDataComponent;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
import haxe.ui.containers.Box;
import haxe.ui.core.Component;
-import haxe.ui.core.IDataComponent;
-import haxe.ui.data.DataSource;
import haxe.ui.events.AnimationEvent;
-import haxe.ui.events.UIEvent;
import haxe.ui.geom.Size;
import haxe.ui.layouts.DefaultLayout;
-import haxe.ui.styles.Style;
-import openfl.Assets;
-private typedef AnimationInfo =
+typedef AnimationInfo =
{
var name:String;
var prefix:String;
@@ -29,6 +23,10 @@ private typedef AnimationInfo =
var flipY:Null; // default false
}
+/**
+ * A variant of SparrowPlayer which loads a BaseCharacter instead.
+ * This allows it to play appropriate animations based on song events.
+ */
@:composite(Layout)
class CharacterPlayer extends Box
{
@@ -37,7 +35,7 @@ class CharacterPlayer extends Box
public function new(?defaultToBf:Bool = true)
{
super();
- this._overrideSkipTransformChildren = false;
+ _overrideSkipTransformChildren = false;
if (defaultToBf)
{
@@ -45,52 +43,39 @@ class CharacterPlayer extends Box
}
}
- var _charId:String;
-
public var charId(get, set):String;
function get_charId():String
{
- return _charId;
+ return character.characterId;
}
function set_charId(value:String):String
{
- _charId = value;
- loadCharacter(_charId);
+ loadCharacter(value);
return value;
}
- var _redispatchLoaded:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure
- var _redispatchStart:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure
+ public var charName(get, null):String;
- public override function onReady()
+ function get_charName():String
{
- super.onReady();
-
- invalidateComponentLayout();
-
- if (_redispatchLoaded)
- {
- _redispatchLoaded = false;
- dispatch(new AnimationEvent(AnimationEvent.LOADED));
- }
-
- if (_redispatchStart)
- {
- _redispatchStart = false;
- dispatch(new AnimationEvent(AnimationEvent.START));
- }
-
- parentComponent._overrideSkipTransformChildren = false;
+ return character.characterName;
}
- public function loadCharacter(id:String)
+ // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... is it smart to "collect and redispatch"? Not sure
+ var _redispatchLoaded:Bool = false;
+ // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... is it smart to "collect and redispatch"? Not sure
+ var _redispatchStart:Bool = false;
+ var _characterLoaded:Bool = false;
+
+ /**
+ * Loads a character by ID.
+ * @param id The ID of the character to load.
+ */
+ public function loadCharacter(id:String):Void
{
- if (id == null)
- {
- return;
- }
+ if (id == null) return;
if (character != null)
{
@@ -99,32 +84,24 @@ class CharacterPlayer extends Box
character = null;
}
- var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id);
-
- if (newCharacter == null)
- {
- return;
- }
+ // Prevent script issues by fetching with debug=true.
+ var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id, true);
+ if (newCharacter == null) return; // Fail if character doesn't exist.
+ // Assign character.
character = newCharacter;
- if (_characterType != null)
- {
- character.characterType = _characterType;
- }
- if (flip)
- {
- character.flipX = !character.flipX;
- }
- character.scale.x *= _scale;
- character.scale.y *= _scale;
+ // Set character properties.
+ if (characterType != null) character.characterType = characterType;
+ if (flip) character.flipX = !character.flipX;
+ if (targetScale != 1.0) character.setScale(targetScale);
- character.animation.callback = function(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1) {
+ character.animation.callback = function(name:String = '', frameNumber:Int = -1, frameIndex:Int = -1) {
@:privateAccess
character.onAnimationFrame(name, frameNumber, frameIndex);
dispatch(new AnimationEvent(AnimationEvent.FRAME));
};
- character.animation.finishCallback = function(name:String = "") {
+ character.animation.finishCallback = function(name:String = '') {
@:privateAccess
character.onAnimationFinished(name);
dispatch(new AnimationEvent(AnimationEvent.END));
@@ -143,28 +120,15 @@ class CharacterPlayer extends Box
}
}
- override function repositionChildren()
+ /**
+ * The character type (such as BF, Dad, GF, etc).
+ */
+ public var characterType(default, set):CharacterType;
+
+ function set_characterType(value:CharacterType):CharacterType
{
- super.repositionChildren();
-
- @:privateAccess
- var animOffsets = character.animOffsets;
-
- character.x = this.screenX + ((this.width / 2) - (character.frameWidth / 2));
- character.x -= animOffsets[0];
- character.y = this.screenY + ((this.height / 2) - (character.frameHeight / 2));
- character.y -= animOffsets[1];
- }
-
- var _characterType:CharacterType;
-
- public function setCharacterType(value:CharacterType)
- {
- _characterType = value;
- if (character != null)
- {
- character.characterType = value;
- }
+ if (character != null) character.characterType = value;
+ return characterType = value;
}
public var flip(default, set):Bool;
@@ -181,89 +145,133 @@ class CharacterPlayer extends Box
return flip = value;
}
- var _scale:Float = 1.0;
+ public var targetScale(default, set):Float = 1.0;
- public function setScale(value)
+ function set_targetScale(value:Float):Float
{
- _scale = value;
+ if (value == targetScale) return value;
+
if (character != null)
{
- character.scale.x *= _scale;
- character.scale.y *= _scale;
+ character.setScale(value);
}
+
+ return targetScale = value;
}
- public function onUpdate(event:UpdateScriptEvent)
+ function onFrame(name:String, frameNumber:Int, frameIndex:Int):Void
+ {
+ dispatch(new AnimationEvent(AnimationEvent.FRAME));
+ }
+
+ function onFinish(name:String):Void
+ {
+ dispatch(new AnimationEvent(AnimationEvent.END));
+ }
+
+ override function repositionChildren():Void
+ {
+ super.repositionChildren();
+ character.x = this.screenX;
+ character.y = this.screenY;
+
+ // Apply animation offsets, so the character is positioned correctly based on the animation.
+ @:privateAccess var animOffsets:Array = character.animOffsets;
+
+ character.x -= animOffsets[0] * targetScale * (flip ? -1 : 1);
+ character.y -= animOffsets[1] * targetScale;
+ }
+
+ /**
+ * Called when an update event is hit in the song.
+ * Used to play character animations.
+ * @param event The event.
+ */
+ public function onUpdate(event:UpdateScriptEvent):Void
{
if (character != null) character.onUpdate(event);
}
+ /**
+ * Called when an beat is hit in the song
+ * Used to play character animations.
+ * @param event The event.
+ */
public function onBeatHit(event:SongTimeScriptEvent):Void
{
if (character != null) character.onBeatHit(event);
-
- this.repositionChildren();
}
+ /**
+ * Called when a step is hit in the song
+ * Used to play character animations.
+ * @param event The event.
+ */
public function onStepHit(event:SongTimeScriptEvent):Void
{
if (character != null) character.onStepHit(event);
}
+ /**
+ * Called when a note is hit in the song
+ * Used to play character animations.
+ * @param event The event.
+ */
public function onNoteHit(event:NoteScriptEvent):Void
{
if (character != null) character.onNoteHit(event);
-
- this.repositionChildren();
}
+ /**
+ * Called when a note is missed in the song
+ * Used to play character animations.
+ * @param event The event.
+ */
public function onNoteMiss(event:NoteScriptEvent):Void
{
if (character != null) character.onNoteMiss(event);
-
- this.repositionChildren();
}
+ /**
+ * Called when a key is pressed but no note is hit in the song
+ * Used to play character animations.
+ * @param event The event.
+ */
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void
{
if (character != null) character.onNoteGhostMiss(event);
-
- this.repositionChildren();
}
}
@:access(funkin.ui.haxeui.components.CharacterPlayer)
private class Layout extends DefaultLayout
{
- public override function repositionChildren()
+ public override function resizeChildren():Void
{
- var player = cast(_component, CharacterPlayer);
- var sprite:BaseCharacter = player.character;
- if (sprite == null)
+ super.resizeChildren();
+
+ var player:CharacterPlayer = cast(_component, CharacterPlayer);
+ var character:BaseCharacter = player.character;
+ if (character == null)
{
- return super.repositionChildren();
+ return super.resizeChildren();
}
- @:privateAccess
- var animOffsets = sprite.animOffsets;
-
- sprite.x = _component.screenLeft + ((_component.width / 2) - (sprite.frameWidth / 2));
- sprite.x += animOffsets[0];
- sprite.y = _component.screenTop + ((_component.height / 2) - (sprite.frameHeight / 2));
- sprite.y += animOffsets[1];
+ character.cornerPosition.set(0, 0);
+ // character.setGraphicSize(Std.int(innerWidth), Std.int(innerHeight));
}
public override function calcAutoSize(exclusions:Array = null):Size
{
- var player = cast(_component, CharacterPlayer);
- var sprite = player.character;
- if (sprite == null)
+ var player:CharacterPlayer = cast(_component, CharacterPlayer);
+ var character:BaseCharacter = player.character;
+ if (character == null)
{
return super.calcAutoSize(exclusions);
}
- var size = new Size();
- size.width = sprite.frameWidth + paddingLeft + paddingRight;
- size.height = sprite.frameHeight + paddingTop + paddingBottom;
+ var size:Size = new Size();
+ size.width = character.width + paddingLeft + paddingRight;
+ size.height = character.height + paddingTop + paddingBottom;
return size;
}
}
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index a0063741b..2d38059db 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -173,9 +173,33 @@ class Constants
public static final MS_PER_SEC:Float = 1000;
/**
- * The number of steps in one beat.
- *
- * Each beat represents ONE quarter note, so one step is one sixteenth note!
+ * The number of microseconds in a millisecond.
+ */
+ public static final US_PER_MS:Int = 1000;
+
+ /**
+ * The number of microseconds in a second.
+ */
+ public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC;
+
+ /**
+ * The number of nanoseconds in a microsecond.
+ */
+ public static final NS_PER_US:Int = 1000;
+
+ /**
+ * The number of nanoseconds in a millisecond.
+ */
+ public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS;
+
+ /**
+ * The number of nanoseconds in a second.
+ */
+ public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC;
+
+ /**
+ * Number of steps in a beat.
+ * One step is one 16th note and one beat is one quarter note.
*/
public static final STEPS_PER_BEAT:Int = 4;
@@ -284,6 +308,13 @@ class Constants
*/
public static final COUNTDOWN_VOLUME:Float = 0.6;
+ /**
+ * The horizontal offset of the strumline from the left edge of the screen.
+ */
public static final STRUMLINE_X_OFFSET:Float = 48;
+
+ /**
+ * The vertical offset of the strumline from the top edge of the screen.
+ */
public static final STRUMLINE_Y_OFFSET:Float = 24;
}
diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx
index 57dc7b12e..3494e620b 100644
--- a/source/funkin/util/FileUtil.hx
+++ b/source/funkin/util/FileUtil.hx
@@ -26,9 +26,9 @@ class FileUtil
?dialogTitle:String):Bool
{
#if desktop
- var filter = convertTypeFilter(typeFilter);
+ var filter:String = convertTypeFilter(typeFilter);
- var fileDialog = new FileDialog();
+ var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelect.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
@@ -54,9 +54,9 @@ class FileUtil
?dialogTitle:String):Bool
{
#if desktop
- var filter = convertTypeFilter(typeFilter);
+ var filter:String = convertTypeFilter(typeFilter);
- var fileDialog = new FileDialog();
+ var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelect.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
@@ -81,9 +81,9 @@ class FileUtil
?dialogTitle:String):Bool
{
#if desktop
- var filter = convertTypeFilter(typeFilter);
+ var filter:String = convertTypeFilter(typeFilter);
- var fileDialog = new FileDialog();
+ var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelectMultiple.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
@@ -109,9 +109,9 @@ class FileUtil
?dialogTitle:String):Bool
{
#if desktop
- var filter = convertTypeFilter(typeFilter);
+ var filter:String = convertTypeFilter(typeFilter);
- var fileDialog = new FileDialog();
+ var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelect.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
@@ -136,29 +136,29 @@ class FileUtil
public static function openFile(?typeFilter:Array, ?onOpen:Bytes->Void, ?onCancel:Void->Void, ?defaultPath:String, ?dialogTitle:String):Bool
{
#if desktop
- var filter = convertTypeFilter(typeFilter);
+ var filter:String = convertTypeFilter(typeFilter);
- var fileDialog = new FileDialog();
+ var fileDialog:FileDialog = new FileDialog();
if (onOpen != null) fileDialog.onOpen.add(onOpen);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.open(filter, defaultPath, dialogTitle);
return true;
#elseif html5
- var onFileLoaded = function(event) {
+ var onFileLoaded:Event->Void = function(event) {
var loadedFileRef:FileReference = event.target;
trace('Loaded file: ' + loadedFileRef.name);
onOpen(loadedFileRef.data);
}
- var onFileSelected = function(event) {
+ var onFileSelected:Event->Void = function(event) {
var selectedFileRef:FileReference = event.target;
trace('Selected file: ' + selectedFileRef.name);
selectedFileRef.addEventListener(Event.COMPLETE, onFileLoaded);
selectedFileRef.load();
}
- var fileRef = new FileReference();
+ var fileRef:FileReference = new FileReference();
fileRef.addEventListener(Event.SELECT, onFileSelected);
fileRef.browse(typeFilter);
return true;
@@ -177,18 +177,18 @@ class FileUtil
public static function saveFile(data:Bytes, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String, ?dialogTitle:String):Bool
{
#if desktop
- var filter = defaultFileName != null ? Path.extension(defaultFileName) : null;
+ var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null;
- var fileDialog = new FileDialog();
+ var fileDialog:FileDialog = new FileDialog();
if (onSave != null) fileDialog.onSelect.add(onSave);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.save(data, filter, defaultFileName, dialogTitle);
return true;
#elseif html5
- var filter = defaultFileName != null ? Path.extension(defaultFileName) : null;
+ var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null;
- var fileDialog = new FileDialog();
+ var fileDialog:FileDialog = new FileDialog();
if (onSave != null) fileDialog.onSave.add(onSave);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
@@ -213,7 +213,7 @@ class FileUtil
{
#if desktop
// Prompt the user for a directory, then write all of the files to there.
- var onSelectDir = function(targetPath:String) {
+ var onSelectDir:String->Void = function(targetPath:String):Void {
var paths:Array = [];
for (resource in resources)
{
@@ -230,7 +230,7 @@ class FileUtil
writeBytesToPath(filePath, resource.data, force ? Force : Skip);
}
}
- catch (e:Dynamic)
+ catch (_)
{
trace('Failed to write file (probably already exists): $filePath' + filePath);
continue;
@@ -240,7 +240,7 @@ class FileUtil
onSaveAll(paths);
}
- browseForDirectory(null, onSelectDir, onCancel, defaultPath, "Choose directory to save all files to...");
+ browseForDirectory(null, onSelectDir, onCancel, defaultPath, 'Choose directory to save all files to...');
return true;
#elseif html5
@@ -260,14 +260,14 @@ class FileUtil
?force:Bool = false):Bool
{
// Create a ZIP file.
- var zipBytes = createZIPFromEntries(resources);
+ var zipBytes:Bytes = createZIPFromEntries(resources);
- var onSave = function(path:String) {
+ var onSave:String->Void = function(path:String) {
onSave([path]);
};
// Prompt the user to save the ZIP file.
- saveFile(zipBytes, onSave, onCancel, defaultPath, "Save files as ZIP...");
+ saveFile(zipBytes, onSave, onCancel, defaultPath, 'Save files as ZIP...');
return true;
}
@@ -282,7 +282,7 @@ class FileUtil
{
#if desktop
// Create a ZIP file.
- var zipBytes = createZIPFromEntries(resources);
+ var zipBytes:Bytes = createZIPFromEntries(resources);
// Write the ZIP.
writeBytesToPath(path, zipBytes, force ? Force : Skip);
@@ -293,13 +293,70 @@ class FileUtil
#end
}
+ /**
+ * Read string file contents directly from a given path.
+ * Only works on desktop.
+ *
+ * @param path The path to the file.
+ * @return The file contents.
+ */
+ public static function readStringFromPath(path:String):String
+ {
+ #if sys
+ return sys.io.File.getContent(path);
+ #else
+ return null;
+ #end
+ }
+
+ /**
+ * Read bytes file contents directly from a given path.
+ * Only works on desktop.
+ *
+ * @param path The path to the file.
+ * @return The file contents.
+ */
+ public static function readBytesFromPath(path:String):Bytes
+ {
+ #if sys
+ return Bytes.ofString(sys.io.File.getContent(path));
+ #else
+ return null;
+ #end
+ }
+
+ /**
+ * Read JSON file contents directly from a given path.
+ * Only works on desktop.
+ *
+ * @param path The path to the file.
+ * @return The JSON data.
+ */
+ public static function readJSONFromPath(path:String):Dynamic
+ {
+ #if sys
+ try
+ {
+ return SerializerUtil.fromJSON(sys.io.File.getContent(path));
+ }
+ catch (ex)
+ {
+ return null;
+ }
+ #else
+ return null;
+ #end
+ }
+
/**
* Write string file contents directly to a given path.
* Only works on desktop.
*
+ * @param path The path to the file.
+ * @param data The string to write.
* @param mode Whether to Force, Skip, or Ask to overwrite an existing file.
*/
- public static function writeStringToPath(path:String, data:String, mode:FileWriteMode = Skip)
+ public static function writeStringToPath(path:String, data:String, mode:FileWriteMode = Skip):Void
{
#if sys
createDirIfNotExists(Path.directory(path));
@@ -336,9 +393,11 @@ class FileUtil
* Write byte file contents directly to a given path.
* Only works on desktop.
*
+ * @param path The path to the file.
+ * @param data The bytes to write.
* @param mode Whether to Force, Skip, or Ask to overwrite an existing file.
*/
- public static function writeBytesToPath(path:String, data:Bytes, mode:FileWriteMode = Skip)
+ public static function writeBytesToPath(path:String, data:Bytes, mode:FileWriteMode = Skip):Void
{
#if sys
createDirIfNotExists(Path.directory(path));
@@ -374,8 +433,11 @@ class FileUtil
/**
* Write string file contents directly to the end of a file at the given path.
* Only works on desktop.
+ *
+ * @param path The path to the file.
+ * @param data The string to append.
*/
- public static function appendStringToPath(path:String, data:String)
+ public static function appendStringToPath(path:String, data:String):Void
{
#if sys
sys.io.File.append(path, false).writeString(data);
@@ -387,8 +449,10 @@ class FileUtil
/**
* Create a directory if it doesn't already exist.
* Only works on desktop.
+ *
+ * @param dir The path to the directory.
*/
- public static function createDirIfNotExists(dir:String)
+ public static function createDirIfNotExists(dir:String):Void
{
#if sys
if (!sys.FileSystem.exists(dir))
@@ -404,6 +468,8 @@ class FileUtil
/**
* Get the path to a temporary directory we can use for writing files.
* Only works on desktop.
+ *
+ * @return The path to the temporary directory.
*/
public static function getTempDir():String
{
@@ -421,9 +487,11 @@ class FileUtil
if (path != null) break;
}
- return tempDir = Path.join([path, 'funkin/']);
+ tempDir = Path.join([path, 'funkin/']);
+ return tempDir;
#else
- return tempDir = '/tmp/funkin/';
+ tempDir = '/tmp/funkin/';
+ return tempDir;
#end
#else
return null;
@@ -438,9 +506,9 @@ class FileUtil
*/
public static function createZIPFromEntries(entries:Array):Bytes
{
- var o = new haxe.io.BytesOutput();
+ var o:haxe.io.BytesOutput = new haxe.io.BytesOutput();
- var zipWriter = new haxe.zip.Writer(o);
+ var zipWriter:haxe.zip.Writer = new haxe.zip.Writer(o);
zipWriter.write(entries.list());
return o.getBytes();
@@ -455,8 +523,20 @@ class FileUtil
*/
public static function makeZIPEntry(name:String, content:String):Entry
{
- var data = haxe.io.Bytes.ofString(content, UTF8);
+ var data:Bytes = haxe.io.Bytes.ofString(content, UTF8);
+ return makeZIPEntryFromBytes(name, data);
+ }
+
+ /**
+ * Create a ZIP file entry from a file name and its string contents.
+ *
+ * @param name The name of the file. You can use slashes to create subdirectories.
+ * @param data The byte data of the file.
+ * @return The resulting entry.
+ */
+ public static function makeZIPEntryFromBytes(name:String, data:haxe.io.Bytes):Entry
+ {
return {
fileName: name,
fileSize: data.length,
@@ -474,15 +554,15 @@ class FileUtil
static function convertTypeFilter(typeFilter:Array):String
{
- var filter = null;
+ var filter:String = null;
if (typeFilter != null)
{
- var filters = [];
+ var filters:Array = [];
for (type in typeFilter)
{
- filters.push(StringTools.replace(StringTools.replace(type.extension, "*.", ""), ";", ","));
+ filters.push(StringTools.replace(StringTools.replace(type.extension, '*.', ''), ';', ','));
}
- filter = filters.join(";");
+ filter = filters.join(';');
}
return filter;
diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx
index 662c05250..9452b7785 100644
--- a/source/funkin/util/SerializerUtil.hx
+++ b/source/funkin/util/SerializerUtil.hx
@@ -44,7 +44,16 @@ class SerializerUtil
*/
public static function fromJSONBytes(input:Bytes):Dynamic
{
- return Json.parse(input.toString());
+ try
+ {
+ return Json.parse(input.toString());
+ }
+ catch (e:Dynamic)
+ {
+ trace('An error occurred while parsing JSON from byte data');
+ trace(e);
+ return null;
+ }
}
/**
diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx
index 1b38edd28..61418c299 100644
--- a/source/funkin/util/SortUtil.hx
+++ b/source/funkin/util/SortUtil.hx
@@ -31,9 +31,12 @@ class SortUtil
/**
* Sort predicate for sorting strings alphabetically.
*/
- public static function alphabetical(a:String, b:String):Int
+ public static function alphabetically(a:String, b:String)
{
+ a = a.toUpperCase();
+ b = b.toUpperCase();
+
// Sort alphabetically. Yes that's how this works.
- return a > b ? 1 : -1;
+ return a == b ? 0 : a > b ? 1 : -1;
}
}