diff --git a/.vscode/settings.json b/.vscode/settings.json index 80d2bf76a..92d49c3d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -71,7 +71,7 @@ "files.eol": "\n", "haxe.displayPort": "auto", - "haxe.enableCompilationServer": true, + "haxe.enableCompilationServer": false, "haxe.displayServer": { "arguments": ["-v"] }, @@ -97,15 +97,35 @@ "args": ["-debug"] }, { - "label": "Windows / Debug (DEBUG ASSETS)", - "target": "windows", - "args": ["-debug", "-DDEBUG_ASSETS"] - }, - { - "label": "Windows / Debug (ANIMATE)", + "label": "Windows / Debug (FlxAnimate Test)", "target": "windows", "args": ["-debug", "-DANIMATE"] }, + { + "label": "Windows / Debug (Straight to Freeplay)", + "target": "windows", + "args": ["-debug", "-DFREEPLAY"] + }, + { + "label": "Windows / Debug (Straight to Play - Bopeebo Normal)", + "target": "windows", + "args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"] + }, + { + "label": "Windows / Debug (Straight to Chart Editor)", + "target": "windows", + "args": ["-debug", "-DCHARTING"] + }, + { + "label": "Windows / Debug (Straight to Animation Editor)", + "target": "windows", + "args": ["-debug", "-DANIMDEBUG"] + }, + { + "label": "Windows / Debug (Latency Test)", + "target": "windows", + "args": ["-debug", "-DLATENCY"] + }, { "label": "HTML5 / Debug", "target": "html5", diff --git a/assets b/assets index ef79a6cf1..be9d790af 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ef79a6cf1ae3dcbd86a5b798f8117a6c692c0156 +Subproject commit be9d790af9c6f1f5e3afc7aed2b1d5c21823bc20 diff --git a/docs/style-guide.md b/docs/style-guide.md index 71ae844c4..1131cca2b 100644 --- a/docs/style-guide.md +++ b/docs/style-guide.md @@ -24,7 +24,7 @@ Example: ``` /** * Finds the largest deviation from the desired time inside this VoicesGroup. - * + * * @param targetTime The time to check against. * If none is provided, it checks the time of all members against the first member of this VoicesGroup. * @return The largest deviation from the target time found. @@ -52,3 +52,10 @@ import sys.io.File; #end ``` +## Argument Formatting + +[Optional arguments](https://haxe.org/manual/types-function-optional-arguments.html) and [default arguments](https://haxe.org/manual/types-function-default-values.html) should be mutually exclusive and not used together! + +For example, `myFunction(?input:Int)` should be used if you want the argument to be a `Null<Int>` whose value is `null` if no value is passed, and `myFunction(input:Int = 0)` should be used if you want the argument to be an `Int`, whose value is `0` if no value is passed. + +Using both at the same time is considered valid by Haxe, but `myFunction(?input:Int = 0)` results in a `Null<Int>` whose value defaults to 0 anyway, so it's never null, but it's annotated as nullable! The biggest consequence of this is that it makes null safety more annoying. diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index ecfa32eb3..5299a3aa0 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -266,6 +266,10 @@ class InitState extends FlxState return; } + // Load and cache the song's charts. + // TODO: Do this in the loading state. + songData.cacheCharts(true); + LoadingState.loadAndSwitchState(new funkin.play.PlayState( { targetSong: songData, diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx index 9a986a8b5..9861c48c7 100644 --- a/source/funkin/MusicBeatState.hx +++ b/source/funkin/MusicBeatState.hx @@ -117,7 +117,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler public function stepHit():Bool { - var event = new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep); + var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep); dispatchEvent(event); @@ -128,7 +128,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler public function beatHit():Bool { - var event = new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep); + var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep); dispatchEvent(event); @@ -148,7 +148,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler override function startOutro(onComplete:() -> Void):Void { - var event = new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_BEGIN, null, true); + var event = new StateChangeScriptEvent(STATE_CHANGE_BEGIN, null, true); dispatchEvent(event); @@ -164,7 +164,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler public override function openSubState(targetSubState:FlxSubState):Void { - var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_BEGIN, targetSubState, true); + var event = new SubStateScriptEvent(SUBSTATE_OPEN_BEGIN, targetSubState, true); dispatchEvent(event); @@ -175,12 +175,12 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler function onOpenSubStateComplete(targetState:FlxSubState):Void { - dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_OPEN_END, targetState, true)); + dispatchEvent(new SubStateScriptEvent(SUBSTATE_OPEN_END, targetState, true)); } public override function closeSubState():Void { - var event = new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_BEGIN, this.subState, true); + var event = new SubStateScriptEvent(SUBSTATE_CLOSE_BEGIN, this.subState, true); dispatchEvent(event); @@ -191,6 +191,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler function onCloseSubStateComplete(targetState:FlxSubState):Void { - dispatchEvent(new SubStateScriptEvent(ScriptEvent.SUBSTATE_CLOSE_END, targetState, true)); + dispatchEvent(new SubStateScriptEvent(SUBSTATE_CLOSE_END, targetState, true)); } } diff --git a/source/funkin/MusicBeatSubState.hx b/source/funkin/MusicBeatSubState.hx index 31d1bd14c..53fe19bdd 100644 --- a/source/funkin/MusicBeatSubState.hx +++ b/source/funkin/MusicBeatSubState.hx @@ -96,7 +96,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl */ public function stepHit():Bool { - var event:ScriptEvent = new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep); + var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep); dispatchEvent(event); @@ -112,7 +112,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl */ public function beatHit():Bool { - var event:ScriptEvent = new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep); + var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep); dispatchEvent(event); diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 88993e519..783f52a64 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -447,7 +447,7 @@ class SongChartData } } -class SongEventData +class SongEventDataRaw { /** * The timestamp of the event. The timestamp is in the format of the song's time format. @@ -503,40 +503,57 @@ class SongEventData return _stepTime = Conductor.getTimeInSteps(this.time); } +} + +/** + * Wrap SongEventData in an abstract so we can overload operators. + */ +@:forward(time, event, value, activated, getStepTime) +abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw +{ + public function new(time:Float, event:String, value:Dynamic = null) + { + this = new SongEventDataRaw(time, event, value); + } public inline function getDynamic(key:String):Null<Dynamic> { - return value == null ? null : Reflect.field(value, key); + return this.value == null ? null : Reflect.field(this.value, key); } public inline function getBool(key:String):Null<Bool> { - return value == null ? null : cast Reflect.field(value, key); + return this.value == null ? null : cast Reflect.field(this.value, key); } public inline function getInt(key:String):Null<Int> { - return value == null ? null : cast Reflect.field(value, key); + return this.value == null ? null : cast Reflect.field(this.value, key); } public inline function getFloat(key:String):Null<Float> { - return value == null ? null : cast Reflect.field(value, key); + return this.value == null ? null : cast Reflect.field(this.value, key); } public inline function getString(key:String):String { - return value == null ? null : cast Reflect.field(value, key); + return this.value == null ? null : cast Reflect.field(this.value, key); } public inline function getArray(key:String):Array<Dynamic> { - return value == null ? null : cast Reflect.field(value, key); + return this.value == null ? null : cast Reflect.field(this.value, key); } public inline function getBoolArray(key:String):Array<Bool> { - return value == null ? null : cast Reflect.field(value, key); + return this.value == null ? null : cast Reflect.field(this.value, key); + } + + public function clone():SongEventData + { + return new SongEventData(this.time, this.event, this.value); } @:op(A == B) @@ -584,7 +601,7 @@ class SongEventData } } -class SongNoteData +class SongNoteDataRaw { /** * The timestamp of the note. The timestamp is in the format of the song's time format. @@ -655,6 +672,48 @@ class SongNoteData return _stepTime = Conductor.getTimeInSteps(this.time); } + @:jignored + var _stepLength:Null<Float> = null; + + /** + * @param force Set to `true` to force recalculation (good after BPM changes) + * @return The length of the hold note in steps, or `0` if this is not a hold note. + */ + public function getStepLength(force = false):Float + { + if (this.length <= 0) return 0.0; + + if (_stepLength != null && !force) return _stepLength; + + return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime(); + } + + public function setStepLength(value:Float):Void + { + if (value <= 0) + { + this.length = 0.0; + } + else + { + var lengthMs:Float = Conductor.getStepTimeInMs(value) - this.time; + this.length = lengthMs; + } + _stepLength = null; + } +} + +/** + * Wrap SongNoteData in an abstract so we can overload operators. + */ +@:forward +abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw +{ + public function new(time:Float, data:Int, length:Float = 0, kind:String = '') + { + this = new SongNoteDataRaw(time, data, length, kind); + } + /** * The direction of the note, if applicable. * Strips the strumline index from the data. @@ -668,7 +727,12 @@ class SongNoteData public function getDirectionName(strumlineSize:Int = 4):String { - switch (this.data % strumlineSize) + return SongNoteData.buildDirectionName(this.data, strumlineSize); + } + + public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String + { + switch (data % strumlineSize) { case 0: return 'Left'; @@ -705,36 +769,6 @@ class SongNoteData return getStrumlineIndex(strumlineSize) == 0; } - @:jignored - var _stepLength:Null<Float> = null; - - /** - * @param force Set to `true` to force recalculation (good after BPM changes) - * @return The length of the hold note in steps, or `0` if this is not a hold note. - */ - public function getStepLength(force = false):Float - { - if (this.length <= 0) return 0.0; - - if (_stepLength != null && !force) return _stepLength; - - return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime(); - } - - public function setStepLength(value:Float):Void - { - if (value <= 0) - { - this.length = 0.0; - } - else - { - var lengthMs:Float = Conductor.getStepTimeInMs(value) - this.time; - this.length = lengthMs; - } - _stepLength = null; - } - @:jignored public var isHoldNote(get, never):Bool; @@ -797,6 +831,11 @@ class SongNoteData return this.time <= other.time; } + public function clone():SongNoteData + { + return new SongNoteData(this.time, this.data, this.length, this.kind); + } + /** * Produces a string representation suitable for debugging. */ diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index 3ff3943c6..984af18fa 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -66,8 +66,14 @@ class SongDataUtils var result = notes.filter(function(note:SongNoteData):Bool { for (x in subtrahend) + { + // The currently iterated note is in the subtrahend array. // SongNoteData's == operation has been overridden so that this will work. - if (x == note) return false; + if (x == note) + { + return false; + } + } return true; }); diff --git a/source/funkin/import.hx b/source/funkin/import.hx index 1c3a0fdb4..8c7124da0 100644 --- a/source/funkin/import.hx +++ b/source/funkin/import.hx @@ -12,7 +12,9 @@ using Lambda; using StringTools; using funkin.util.tools.ArraySortTools; using funkin.util.tools.ArrayTools; +using funkin.util.tools.FloatTools; using funkin.util.tools.Int64Tools; +using funkin.util.tools.IntTools; using funkin.util.tools.IteratorTools; using funkin.util.tools.MapTools; using funkin.util.tools.SongEventDataArrayTools; diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 586a6206c..18f934aee 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -10,265 +10,12 @@ import funkin.play.notes.NoteDirection; import openfl.events.EventType; import openfl.events.KeyboardEvent; -typedef ScriptEventType = EventType<ScriptEvent>; - /** * This is a base class for all events that are issued to scripted classes. * It can be used to identify the type of event called, store data, and cancel event propagation. */ class ScriptEvent { - /** - * Called when the relevant object is created. - * Keep in mind that the constructor may be called before the object is needed, - * for the purposes of caching data or otherwise. - * - * This event is not cancelable. - */ - public static inline final CREATE:ScriptEventType = 'CREATE'; - - /** - * Called when the relevant object is destroyed. - * This should perform relevant cleanup to ensure good performance. - * - * This event is not cancelable. - */ - public static inline final DESTROY:ScriptEventType = 'DESTROY'; - - /** - * Called when the relevent object is added to the game state. - * This assumes all data is loaded and ready to go. - * - * This event is not cancelable. - */ - public static inline final ADDED:ScriptEventType = 'ADDED'; - - /** - * Called during the update function. - * This is called every frame, so be careful! - * - * This event is not cancelable. - */ - public static inline final UPDATE:ScriptEventType = 'UPDATE'; - - /** - * Called when the player moves to pause the game. - * - * This event IS cancelable! Canceling the event will prevent the game from pausing. - */ - public static inline final PAUSE:ScriptEventType = 'PAUSE'; - - /** - * Called when the player moves to unpause the game while paused. - * - * This event IS cancelable! Canceling the event will prevent the game from resuming. - */ - public static inline final RESUME:ScriptEventType = 'RESUME'; - - /** - * Called once per step in the song. This happens 4 times per measure. - * - * This event is not cancelable. - */ - public static inline final SONG_BEAT_HIT:ScriptEventType = 'BEAT_HIT'; - - /** - * Called once per step in the song. This happens 16 times per measure. - * - * This event is not cancelable. - */ - public static inline final SONG_STEP_HIT:ScriptEventType = 'STEP_HIT'; - - /** - * Called when a character hits a note. - * Important information such as judgement/timing, note data, player/opponent, etc. are all provided. - * - * This event IS cancelable! Canceling this event prevents the note from being hit, - * and will likely result in a miss later. - */ - public static inline final NOTE_HIT:ScriptEventType = 'NOTE_HIT'; - - /** - * Called when a character misses a note. - * Important information such as note data, player/opponent, etc. are all provided. - * - * This event IS cancelable! Canceling this event prevents the note from being considered missed, - * avoiding a combo break and lost health. - */ - public static inline final NOTE_MISS:ScriptEventType = 'NOTE_MISS'; - - /** - * Called when a character presses a note when there was none there, causing them to lose health. - * Important information such as direction pressed, etc. are all provided. - * - * This event IS cancelable! Canceling this event prevents the note from being considered missed, - * avoiding lost health/score and preventing the miss animation. - */ - public static inline final NOTE_GHOST_MISS:ScriptEventType = 'NOTE_GHOST_MISS'; - - /** - * Called when a song event is reached in the chart. - * - * This event IS cancelable! Cancelling this event prevents the event from being triggered, - * thus blocking its normal functionality. - */ - public static inline final SONG_EVENT:ScriptEventType = 'SONG_EVENT'; - - /** - * Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin. - * - * This event is not cancelable. - */ - public static inline final SONG_START:ScriptEventType = 'SONG_START'; - - /** - * Called when the song ends. This happens as the instrumental and vocals end. - * - * This event is not cancelable. - */ - public static inline final SONG_END:ScriptEventType = 'SONG_END'; - - /** - * Called when the countdown begins. This occurs before the song starts. - * - * This event IS cancelable! Canceling this event will prevent the countdown from starting. - * - The song will not start until you call Countdown.performCountdown() later. - * - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it. - */ - public static inline final COUNTDOWN_START:ScriptEventType = 'COUNTDOWN_START'; - - /** - * Called when a step of the countdown happens. - * Includes information about what step of the countdown was hit. - * - * This event IS cancelable! Canceling this event will pause the countdown. - * - The countdown will not resume until you call PlayState.resumeCountdown(). - */ - public static inline final COUNTDOWN_STEP:ScriptEventType = 'COUNTDOWN_STEP'; - - /** - * Called when the countdown is done but just before the song starts. - * - * This event is not cancelable. - */ - public static inline final COUNTDOWN_END:ScriptEventType = 'COUNTDOWN_END'; - - /** - * Called before the game over screen triggers and the death animation plays. - * - * This event is not cancelable. - */ - public static inline final GAME_OVER:ScriptEventType = 'GAME_OVER'; - - /** - * Called after the player presses a key to restart the game. - * This can happen from the pause menu or the game over screen. - * - * This event IS cancelable! Canceling this event will prevent the game from restarting. - */ - public static inline final SONG_RETRY:ScriptEventType = 'SONG_RETRY'; - - /** - * Called when the player pushes down any key on the keyboard. - * - * This event is not cancelable. - */ - public static inline final KEY_DOWN:ScriptEventType = 'KEY_DOWN'; - - /** - * Called when the player releases a key on the keyboard. - * - * This event is not cancelable. - */ - public static inline final KEY_UP:ScriptEventType = 'KEY_UP'; - - /** - * Called when the game has finished loading the notes from JSON. - * This allows modders to mutate the notes before they are used in the song. - * - * This event is not cancelable. - */ - public static inline final SONG_LOADED:ScriptEventType = 'SONG_LOADED'; - - /** - * Called when the game is about to switch the current FlxState. - * - * This event is not cancelable. - */ - public static inline final STATE_CHANGE_BEGIN:ScriptEventType = 'STATE_CHANGE_BEGIN'; - - /** - * Called when the game has finished switching the current FlxState. - * - * This event is not cancelable. - */ - public static inline final STATE_CHANGE_END:ScriptEventType = 'STATE_CHANGE_END'; - - /** - * Called when the game is about to open a new FlxSubState. - * - * This event is not cancelable. - */ - public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = 'SUBSTATE_OPEN_BEGIN'; - - /** - * Called when the game has finished opening a new FlxSubState. - * - * This event is not cancelable. - */ - public static inline final SUBSTATE_OPEN_END:ScriptEventType = 'SUBSTATE_OPEN_END'; - - /** - * Called when the game is about to close the current FlxSubState. - * - * This event is not cancelable. - */ - public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = 'SUBSTATE_CLOSE_BEGIN'; - - /** - * Called when the game has finished closing the current FlxSubState. - * - * This event is not cancelable. - */ - public static inline final SUBSTATE_CLOSE_END:ScriptEventType = 'SUBSTATE_CLOSE_END'; - - /** - * Called when the game starts a conversation. - * - * This event is not cancelable. - */ - public static inline final DIALOGUE_START:ScriptEventType = 'DIALOGUE_START'; - - /** - * Called to display the next line of conversation. - * - * This event IS cancelable! Canceling this event will prevent the conversation from moving to the next line. - * - This event is called when the conversation starts, or when the user presses ACCEPT to advance the conversation. - */ - public static inline final DIALOGUE_LINE:ScriptEventType = 'DIALOGUE_LINE'; - - /** - * Called to skip scrolling the current line of conversation. - * - * This event IS cancelable! Canceling this event will prevent the conversation from skipping to the next line. - * - This event is called when the user presses ACCEPT to advance the conversation while it is already advancing. - */ - public static inline final DIALOGUE_COMPLETE_LINE:ScriptEventType = 'DIALOGUE_COMPLETE_LINE'; - - /** - * Called to skip the conversation. - * - * This event IS cancelable! Canceling this event will prevent the conversation from skipping. - */ - public static inline final DIALOGUE_SKIP:ScriptEventType = 'DIALOGUE_SKIP'; - - /** - * Called when the game ends a conversation. - * - * This event is not cancelable. - */ - public static inline final DIALOGUE_END:ScriptEventType = 'DIALOGUE_END'; - /** * If true, the behavior associated with this event can be prevented. * For example, cancelling COUNTDOWN_START should prevent the countdown from starting, @@ -411,7 +158,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void { - super(ScriptEvent.NOTE_GHOST_MISS, true); + super(NOTE_GHOST_MISS, true); this.dir = dir; this.hasPossibleNotes = hasPossibleNotes; this.healthChange = healthChange; @@ -439,7 +186,7 @@ class SongEventScriptEvent extends ScriptEvent public function new(event:funkin.data.song.SongData.SongEventData):Void { - super(ScriptEvent.SONG_EVENT, true); + super(SONG_EVENT, true); this.event = event; } @@ -462,7 +209,7 @@ class UpdateScriptEvent extends ScriptEvent public function new(elapsed:Float):Void { - super(ScriptEvent.UPDATE, false); + super(UPDATE, false); this.elapsed = elapsed; } @@ -591,7 +338,7 @@ class SongLoadScriptEvent extends ScriptEvent public function new(id:String, difficulty:String, notes:Array<SongNoteData>):Void { - super(ScriptEvent.SONG_LOADED, false); + super(SONG_LOADED, false); this.id = id; this.difficulty = difficulty; this.notes = notes; @@ -660,7 +407,7 @@ class PauseScriptEvent extends ScriptEvent public function new(gitaroo:Bool):Void { - super(ScriptEvent.PAUSE, true); + super(PAUSE, true); this.gitaroo = gitaroo; } } diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index 5e3e32a46..f5d797ea4 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -23,15 +23,16 @@ class ScriptEventDispatcher // IScriptedClass switch (event.type) { - case ScriptEvent.CREATE: + case CREATE: target.onCreate(event); return; - case ScriptEvent.DESTROY: + case DESTROY: target.onDestroy(event); return; - case ScriptEvent.UPDATE: + case UPDATE: target.onUpdate(cast event); return; + default: // Continue; } if (Std.isOfType(target, IStateStageProp)) @@ -39,9 +40,10 @@ class ScriptEventDispatcher var t:IStateStageProp = cast(target, IStateStageProp); switch (event.type) { - case ScriptEvent.ADDED: + case ADDED: t.onAdd(cast event); return; + default: // Continue; } } @@ -50,21 +52,22 @@ class ScriptEventDispatcher var t:IDialogueScriptedClass = cast(target, IDialogueScriptedClass); switch (event.type) { - case ScriptEvent.DIALOGUE_START: + case DIALOGUE_START: t.onDialogueStart(cast event); return; - case ScriptEvent.DIALOGUE_LINE: + case DIALOGUE_LINE: t.onDialogueLine(cast event); return; - case ScriptEvent.DIALOGUE_COMPLETE_LINE: + case DIALOGUE_COMPLETE_LINE: t.onDialogueCompleteLine(cast event); return; - case ScriptEvent.DIALOGUE_SKIP: + case DIALOGUE_SKIP: t.onDialogueSkip(cast event); return; - case ScriptEvent.DIALOGUE_END: + case DIALOGUE_END: t.onDialogueEnd(cast event); return; + default: // Continue; } } @@ -73,54 +76,55 @@ class ScriptEventDispatcher var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass); switch (event.type) { - case ScriptEvent.NOTE_HIT: + case NOTE_HIT: t.onNoteHit(cast event); return; - case ScriptEvent.NOTE_MISS: + case NOTE_MISS: t.onNoteMiss(cast event); return; - case ScriptEvent.NOTE_GHOST_MISS: + case NOTE_GHOST_MISS: t.onNoteGhostMiss(cast event); return; - case ScriptEvent.SONG_BEAT_HIT: + case SONG_BEAT_HIT: t.onBeatHit(cast event); return; - case ScriptEvent.SONG_STEP_HIT: + case SONG_STEP_HIT: t.onStepHit(cast event); return; - case ScriptEvent.SONG_START: + case SONG_START: t.onSongStart(event); return; - case ScriptEvent.SONG_END: + case SONG_END: t.onSongEnd(event); return; - case ScriptEvent.SONG_RETRY: + case SONG_RETRY: t.onSongRetry(event); return; - case ScriptEvent.GAME_OVER: + case GAME_OVER: t.onGameOver(event); return; - case ScriptEvent.PAUSE: + case PAUSE: t.onPause(cast event); return; - case ScriptEvent.RESUME: + case RESUME: t.onResume(event); return; - case ScriptEvent.SONG_EVENT: + case SONG_EVENT: t.onSongEvent(cast event); return; - case ScriptEvent.COUNTDOWN_START: + case COUNTDOWN_START: t.onCountdownStart(cast event); return; - case ScriptEvent.COUNTDOWN_STEP: + case COUNTDOWN_STEP: t.onCountdownStep(cast event); return; - case ScriptEvent.COUNTDOWN_END: + case COUNTDOWN_END: t.onCountdownEnd(cast event); return; - case ScriptEvent.SONG_LOADED: + case SONG_LOADED: t.onSongLoaded(cast event); return; + default: // Continue; } } @@ -129,24 +133,25 @@ class ScriptEventDispatcher var t = cast(target, IStateChangingScriptedClass); switch (event.type) { - case ScriptEvent.STATE_CHANGE_BEGIN: + case STATE_CHANGE_BEGIN: t.onStateChangeBegin(cast event); return; - case ScriptEvent.STATE_CHANGE_END: + case STATE_CHANGE_END: t.onStateChangeEnd(cast event); return; - case ScriptEvent.SUBSTATE_OPEN_BEGIN: + case SUBSTATE_OPEN_BEGIN: t.onSubStateOpenBegin(cast event); return; - case ScriptEvent.SUBSTATE_OPEN_END: + case SUBSTATE_OPEN_END: t.onSubStateOpenEnd(cast event); return; - case ScriptEvent.SUBSTATE_CLOSE_BEGIN: + case SUBSTATE_CLOSE_BEGIN: t.onSubStateCloseBegin(cast event); return; - case ScriptEvent.SUBSTATE_CLOSE_END: + case SUBSTATE_CLOSE_END: t.onSubStateCloseEnd(cast event); return; + default: // Continue; } } else diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx new file mode 100644 index 000000000..e06b5ad24 --- /dev/null +++ b/source/funkin/modding/events/ScriptEventType.hx @@ -0,0 +1,271 @@ +package funkin.modding.events; + +enum abstract ScriptEventType(String) from String to String +{ + /** + * Called when the relevant object is created. + * Keep in mind that the constructor may be called before the object is needed, + * for the purposes of caching data or otherwise. + * + * This event is not cancelable. + */ + var CREATE = 'CREATE'; + + /** + * Called when the relevant object is destroyed. + * This should perform relevant cleanup to ensure good performance. + * + * This event is not cancelable. + */ + var DESTROY = 'DESTROY'; + + /** + * Called when the relevent object is added to the game state. + * This assumes all data is loaded and ready to go. + * + * This event is not cancelable. + */ + var ADDED = 'ADDED'; + + /** + * Called during the update function. + * This is called every frame, so be careful! + * + * This event is not cancelable. + */ + var UPDATE = 'UPDATE'; + + /** + * Called when the player moves to pause the game. + * + * This event IS cancelable! Canceling the event will prevent the game from pausing. + */ + var PAUSE = 'PAUSE'; + + /** + * Called when the player moves to unpause the game while paused. + * + * This event IS cancelable! Canceling the event will prevent the game from resuming. + */ + var RESUME = 'RESUME'; + + /** + * Called once per step in the song. This happens 4 times per measure. + * + * This event is not cancelable. + */ + var SONG_BEAT_HIT = 'BEAT_HIT'; + + /** + * Called once per step in the song. This happens 16 times per measure. + * + * This event is not cancelable. + */ + var SONG_STEP_HIT = 'STEP_HIT'; + + /** + * Called when a character hits a note. + * Important information such as judgement/timing, note data, player/opponent, etc. are all provided. + * + * This event IS cancelable! Canceling this event prevents the note from being hit, + * and will likely result in a miss later. + */ + var NOTE_HIT = 'NOTE_HIT'; + + /** + * Called when a character misses a note. + * Important information such as note data, player/opponent, etc. are all provided. + * + * This event IS cancelable! Canceling this event prevents the note from being considered missed, + * avoiding a combo break and lost health. + */ + var NOTE_MISS = 'NOTE_MISS'; + + /** + * Called when a character presses a note when there was none there, causing them to lose health. + * Important information such as direction pressed, etc. are all provided. + * + * This event IS cancelable! Canceling this event prevents the note from being considered missed, + * avoiding lost health/score and preventing the miss animation. + */ + var NOTE_GHOST_MISS = 'NOTE_GHOST_MISS'; + + /** + * Called when a song event is reached in the chart. + * + * This event IS cancelable! Cancelling this event prevents the event from being triggered, + * thus blocking its normal functionality. + */ + var SONG_EVENT = 'SONG_EVENT'; + + /** + * Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin. + * + * This event is not cancelable. + */ + var SONG_START = 'SONG_START'; + + /** + * Called when the song ends. This happens as the instrumental and vocals end. + * + * This event is not cancelable. + */ + var SONG_END = 'SONG_END'; + + /** + * Called when the countdown begins. This occurs before the song starts. + * + * This event IS cancelable! Canceling this event will prevent the countdown from starting. + * - The song will not start until you call Countdown.performCountdown() later. + * - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it. + */ + var COUNTDOWN_START = 'COUNTDOWN_START'; + + /** + * Called when a step of the countdown happens. + * Includes information about what step of the countdown was hit. + * + * This event IS cancelable! Canceling this event will pause the countdown. + * - The countdown will not resume until you call PlayState.resumeCountdown(). + */ + var COUNTDOWN_STEP = 'COUNTDOWN_STEP'; + + /** + * Called when the countdown is done but just before the song starts. + * + * This event is not cancelable. + */ + var COUNTDOWN_END = 'COUNTDOWN_END'; + + /** + * Called before the game over screen triggers and the death animation plays. + * + * This event is not cancelable. + */ + var GAME_OVER = 'GAME_OVER'; + + /** + * Called after the player presses a key to restart the game. + * This can happen from the pause menu or the game over screen. + * + * This event IS cancelable! Canceling this event will prevent the game from restarting. + */ + var SONG_RETRY = 'SONG_RETRY'; + + /** + * Called when the player pushes down any key on the keyboard. + * + * This event is not cancelable. + */ + var KEY_DOWN = 'KEY_DOWN'; + + /** + * Called when the player releases a key on the keyboard. + * + * This event is not cancelable. + */ + var KEY_UP = 'KEY_UP'; + + /** + * Called when the game has finished loading the notes from JSON. + * This allows modders to mutate the notes before they are used in the song. + * + * This event is not cancelable. + */ + var SONG_LOADED = 'SONG_LOADED'; + + /** + * Called when the game is about to switch the current FlxState. + * + * This event is not cancelable. + */ + var STATE_CHANGE_BEGIN = 'STATE_CHANGE_BEGIN'; + + /** + * Called when the game has finished switching the current FlxState. + * + * This event is not cancelable. + */ + var STATE_CHANGE_END = 'STATE_CHANGE_END'; + + /** + * Called when the game is about to open a new FlxSubState. + * + * This event is not cancelable. + */ + var SUBSTATE_OPEN_BEGIN = 'SUBSTATE_OPEN_BEGIN'; + + /** + * Called when the game has finished opening a new FlxSubState. + * + * This event is not cancelable. + */ + var SUBSTATE_OPEN_END = 'SUBSTATE_OPEN_END'; + + /** + * Called when the game is about to close the current FlxSubState. + * + * This event is not cancelable. + */ + var SUBSTATE_CLOSE_BEGIN = 'SUBSTATE_CLOSE_BEGIN'; + + /** + * Called when the game has finished closing the current FlxSubState. + * + * This event is not cancelable. + */ + var SUBSTATE_CLOSE_END = 'SUBSTATE_CLOSE_END'; + + /** + * Called when the game starts a conversation. + * + * This event is not cancelable. + */ + var DIALOGUE_START = 'DIALOGUE_START'; + + /** + * Called to display the next line of conversation. + * + * This event IS cancelable! Canceling this event will prevent the conversation from moving to the next line. + * - This event is called when the conversation starts, or when the user presses ACCEPT to advance the conversation. + */ + var DIALOGUE_LINE = 'DIALOGUE_LINE'; + + /** + * Called to skip scrolling the current line of conversation. + * + * This event IS cancelable! Canceling this event will prevent the conversation from skipping to the next line. + * - This event is called when the user presses ACCEPT to advance the conversation while it is already advancing. + */ + var DIALOGUE_COMPLETE_LINE = 'DIALOGUE_COMPLETE_LINE'; + + /** + * Called to skip the conversation. + * + * This event IS cancelable! Canceling this event will prevent the conversation from skipping. + */ + var DIALOGUE_SKIP = 'DIALOGUE_SKIP'; + + /** + * Called when the game ends a conversation. + * + * This event is not cancelable. + */ + var DIALOGUE_END = 'DIALOGUE_END'; + + /** + * Allow for comparing `ScriptEventType` to `String`. + */ + @:op(A == B) private static inline function equals(a:ScriptEventType, b:String):Bool + { + return (a : String) == b; + } + + /** + * Allow for comparing `ScriptEventType` to `String`. + */ + @:op(A != B) private static inline function notEquals(a:ScriptEventType, b:String):Bool + { + return (a : String) != b; + } +} diff --git a/source/funkin/modding/module/ModuleHandler.hx b/source/funkin/modding/module/ModuleHandler.hx index 3cc7b7984..4711e7419 100644 --- a/source/funkin/modding/module/ModuleHandler.hx +++ b/source/funkin/modding/module/ModuleHandler.hx @@ -55,7 +55,7 @@ class ModuleHandler static function onStateSwitchComplete():Void { - callEvent(new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_END, FlxG.state, true)); + callEvent(new StateChangeScriptEvent(STATE_CHANGE_END, FlxG.state, true)); } static function addToModuleCache(module:Module):Void @@ -119,7 +119,7 @@ class ModuleHandler { if (moduleCache != null) { - var event = new ScriptEvent(ScriptEvent.DESTROY, false); + var event = new ScriptEvent(DESTROY, false); // Note: Ignore stopPropagation() for (key => value in moduleCache) @@ -148,6 +148,6 @@ class ModuleHandler public static inline function callOnCreate():Void { - callEvent(new ScriptEvent(ScriptEvent.CREATE, false)); + callEvent(new ScriptEvent(CREATE, false)); } } diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index 9796c7161..d23574ce2 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -43,7 +43,7 @@ class Countdown Conductor.update(PlayState.instance.startTimestamp + Conductor.beatLengthMs * -5); // Handle onBeatHit events manually // @:privateAccess - // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, 0, 0)); + // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0)); // The timer function gets called based on the beat of the song. countdownTimer = new FlxTimer(); @@ -59,7 +59,7 @@ class Countdown // onBeatHit events are now properly dispatched by the Conductor even at negative timestamps, // so calling this is no longer necessary. - // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, 0, 0)); + // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0)); // Countdown graphic. showCountdownGraphic(countdownStep, isPixelStyle); @@ -94,11 +94,11 @@ class Countdown switch (index) { case BEFORE: - event = new CountdownScriptEvent(ScriptEvent.COUNTDOWN_START, index); + event = new CountdownScriptEvent(COUNTDOWN_START, index); case THREE | TWO | ONE | GO: // I didn't know you could use `|` in a switch/case block! - event = new CountdownScriptEvent(ScriptEvent.COUNTDOWN_STEP, index); + event = new CountdownScriptEvent(COUNTDOWN_STEP, index); case AFTER: - event = new CountdownScriptEvent(ScriptEvent.COUNTDOWN_END, index, false); + event = new CountdownScriptEvent(COUNTDOWN_END, index, false); default: return true; } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 1d3480efe..4542b9f98 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -682,7 +682,7 @@ class PlayState extends MusicBeatSubState { if (!assertChartExists()) return; - dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY)); + dispatchEvent(new ScriptEvent(SONG_RETRY)); resetCamera(); @@ -867,7 +867,7 @@ class PlayState extends MusicBeatSubState deathCounter += 1; - dispatchEvent(new ScriptEvent(ScriptEvent.GAME_OVER)); + dispatchEvent(new ScriptEvent(GAME_OVER)); // Disable updates, preventing animations in the background from playing. persistentUpdate = false; @@ -994,7 +994,7 @@ class PlayState extends MusicBeatSubState { if (Std.isOfType(subState, PauseSubState)) { - var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true); + var event:ScriptEvent = new ScriptEvent(RESUME, true); dispatchEvent(event); @@ -1097,7 +1097,7 @@ class PlayState extends MusicBeatSubState if (this.currentStage != null) { remove(currentStage); - var event:ScriptEvent = new ScriptEvent(ScriptEvent.DESTROY, false); + var event:ScriptEvent = new ScriptEvent(DESTROY, false); ScriptEventDispatcher.callEvent(currentStage, event); currentStage = null; } @@ -1116,7 +1116,7 @@ class PlayState extends MusicBeatSubState super.debug_refreshModules(); - var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); + var event:ScriptEvent = new ScriptEvent(CREATE, false); ScriptEventDispatcher.callEvent(currentSong, event); } @@ -1332,7 +1332,7 @@ class PlayState extends MusicBeatSubState if (currentStage != null) { // Actually create and position the sprites. - var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); + var event:ScriptEvent = new ScriptEvent(CREATE, false); ScriptEventDispatcher.callEvent(currentStage, event); // Apply camera zoom level from stage data. @@ -1640,7 +1640,7 @@ class PlayState extends MusicBeatSubState add(currentConversation); refresh(); - var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); + var event:ScriptEvent = new ScriptEvent(CREATE, false); ScriptEventDispatcher.callEvent(currentConversation, event); } @@ -1664,7 +1664,7 @@ class PlayState extends MusicBeatSubState */ function startSong():Void { - dispatchEvent(new ScriptEvent(ScriptEvent.SONG_START)); + dispatchEvent(new ScriptEvent(SONG_START)); startingSong = false; @@ -1783,7 +1783,7 @@ class PlayState extends MusicBeatSubState // Call an event to allow canceling the note hit. // NOTE: This is what handles the character animations! - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, 0, true); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, 0, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -1872,7 +1872,7 @@ class PlayState extends MusicBeatSubState { // Call an event to allow canceling the note miss. // NOTE: This is what handles the character animations! - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, 0, true); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, 0, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2021,7 +2021,7 @@ class PlayState extends MusicBeatSubState function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void { - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, Highscore.tallies.combo + 1, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2053,7 +2053,7 @@ class PlayState extends MusicBeatSubState // a MISS is when you let a note scroll past you!! Highscore.tallies.missed++; - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, Highscore.tallies.combo, true); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) return; @@ -2385,7 +2385,7 @@ class PlayState extends MusicBeatSubState */ function endSong():Void { - dispatchEvent(new ScriptEvent(ScriptEvent.SONG_END)); + dispatchEvent(new ScriptEvent(SONG_END)); #if sys // spitter for ravy, teehee!! @@ -2593,7 +2593,7 @@ class PlayState extends MusicBeatSubState { remove(currentStage); currentStage.kill(); - dispatchEvent(new ScriptEvent(ScriptEvent.DESTROY, false)); + dispatchEvent(new ScriptEvent(DESTROY, false)); currentStage = null; } diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index 8be9f25c7..abe8bf992 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -254,7 +254,7 @@ class CharacterDataParser char.debug = debug; // Call onCreate only in the fetchCharacter() function, not at application initialization. - ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEvent.CREATE)); + ScriptEventDispatcher.callEvent(char, new ScriptEvent(CREATE)); return char; } diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx index 2b7db381c..46acf3f37 100644 --- a/source/funkin/play/cutscene/dialogue/Conversation.hx +++ b/source/funkin/play/cutscene/dialogue/Conversation.hx @@ -120,7 +120,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass this.alpha = 1.0; // Start the dialogue. - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_START, this, false)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, false)); } function setupMusic():Void @@ -214,7 +214,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass return; } - ScriptEventDispatcher.callEvent(nextSpeaker, new ScriptEvent(ScriptEvent.CREATE, true)); + ScriptEventDispatcher.callEvent(nextSpeaker, new ScriptEvent(CREATE, true)); currentSpeaker = nextSpeaker; currentSpeaker.zIndex = 200; @@ -258,7 +258,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass return; } - ScriptEventDispatcher.callEvent(nextDialogueBox, new ScriptEvent(ScriptEvent.CREATE, true)); + ScriptEventDispatcher.callEvent(nextDialogueBox, new ScriptEvent(CREATE, true)); currentDialogueBox = nextDialogueBox; currentDialogueBox.zIndex = 300; @@ -293,7 +293,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass public function startConversation():Void { - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_START, this, true)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, true)); } /** @@ -308,13 +308,13 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass switch (state) { case ConversationState.Start: - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_START, this, true)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, true)); case ConversationState.Opening: - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_COMPLETE_LINE, this, true)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_COMPLETE_LINE, this, true)); case ConversationState.Speaking: - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_COMPLETE_LINE, this, true)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_COMPLETE_LINE, this, true)); case ConversationState.Idle: - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_LINE, this, true)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_LINE, this, true)); case ConversationState.Ending: // Skip the outro. endOutro(); @@ -371,7 +371,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass */ public function skipConversation():Void { - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_SKIP, this, true)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_SKIP, this, true)); } static var outroTween:FlxTween; @@ -405,7 +405,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass public function endOutro():Void { outroTween = null; - ScriptEventDispatcher.callEvent(this, new ScriptEvent(ScriptEvent.DESTROY, false)); + ScriptEventDispatcher.callEvent(this, new ScriptEvent(DESTROY, false)); } /** @@ -445,7 +445,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass if (currentDialogueEntry >= currentDialogueEntryCount) { - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_END, this, false)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_END, this, false)); } else { @@ -485,7 +485,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass propagateEvent(event); if (event.eventCanceled) return; - dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_END, this, false)); + dispatchEvent(new DialogueScriptEvent(DIALOGUE_END, this, false)); } public function onDialogueEnd(event:DialogueScriptEvent):Void diff --git a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx b/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx index 4d7f74a58..70ac011a2 100644 --- a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx +++ b/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx @@ -30,7 +30,7 @@ class ConversationDebugState extends MusicBeatState conversation.completeCallback = onConversationComplete; add(conversation); - ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(ScriptEvent.CREATE, false)); + ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false)); } function onConversationComplete():Void diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 60b8b9864..9562ef2ca 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -47,8 +47,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta */ public final _data:Null<SongMetadata>; - final _metadata:Array<SongMetadata>; - + // key = variation id, value = metadata + final _metadata:Map<String, SongMetadata>; final variations:Array<String>; final difficulties:Map<String, SongDifficulty>; @@ -62,7 +62,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta function get_songName():String { if (_data != null) return _data?.songName ?? DEFAULT_SONGNAME; - if (_metadata.length > 0) return _metadata[0]?.songName ?? DEFAULT_SONGNAME; + if (_metadata.size() > 0) return _metadata.get(Constants.DEFAULT_VARIATION)?.songName ?? DEFAULT_SONGNAME; return DEFAULT_SONGNAME; } @@ -71,7 +71,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta function get_songArtist():String { if (_data != null) return _data?.artist ?? DEFAULT_ARTIST; - if (_metadata.length > 0) return _metadata[0]?.artist ?? DEFAULT_ARTIST; + if (_metadata.size() > 0) return _metadata.get(Constants.DEFAULT_VARIATION)?.artist ?? DEFAULT_ARTIST; return DEFAULT_ARTIST; } @@ -88,7 +88,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta _data = _fetchData(id); - _metadata = _data == null ? [] : [_data]; + _metadata = _data == null ? [] : [Constants.DEFAULT_VARIATION => _data]; variations.clear(); variations.push(Constants.DEFAULT_VARIATION); @@ -100,9 +100,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta } for (meta in fetchVariationMetadata(id)) - _metadata.push(meta); + _metadata.set(meta.variation, meta); - if (_metadata.length == 0) + if (_metadata.size() == 0) { trace('[WARN] Could not find song data for songId: $id'); return; @@ -119,7 +119,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta result._metadata.clear(); for (meta in metadata) - result._metadata.push(meta); + result._metadata.set(meta.variation, meta); result.variations.clear(); for (vari in variations) @@ -138,7 +138,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta public function getRawMetadata():Array<SongMetadata> { - return _metadata; + return _metadata.values(); } /** @@ -147,10 +147,10 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta */ function populateDifficulties():Void { - if (_metadata == null || _metadata.length == 0) return; + if (_metadata == null || _metadata.size() == 0) return; // Variations may have different artist, time format, generatedBy, etc. - for (metadata in _metadata) + for (metadata in _metadata.values()) { if (metadata == null || metadata.playData == null) continue; diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index d9875e456..d7ba38e2a 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -7,6 +7,7 @@ import flixel.system.FlxAssets.FlxShader; import flixel.util.FlxSort; import funkin.modding.IScriptedClass; import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEventType; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.BaseCharacter; import funkin.play.stage.StageData.StageDataCharacter; @@ -402,7 +403,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass // Add the character to the scene. this.add(character); - ScriptEventDispatcher.callEvent(character, new ScriptEvent(ScriptEvent.ADDED, false)); + ScriptEventDispatcher.callEvent(character, new ScriptEvent(ADDED, false)); #if debug debugIconGroup.add(debugIcon); diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx deleted file mode 100644 index 1014e67c2..000000000 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ /dev/null @@ -1,879 +0,0 @@ -package funkin.ui.debug.charting; - -import haxe.ui.notifications.NotificationType; -import haxe.ui.notifications.NotificationManager; -import funkin.data.song.SongData.SongEventData; -import funkin.data.song.SongData.SongNoteData; -import funkin.data.song.SongDataUtils; - -using Lambda; - -/** - * Actions in the chart editor are backed by the Command pattern - * (see Bob Nystrom's book "Game Programming Patterns" for more info) - * - * To make a function compatible with the undo/redo history, create a new class - * that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())` - */ -interface ChartEditorCommand -{ - /** - * Calling this function should perform the action that this command represents. - * @param state The ChartEditorState to perform the action on. - */ - public function execute(state:ChartEditorState):Void; - - /** - * Calling this function should perform the inverse of the action that this command represents, - * effectively undoing the action. - * @param state The ChartEditorState to undo the action on. - */ - public function undo(state:ChartEditorState):Void; - - /** - * Get a short description of the action (for the UI). - * For example, return `Add Left Note` to display `Undo Add Left Note` in the menu. - */ - public function toString():String; -} - -@:nullSafety -class AddNotesCommand implements ChartEditorCommand -{ - var notes:Array<SongNoteData>; - var appendToSelection:Bool; - - public function new(notes:Array<SongNoteData>, appendToSelection:Bool = false) - { - this.notes = notes; - this.appendToSelection = appendToSelection; - } - - public function execute(state:ChartEditorState):Void - { - for (note in notes) - { - state.currentSongChartNoteData.push(note); - } - - if (appendToSelection) - { - state.currentNoteSelection = state.currentNoteSelection.concat(notes); - } - else - { - state.currentNoteSelection = notes; - state.currentEventSelection = []; - } - - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteLay')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function undo(state:ChartEditorState):Void - { - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); - state.currentNoteSelection = []; - state.currentEventSelection = []; - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function toString():String - { - if (notes.length == 1) - { - var dir:String = notes[0].getDirectionName(); - return 'Add $dir Note'; - } - - return 'Add ${notes.length} Notes'; - } -} - -@:nullSafety -class RemoveNotesCommand implements ChartEditorCommand -{ - var notes:Array<SongNoteData>; - - public function new(notes:Array<SongNoteData>) - { - this.notes = notes; - } - - public function execute(state:ChartEditorState):Void - { - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); - state.currentNoteSelection = []; - state.currentEventSelection = []; - - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function undo(state:ChartEditorState):Void - { - for (note in notes) - { - state.currentSongChartNoteData.push(note); - } - state.currentNoteSelection = notes; - state.currentEventSelection = []; - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function toString():String - { - if (notes.length == 1 && notes[0] != null) - { - var dir:String = notes[0].getDirectionName(); - return 'Remove $dir Note'; - } - - return 'Remove ${notes.length} Notes'; - } -} - -/** - * Appends one or more items to the selection. - */ -@:nullSafety -class SelectItemsCommand implements ChartEditorCommand -{ - var notes:Array<SongNoteData>; - var events:Array<SongEventData>; - - public function new(notes:Array<SongNoteData>, events:Array<SongEventData>) - { - this.notes = notes; - this.events = events; - } - - public function execute(state:ChartEditorState):Void - { - for (note in this.notes) - { - state.currentNoteSelection.push(note); - } - - for (event in this.events) - { - state.currentEventSelection.push(event); - } - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function undo(state:ChartEditorState):Void - { - state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes); - state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events); - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function toString():String - { - var len:Int = notes.length + events.length; - - if (notes.length == 0) - { - if (events.length == 1) - { - return 'Select Event'; - } - else - { - return 'Select ${events.length} Events'; - } - } - else if (events.length == 0) - { - if (notes.length == 1) - { - return 'Select Note'; - } - else - { - return 'Select ${notes.length} Notes'; - } - } - - return 'Select ${len} Items'; - } -} - -@:nullSafety -class AddEventsCommand implements ChartEditorCommand -{ - var events:Array<SongEventData>; - var appendToSelection:Bool; - - public function new(events:Array<SongEventData>, appendToSelection:Bool = false) - { - this.events = events; - this.appendToSelection = appendToSelection; - } - - public function execute(state:ChartEditorState):Void - { - for (event in events) - { - state.currentSongChartEventData.push(event); - } - - if (appendToSelection) - { - state.currentEventSelection = state.currentEventSelection.concat(events); - } - else - { - state.currentNoteSelection = []; - state.currentEventSelection = events; - } - - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteLay')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function undo(state:ChartEditorState):Void - { - state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); - - state.currentNoteSelection = []; - state.currentEventSelection = []; - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function toString():String - { - var len:Int = events.length; - return 'Add $len Events'; - } -} - -@:nullSafety -class RemoveEventsCommand implements ChartEditorCommand -{ - var events:Array<SongEventData>; - - public function new(events:Array<SongEventData>) - { - this.events = events; - } - - public function execute(state:ChartEditorState):Void - { - state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); - state.currentEventSelection = []; - - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function undo(state:ChartEditorState):Void - { - for (event in events) - { - state.currentSongChartEventData.push(event); - } - state.currentEventSelection = events; - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function toString():String - { - if (events.length == 1 && events[0] != null) - { - return 'Remove Event'; - } - - return 'Remove ${events.length} Events'; - } -} - -@:nullSafety -class RemoveItemsCommand implements ChartEditorCommand -{ - var notes:Array<SongNoteData>; - var events:Array<SongEventData>; - - public function new(notes:Array<SongNoteData>, events:Array<SongEventData>) - { - this.notes = notes; - this.events = events; - } - - public function execute(state:ChartEditorState):Void - { - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); - state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); - - state.currentNoteSelection = []; - state.currentEventSelection = []; - - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function undo(state:ChartEditorState):Void - { - for (note in notes) - { - state.currentSongChartNoteData.push(note); - } - - for (event in events) - { - state.currentSongChartEventData.push(event); - } - - state.currentNoteSelection = notes; - state.currentEventSelection = events; - - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo')); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function toString():String - { - return 'Remove ${notes.length + events.length} Items'; - } -} - -@:nullSafety -class SwitchDifficultyCommand implements ChartEditorCommand -{ - var prevDifficulty:String; - var newDifficulty:String; - var prevVariation:String; - var newVariation:String; - - public function new(prevDifficulty:String, newDifficulty:String, prevVariation:String, newVariation:String) - { - this.prevDifficulty = prevDifficulty; - this.newDifficulty = newDifficulty; - this.prevVariation = prevVariation; - this.newVariation = newVariation; - } - - public function execute(state:ChartEditorState):Void - { - state.selectedVariation = newVariation != null ? newVariation : prevVariation; - state.selectedDifficulty = newDifficulty != null ? newDifficulty : prevDifficulty; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function undo(state:ChartEditorState):Void - { - state.selectedVariation = prevVariation != null ? prevVariation : newVariation; - state.selectedDifficulty = prevDifficulty != null ? prevDifficulty : newDifficulty; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function toString():String - { - return 'Switch Difficulty'; - } -} - -@:nullSafety -class DeselectItemsCommand implements ChartEditorCommand -{ - var notes:Array<SongNoteData>; - var events:Array<SongEventData>; - - public function new(notes:Array<SongNoteData>, events:Array<SongEventData>) - { - this.notes = notes; - this.events = events; - } - - public function execute(state:ChartEditorState):Void - { - state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes); - state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events); - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function undo(state:ChartEditorState):Void - { - for (note in this.notes) - { - state.currentNoteSelection.push(note); - } - - for (event in this.events) - { - state.currentEventSelection.push(event); - } - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function toString():String - { - var noteCount = notes.length + events.length; - - if (noteCount == 1) - { - var dir:String = notes[0].getDirectionName(); - return 'Deselect $dir Items'; - } - - return 'Deselect ${noteCount} Items'; - } -} - -/** - * Sets the selection rather than appends it. - * Deselects any notes that are not in the new selection. - */ -@:nullSafety -class SetItemSelectionCommand implements ChartEditorCommand -{ - var notes:Array<SongNoteData>; - var events:Array<SongEventData>; - var previousNoteSelection:Array<SongNoteData>; - var previousEventSelection:Array<SongEventData>; - - public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>, - previousEventSelection:Array<SongEventData>) - { - this.notes = notes; - this.events = events; - this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; - this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; - } - - public function execute(state:ChartEditorState):Void - { - state.currentNoteSelection = notes; - state.currentEventSelection = events; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function undo(state:ChartEditorState):Void - { - state.currentNoteSelection = previousNoteSelection; - state.currentEventSelection = previousEventSelection; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function toString():String - { - return 'Select ${notes.length} Items'; - } -} - -@:nullSafety -class SelectAllItemsCommand implements ChartEditorCommand -{ - var previousNoteSelection:Array<SongNoteData>; - var previousEventSelection:Array<SongEventData>; - - public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>) - { - this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; - this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; - } - - public function execute(state:ChartEditorState):Void - { - state.currentNoteSelection = state.currentSongChartNoteData; - state.currentEventSelection = state.currentSongChartEventData; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function undo(state:ChartEditorState):Void - { - state.currentNoteSelection = previousNoteSelection; - state.currentEventSelection = previousEventSelection; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function toString():String - { - return 'Select All Items'; - } -} - -@:nullSafety -class InvertSelectedItemsCommand implements ChartEditorCommand -{ - var previousNoteSelection:Array<SongNoteData>; - var previousEventSelection:Array<SongEventData>; - - public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>) - { - this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; - this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; - } - - public function execute(state:ChartEditorState):Void - { - state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection); - state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection); - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function undo(state:ChartEditorState):Void - { - state.currentNoteSelection = previousNoteSelection; - state.currentEventSelection = previousEventSelection; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function toString():String - { - return 'Invert Selected Items'; - } -} - -@:nullSafety -class DeselectAllItemsCommand implements ChartEditorCommand -{ - var previousNoteSelection:Array<SongNoteData>; - var previousEventSelection:Array<SongEventData>; - - public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>) - { - this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; - this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; - } - - public function execute(state:ChartEditorState):Void - { - state.currentNoteSelection = []; - state.currentEventSelection = []; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function undo(state:ChartEditorState):Void - { - state.currentNoteSelection = previousNoteSelection; - state.currentEventSelection = previousEventSelection; - - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - } - - public function toString():String - { - return 'Deselect All Items'; - } -} - -@:nullSafety -class CutItemsCommand implements ChartEditorCommand -{ - var notes:Array<SongNoteData>; - var events:Array<SongEventData>; - - public function new(notes:Array<SongNoteData>, events:Array<SongEventData>) - { - this.notes = notes; - this.events = events; - } - - public function execute(state:ChartEditorState):Void - { - // Copy the notes. - SongDataUtils.writeItemsToClipboard( - { - notes: SongDataUtils.buildNoteClipboard(notes), - events: SongDataUtils.buildEventClipboard(events) - }); - - // Delete the notes. - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); - state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); - state.currentNoteSelection = []; - state.currentEventSelection = []; - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - state.sortChartData(); - } - - public function undo(state:ChartEditorState):Void - { - state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); - state.currentSongChartEventData = state.currentSongChartEventData.concat(events); - - state.currentNoteSelection = notes; - state.currentEventSelection = events; - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - state.sortChartData(); - } - - public function toString():String - { - var len:Int = notes.length + events.length; - - if (notes.length == 0) return 'Cut $len Events to Clipboard'; - else if (events.length == 0) return 'Cut $len Notes to Clipboard'; - else - return 'Cut $len Items to Clipboard'; - } -} - -@:nullSafety -class FlipNotesCommand implements ChartEditorCommand -{ - var notes:Array<SongNoteData> = []; - var flippedNotes:Array<SongNoteData> = []; - - public function new(notes:Array<SongNoteData>) - { - this.notes = notes; - this.flippedNotes = SongDataUtils.flipNotes(notes); - } - - public function execute(state:ChartEditorState):Void - { - // Delete the notes. - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); - - // Add the flipped notes. - state.currentSongChartNoteData = state.currentSongChartNoteData.concat(flippedNotes); - - state.currentNoteSelection = flippedNotes; - state.currentEventSelection = []; - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - state.sortChartData(); - } - - public function undo(state:ChartEditorState):Void - { - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, flippedNotes); - state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); - - state.currentNoteSelection = notes; - state.currentEventSelection = []; - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function toString():String - { - var len:Int = notes.length; - return 'Flip $len Notes'; - } -} - -@:nullSafety -class PasteItemsCommand implements ChartEditorCommand -{ - var targetTimestamp:Float; - // Notes we added with this command, for undo. - var addedNotes:Array<SongNoteData> = []; - var addedEvents:Array<SongEventData> = []; - - public function new(targetTimestamp:Float) - { - this.targetTimestamp = targetTimestamp; - } - - public function execute(state:ChartEditorState):Void - { - var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard(); - - if (currentClipboard.valid != true) - { - #if !mac - NotificationManager.instance.addNotification( - { - title: 'Failed to Paste', - body: 'Could not parse clipboard contents.', - type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME - }); - #end - return; - } - - trace(currentClipboard.notes); - - addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp)); - addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); - - state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); - state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); - state.currentNoteSelection = addedNotes.copy(); - state.currentEventSelection = addedEvents.copy(); - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - - #if !mac - NotificationManager.instance.addNotification( - { - title: 'Paste Successful', - body: 'Successfully pasted clipboard contents.', - type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME - }); - #end - } - - public function undo(state:ChartEditorState):Void - { - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo')); - - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); - state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents); - state.currentNoteSelection = []; - state.currentEventSelection = []; - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function toString():String - { - var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard(); - - var len:Int = currentClipboard.notes.length + currentClipboard.events.length; - - if (currentClipboard.notes.length == 0) return 'Paste $len Events'; - else if (currentClipboard.events.length == 0) return 'Paste $len Notes'; - else - return 'Paste $len Items'; - } -} - -@:nullSafety -class ExtendNoteLengthCommand implements ChartEditorCommand -{ - var note:SongNoteData; - var oldLength:Float; - var newLength:Float; - - public function new(note:SongNoteData, newLength:Float) - { - this.note = note; - this.oldLength = note.length; - this.newLength = newLength; - } - - public function execute(state:ChartEditorState):Void - { - note.length = newLength; - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function undo(state:ChartEditorState):Void - { - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo')); - - note.length = oldLength; - - state.saveDataDirty = true; - state.noteDisplayDirty = true; - state.notePreviewDirty = true; - - state.sortChartData(); - } - - public function toString():String - { - return 'Extend Note Length'; - } -} diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 05173726f..8c18271d9 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,22 +1,14 @@ package funkin.ui.debug.charting; -import funkin.play.stage.StageData; -import funkin.play.character.CharacterData.CharacterDataParser; -import funkin.play.character.CharacterData; -import flixel.system.FlxAssets.FlxSoundAsset; -import flixel.math.FlxMath; -import haxe.ui.components.TextField; -import haxe.ui.components.DropDown; -import haxe.ui.components.NumberStepper; -import haxe.ui.containers.Frame; import flixel.addons.display.FlxSliceSprite; import flixel.addons.display.FlxTiledSprite; +import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxSubState; import flixel.group.FlxSpriteGroup; -import flixel.addons.transition.FlxTransitionableState; import flixel.input.keyboard.FlxKey; +import flixel.math.FlxMath; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.sound.FlxSound; @@ -29,41 +21,59 @@ 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.data.song.SongData.SongChartData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; import funkin.input.Cursor; import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.CharacterData; import funkin.play.HealthIcon; import funkin.play.notes.NoteSprite; -import funkin.play.notes.Strumline; import funkin.play.PlayState; import funkin.play.song.Song; -import funkin.data.song.SongData.SongChartData; -import funkin.data.song.SongRegistry; -import funkin.data.song.SongData.SongEventData; -import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongNoteData; -import funkin.data.song.SongData.SongCharacterData; -import funkin.data.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.play.stage.StageData; +import funkin.ui.debug.charting.commands.AddEventsCommand; +import funkin.ui.debug.charting.commands.AddNotesCommand; +import funkin.ui.debug.charting.commands.ChartEditorCommand; +import funkin.ui.debug.charting.commands.CutItemsCommand; +import funkin.ui.debug.charting.commands.DeselectAllItemsCommand; +import funkin.ui.debug.charting.commands.DeselectItemsCommand; +import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand; +import funkin.ui.debug.charting.commands.FlipNotesCommand; +import funkin.ui.debug.charting.commands.InvertSelectedItemsCommand; +import funkin.ui.debug.charting.commands.MoveEventsCommand; +import funkin.ui.debug.charting.commands.MoveItemsCommand; +import funkin.ui.debug.charting.commands.MoveNotesCommand; +import funkin.ui.debug.charting.commands.PasteItemsCommand; +import funkin.ui.debug.charting.commands.RemoveEventsCommand; +import funkin.ui.debug.charting.commands.RemoveItemsCommand; +import funkin.ui.debug.charting.commands.RemoveNotesCommand; +import funkin.ui.debug.charting.commands.SelectAllItemsCommand; +import funkin.ui.debug.charting.commands.SelectItemsCommand; +import funkin.ui.debug.charting.commands.SetItemSelectionCommand; +import funkin.ui.debug.charting.components.ChartEditorEventSprite; +import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; +import funkin.ui.debug.charting.components.ChartEditorNotePreview; +import funkin.ui.debug.charting.components.ChartEditorNoteSprite; +import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite; 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.DropDown; import haxe.ui.components.Label; +import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; +import haxe.ui.components.TextField; import haxe.ui.containers.dialogs.CollapsibleDialog; +import haxe.ui.containers.Frame; import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; @@ -73,9 +83,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; using Lambda; @@ -88,14 +96,6 @@ using Lambda; * @author MasterEric */ @:nullSafety -// Give other classes access to private instance fields -@:allow(funkin.ui.debug.charting.ChartEditorCommand) -@:allow(funkin.ui.debug.charting.ChartEditorDropdowns) -@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler) -@:allow(funkin.ui.debug.charting.ChartEditorThemeHandler) -@:allow(funkin.ui.debug.charting.ChartEditorAudioHandler) -@:allow(funkin.ui.debug.charting.ChartEditorImportExportHandler) -@:allow(funkin.ui.debug.charting.ChartEditorToolboxHandler) class ChartEditorState extends HaxeUIState { /** @@ -103,129 +103,223 @@ class ChartEditorState extends HaxeUIState */ // ============================== // XML Layouts - static final CHART_EDITOR_LAYOUT:String = Paths.ui('chart-editor/main-view'); + public static final CHART_EDITOR_LAYOUT:String = Paths.ui('chart-editor/main-view'); - 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'); + public static final CHART_EDITOR_NOTIFBAR_LAYOUT:String = Paths.ui('chart-editor/components/notifbar'); + public static final CHART_EDITOR_PLAYBARHEAD_LAYOUT:String = Paths.ui('chart-editor/components/playbar-head'); - 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_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'); + public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); + public static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); + public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); + public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty'); + public static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/player-preview'); + public static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview'); // Validation - static final SUPPORTED_MUSIC_FORMATS:Array<String> = ['ogg']; + public static final SUPPORTED_MUSIC_FORMATS:Array<String> = ['ogg']; + + // Layout /** * The base grid size for the chart editor. */ public static final GRID_SIZE:Int = 40; + /** + * The width of the scroll area. + */ public static final PLAYHEAD_SCROLL_AREA_WIDTH:Int = 12; + /** + * The height of the playhead, in pixels. + */ public static final PLAYHEAD_HEIGHT:Int = Std.int(GRID_SIZE / 8); + /** + * The width of the border between grid squares, where the crosshair changes from "Place Notes" to "Select Notes". + */ public static final GRID_SELECTION_BORDER_WIDTH:Int = 6; + /** + * The height of the menu bar in the layout. + */ + public static final MENU_BAR_HEIGHT:Int = 32; + + /** + * The height of the playbar in the layout. + */ + public static final PLAYBAR_HEIGHT:Int = 48; + + /** + * The amount of padding between the menu bar and the chart grid when fully scrolled up. + */ + public static final GRID_TOP_PAD:Int = 8; + + // Colors + // Background color tint. + public static final CURSOR_COLOR:FlxColor = 0xE0FFFFFF; + public static final PREVIEW_BG_COLOR:FlxColor = 0xFF303030; + public static final PLAYHEAD_SCROLL_AREA_COLOR:FlxColor = 0xFF682B2F; + public static final SPECTROGRAM_COLOR:FlxColor = 0xFFFF0000; + public static final PLAYHEAD_COLOR:FlxColor = 0xC0BD0231; + + // Timings + + /** + * Duration, in seconds, for the scroll easing animation. + */ + public static final SCROLL_EASE_DURATION:Float = 0.2; + + // Other + /** * Number of notes in each player's strumline. */ public static final STRUMLINE_SIZE:Int = 4; - /** - * The height of the menu bar in the layout. - */ - static final MENU_BAR_HEIGHT:Int = 32; - - /** - * The height of the playbar in the layout. - */ - static final PLAYBAR_HEIGHT:Int = 48; - - /** - * Duration to wait before autosaving the chart. - */ - static final AUTOSAVE_TIMER_DELAY:Float = 60.0 * 5.0; - - /** - * The amount of padding between the menu bar and the chart grid when fully scrolled up. - */ - static final GRID_TOP_PAD:Int = 8; - - /** - * Duration, in milliseconds, until toast notifications are automatically hidden. - */ - static final NOTIFICATION_DISMISS_TIME:Int = 5000; - - /** - * Duration, in seconds, for the scroll easing animation. - */ - static final SCROLL_EASE_DURATION:Float = 0.2; - - // UI Element Colors - // Background color tint. - static final CURSOR_COLOR:FlxColor = 0xE0FFFFFF; - static final PREVIEW_BG_COLOR:FlxColor = 0xFF303030; - static final PLAYHEAD_SCROLL_AREA_COLOR:FlxColor = 0xFF682B2F; - static final SPECTROGRAM_COLOR:FlxColor = 0xFFFF0000; - static final PLAYHEAD_COLOR:FlxColor = 0xC0BD0231; - /** * How many pixels far the user needs to move the mouse before the cursor is considered to be dragged rather than clicked. */ - static final DRAG_THRESHOLD:Float = 16.0; + public static final DRAG_THRESHOLD:Float = 16.0; /** - * Types of notes you can snap to. + * Precisions of notes you can snap to. */ - static final SNAP_QUANTS:Array<Int> = [4, 8, 12, 16, 20, 24, 32, 48, 64, 96, 192]; + public static final SNAP_QUANTS:Array<Int> = [4, 8, 12, 16, 20, 24, 32, 48, 64, 96, 192]; - static final BASE_QUANT:Int = 16; + /** + * The default note snapping value. + */ + public static final BASE_QUANT:Int = 16; + + /** + * The index of thet default note snapping value in the `SNAP_QUANTS` array. + */ + public static final BASE_QUANT_INDEX:Int = 3; /** * INSTANCE DATA */ // ============================== + // Song Length /** - * The internal index of what note snapping value is in use. - * Increment to make placement more preceise and decrement to make placement less precise. + * The length of the current instrumental, in milliseconds. */ - var noteSnapQuantIndex:Int = 3; // default is 16 + @:isVar var songLengthInMs(get, set):Float = 0; - /** - * The current note snapping value. - * For example, `32` when snapping to 32nd notes. - */ - public var noteSnapQuant(get, never):Int; - - function get_noteSnapQuant():Int + function get_songLengthInMs():Float { - return SNAP_QUANTS[noteSnapQuantIndex]; + if (songLengthInMs <= 0) return 1000; + return songLengthInMs; + } + + function set_songLengthInMs(value:Float):Float + { + this.songLengthInMs = value; + + // Make sure playhead doesn't go outside the song. + if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs; + + return this.songLengthInMs; } /** - * The ratio of the current note snapping value to the default. - * For example, `32` becomes `0.5` when snapping to 16th notes. + * The length of the current instrumental, converted to steps. + * Dependant on BPM, because the size of a grid square does not change with BPM but the length of a beat does. */ - public var noteSnapRatio(get, never):Float; + var songLengthInSteps(get, set):Float; - function get_noteSnapRatio():Float + function get_songLengthInSteps():Float { - return BASE_QUANT / noteSnapQuant; + return Conductor.getTimeInSteps(songLengthInMs); + } + + function set_songLengthInSteps(value:Float):Float + { + // Getting a reasonable result from setting songLengthInSteps requires that Conductor.mapBPMChanges be called first. + songLengthInMs = Conductor.getStepTimeInMs(value); + return value; } /** - * scrollPosition is the current position in the song, in pixels. + * The length of the current instrumental, in PIXELS. + * Dependant on BPM, because the size of a grid square does not change with BPM but the length of a beat does. + */ + var songLengthInPixels(get, set):Int; + + function get_songLengthInPixels():Int + { + return Std.int(songLengthInSteps * GRID_SIZE); + } + + function set_songLengthInPixels(value:Int):Int + { + songLengthInSteps = value / GRID_SIZE; + return value; + } + + // Scroll Position + + /** + * The relative scroll position in the song, in pixels. * One pixel is 1/40 of 1 step, and 1/160 of 1 beat. */ var scrollPositionInPixels(default, set):Float = -1.0; + function set_scrollPositionInPixels(value:Float):Float + { + if (value < 0) + { + // If we're scrolling up, and we hit the top, + // but the playhead is in the middle, move the playhead up. + if (playheadPositionInPixels > 0) + { + var amount:Float = scrollPositionInPixels - value; + playheadPositionInPixels -= amount; + } + + value = 0; + } + + if (value > songLengthInPixels) value = songLengthInPixels; + + if (value == scrollPositionInPixels) return value; + + // Difference in pixels. + var diff:Float = value - scrollPositionInPixels; + + this.scrollPositionInPixels = value; + + // Move the grid sprite to the correct position. + if (gridTiledSprite != null && gridPlayheadScrollArea != null) + { + if (isViewDownscroll) + { + gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + gridPlayheadScrollArea.y = gridTiledSprite.y; + } + else + { + gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + gridPlayheadScrollArea.y = gridTiledSprite.y; + } + } + + // Move the rendered notes to the correct position. + renderedNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); + renderedHoldNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); + renderedEvents.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); + renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); + // Offset the selection box start position, if we are dragging. + if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; + // Update the note preview viewport box. + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + return this.scrollPositionInPixels; + } + /** - * scrollPosition, converted to steps. + * The relative scroll position in the song, converted to steps. * NOT dependant on BPM, because the size of a grid square does not change with BPM. */ var scrollPositionInSteps(get, set):Float; @@ -242,7 +336,7 @@ class ChartEditorState extends HaxeUIState } /** - * scrollPosition, converted to milliseconds. + * The relative scroll position in the song, converted to milliseconds. * DEPENDANT on BPM, because the duration of a grid square changes with BPM. */ var scrollPositionInMs(get, set):Float; @@ -258,11 +352,13 @@ class ChartEditorState extends HaxeUIState return value; } + // Playhead (on the grid) + /** - * The position of the playhead, in pixels, relative to the scrollPosition. - * 0 means playhead is at the top of the grid. - * 40 means the playhead is 1 grid length below the base position. - * -40 means the playhead is 1 grid length above the base position. + * The position of the playhead, in pixels, relative to the `scrollPositionInPixels`. + * `0` means playhead is at the top of the grid. + * `40` means the playhead is 1 grid length below the base position. + * `-40` means the playhead is 1 grid length above the base position. */ var playheadPositionInPixels(default, set):Float = 0.0; @@ -314,77 +410,7 @@ class ChartEditorState extends HaxeUIState return value; } - /** - * songLength, in milliseconds. - */ - @:isVar var songLengthInMs(get, set):Float = 0; - - function get_songLengthInMs():Float - { - if (songLengthInMs <= 0) return 1000; - return songLengthInMs; - } - - function set_songLengthInMs(value:Float):Float - { - this.songLengthInMs = value; - - // Make sure playhead doesn't go outside the song. - if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs; - - return this.songLengthInMs; - } - - /** - * songLength, converted to steps. - * Dependant on BPM, because the size of a grid square does not change with BPM but the length of a beat does. - */ - var songLengthInSteps(get, set):Float; - - function get_songLengthInSteps():Float - { - return Conductor.getTimeInSteps(songLengthInMs); - } - - function set_songLengthInSteps(value:Float):Float - { - // Getting a reasonable result from setting songLengthInSteps requires that Conductor.mapBPMChanges be called first. - songLengthInMs = Conductor.getStepTimeInMs(value); - return value; - } - - /** - * This is the song's length in PIXELS, same format as scrollPosition. - * Dependant on BPM, because the size of a grid square does not change with BPM but the length of a beat does. - */ - var songLengthInPixels(get, set):Int; - - function get_songLengthInPixels():Int - { - return Std.int(songLengthInSteps * GRID_SIZE); - } - - function set_songLengthInPixels(value:Int):Int - { - songLengthInSteps = value / GRID_SIZE; - return value; - } - - /** - * The current theme used by the editor. - * Dictates the appearance of many UI elements. - * Currently hardcoded to just Light and Dark. - */ - var currentTheme(default, set):ChartEditorTheme = ChartEditorTheme.Light; - - function set_currentTheme(value:ChartEditorTheme):ChartEditorTheme - { - if (value == null || value == currentTheme) return currentTheme; - - currentTheme = value; - ChartEditorThemeHandler.updateTheme(this); - return value; - } + // Playbar (at the bottom) /** * Whether a skip button has been pressed on the playbar, and which one. @@ -404,47 +430,62 @@ class ChartEditorState extends HaxeUIState */ var playbarHeadDraggingWasPlaying:Bool = false; + // Tools Status + /** * The note kind to use for notes being placed in the chart. Defaults to `''`. */ var selectedNoteKind:String = ''; /** - * The note kind to use for notes being placed in the chart. Defaults to `''`. + * The event type to use for events being placed in the chart. Defaults to `''`. */ var selectedEventKind:String = 'FocusCamera'; /** - * The note data as a struct. + * The event data to use for events being placed in the chart. */ var selectedEventData:DynamicAccess<Dynamic> = {}; /** - * Whether to play a metronome sound while the playhead is moving. + * The internal index of what note snapping value is in use. + * Increment to make placement more preceise and decrement to make placement less precise. */ - var isMetronomeEnabled:Bool = true; + var noteSnapQuantIndex:Int = BASE_QUANT_INDEX; /** - * Use the tool window to affect how the user interacts with the program. + * The current note snapping value. + * For example, `32` when snapping to 32nd notes. */ - var currentToolMode:ChartEditorToolMode = ChartEditorToolMode.Select; + var noteSnapQuant(get, never):Int; + + function get_noteSnapQuant():Int + { + return SNAP_QUANTS[noteSnapQuantIndex]; + } /** - * The character sprite in the Player Preview window. - * `null` until accessed. + * The ratio of the current note snapping value to the default. + * For example, `32` becomes `0.5` when snapping to 16th notes. */ - var currentPlayerCharacterPlayer:Null<CharacterPlayer> = null; + var noteSnapRatio(get, never):Float; - /** - * The character sprite in the Opponent Preview window. - * `null` until accessed. - */ - var currentOpponentCharacterPlayer:Null<CharacterPlayer> = null; + function get_noteSnapRatio():Float + { + return BASE_QUANT / noteSnapQuant; + } /** * The currently selected live input style. */ - var currentLiveInputStyle:LiveInputStyle = LiveInputStyle.None; + var currentLiveInputStyle:ChartEditorLiveInputStyle = None; + + /** + * If true, playtesting a chart will skip to the current playhead position. + */ + var playtestStartTime:Bool = false; + + // Visuals /** * Whether the current view is in downscroll mode. @@ -467,33 +508,38 @@ class ChartEditorState extends HaxeUIState } /** - * If true, playtesting a chart will skip to the current playhead position. + * The current theme used by the editor. + * Dictates the appearance of many UI elements. + * Currently hardcoded to just Light and Dark. */ - var playtestStartTime:Bool = false; + var currentTheme(default, set):ChartEditorTheme = ChartEditorTheme.Light; - /** - * Whether hitsounds are enabled for at least one character. - */ - var hitsoundsEnabled(get, never):Bool; - - function get_hitsoundsEnabled():Bool + function set_currentTheme(value:ChartEditorTheme):ChartEditorTheme { - return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent; + if (value == null || value == currentTheme) return currentTheme; + + currentTheme = value; + this.updateTheme(); + return value; } /** - * Whether hitsounds are enabled for the player. + * The character sprite in the Player Preview window. + * `null` until accessed. */ - var hitsoundsEnabledPlayer:Bool = true; + var currentPlayerCharacterPlayer:Null<CharacterPlayer> = null; /** - * Whether hitsounds are enabled for the opponent. + * The character sprite in the Opponent Preview window. + * `null` until accessed. */ - var hitsoundsEnabledOpponent:Bool = true; + var currentOpponentCharacterPlayer:Null<CharacterPlayer> = null; + + // HaxeUI /** * Whether the user's mouse cursor is hovering over a SOLID component of the HaxeUI. - * If so, ignore mouse events underneath. + * If so, ignore mouse events underneath as well as certain key events. */ var isCursorOverHaxeUI(get, never):Bool; @@ -513,107 +559,143 @@ class ChartEditorState extends HaxeUIState /** * Set by ChartEditorDialogHandler, used to prevent background interaction while the dialog is open. */ - public var isHaxeUIDialogOpen:Bool = false; + var isHaxeUIDialogOpen:Bool = false; /** - * The variation ID for the difficulty which is currently being edited. + * The Dialog components representing the currently available tool windows. + * Dialogs are retained here even when collapsed or hidden. */ - var selectedVariation(default, set):String = Constants.DEFAULT_VARIATION; + var activeToolboxes:Map<String, CollapsibleDialog> = new Map<String, CollapsibleDialog>(); + + // Audio /** - * Setter called when we are switching variations. - * We will likely need to switch instrumentals as well. + * Whether to play a metronome sound while the playhead is moving. */ - function set_selectedVariation(value:String):String - { - // Don't update if we're already on the variation. - if (selectedVariation == value) return selectedVariation; - selectedVariation = value; - - // Make sure view is updated when the variation changes. - noteDisplayDirty = true; - notePreviewDirty = true; - notePreviewViewportBoundsDirty = true; - - switchToCurrentInstrumental(); - - return selectedVariation; - } + var isMetronomeEnabled:Bool = true; /** - * The difficulty ID for the difficulty which is currently being edited. + * Whether hitsounds are enabled for the player. */ - var selectedDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; - - function set_selectedDifficulty(value:String):String - { - selectedDifficulty = value; - - // Make sure view is updated when the difficulty changes. - noteDisplayDirty = true; - notePreviewDirty = true; - notePreviewViewportBoundsDirty = true; - - // Make sure the difficulty we selected is in the list of difficulties. - currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty); - - return selectedDifficulty; - } + var hitsoundsEnabledPlayer:Bool = true; /** - * The instrumental ID which is currently selected. + * Whether hitsounds are enabled for the opponent. */ - var currentInstrumentalId(get, set):String; - - function get_currentInstrumentalId():String - { - var instId:Null<String> = currentSongMetadata.playData.characters.instrumental; - if (instId == null || instId == '') instId = (selectedVariation == Constants.DEFAULT_VARIATION) ? '' : selectedVariation; - return instId; - } - - function set_currentInstrumentalId(value:String):String - { - return currentSongMetadata.playData.characters.instrumental = value; - } + var hitsoundsEnabledOpponent:Bool = true; /** - * The character ID for the character which is currently selected. + * Whether hitsounds are enabled for at least one character. */ - var selectedCharacter(default, set):String = Constants.DEFAULT_CHARACTER; + var hitsoundsEnabled(get, never):Bool; - function set_selectedCharacter(value:String):String + function get_hitsoundsEnabled():Bool { - selectedCharacter = value; - - // Make sure view is updated when the character changes. - noteDisplayDirty = true; - notePreviewDirty = true; - notePreviewViewportBoundsDirty = true; - - return selectedCharacter; + return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent; } + // Auto-save + /** - * Whether the user is currently in Pattern Mode. - * This overrides the chart editor's normal behavior. + * A timer used to auto-save the chart after a period of inactivity. */ - var isInPatternMode(default, set):Bool = false; + var autoSaveTimer:Null<FlxTimer> = null; - function set_isInPatternMode(value:Bool):Bool - { - isInPatternMode = value; + // Scrolling - // Make sure view is updated when we change modes. - noteDisplayDirty = true; - notePreviewDirty = true; - notePreviewViewportBoundsDirty = true; - this.scrollPositionInPixels = 0; + /** + * Whether the user's last mouse click was on the playhead scroll area. + */ + var gridPlayheadScrollAreaPressed:Bool = false; - return isInPatternMode; - } + /** + * Where the user's last mouse click was on the note preview scroll area. + * `null` if the user isn't clicking on the note preview. + */ + var notePreviewScrollAreaStartPos:Null<FlxPoint> = null; - var currentPattern:String = ''; + /** + * The current process that is lerping the scroll position. + * Used to cancel the previous lerp if the user scrolls again. + */ + var currentScrollEase:Null<VarTween>; + + // Note Placement + + /** + * The SongNoteData which is currently being placed. + * `null` if the user isn't currently placing a note. + * As the user drags, we will update this note's sustain length, and finalize the note when they release. + */ + var currentPlaceNoteData:Null<SongNoteData> = null; + + // Note Movement + + /** + * The note sprite we are currently moving, if any. + */ + var dragTargetNote:Null<ChartEditorNoteSprite> = null; + + /** + * The song event sprite we are currently moving, if any. + */ + var dragTargetEvent:Null<ChartEditorEventSprite> = null; + + /** + * The amount of vertical steps the note sprite has moved by since the user started dragging. + */ + var dragTargetCurrentStep:Float = 0; + + /** + * The amount of horizontal columns the note sprite has moved by since the user started dragging. + */ + var dragTargetCurrentColumn:Int = 0; + + // Hold Note Dragging + + /** + * The current length of the hold note we are dragging, in steps. + * Play a sound when this value changes. + */ + var dragLengthCurrent:Float = 0; + + /** + * Flip-flop to alternate between two stretching sounds. + */ + var stretchySounds:Bool = false; + + // Selection + + /** + * The notes which are currently in the user's selection. + */ + var currentNoteSelection:Array<SongNoteData> = []; + + /** + * The events which are currently in the user's selection. + */ + var currentEventSelection:Array<SongEventData> = []; + + /** + * The position where the user clicked to start a selection. + * `null` if the user isn't currently selecting anything. + * The selection box extends from this point to the current mouse position. + */ + var selectionBoxStartPos:Null<FlxPoint> = null; + + // History + + /** + * The list of command previously performed. Used for undoing previous actions. + */ + var undoHistory:Array<ChartEditorCommand> = []; + + /** + * The list of commands that have been undone. Used for redoing previous actions. + */ + var redoHistory:Array<ChartEditorCommand> = []; + + // Dirty Flags /** * Whether the note display render group has been modified and needs to be updated. @@ -652,7 +734,7 @@ class ChartEditorState extends HaxeUIState if (value) { // Start the auto-save timer. - autoSaveTimer = new FlxTimer().start(AUTOSAVE_TIMER_DELAY, (_) -> autoSave()); + autoSaveTimer = new FlxTimer().start(Constants.AUTOSAVE_TIMER_DELAY_SEC, (_) -> autoSave()); } else { @@ -668,11 +750,6 @@ class ChartEditorState extends HaxeUIState return saveDataDirty = value; } - /** - * A timer used to auto-save the chart after a period of inactivity. - */ - var autoSaveTimer:Null<FlxTimer> = null; - /** * Whether the difficulty tree view in the toolbox has been modified and needs to be updated. * This happens when we add/remove difficulties. @@ -697,17 +774,12 @@ class ChartEditorState extends HaxeUIState */ var opponentPreviewDirty:Bool = true; - var isInPlaytestMode:Bool = false; - /** - * The list of command previously performed. Used for undoing previous actions. + * Whether the undo/redo histories have changed since the last time the UI was updated. */ - var undoHistory:Array<ChartEditorCommand> = []; + var commandHistoryDirty:Bool = true; - /** - * The list of commands that have been undone. Used for redoing previous actions. - */ - var redoHistory:Array<ChartEditorCommand> = []; + // Input /** * Handler used to track how long the user has been holding the undo keybind. @@ -749,52 +821,6 @@ class ChartEditorState extends HaxeUIState */ var pageDownKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEDOWN); - /** - * Whether the undo/redo histories have changed since the last time the UI was updated. - */ - var commandHistoryDirty:Bool = true; - - /** - * The notes which are currently in the user's selection. - */ - var currentNoteSelection:Array<SongNoteData> = []; - - /** - * The events which are currently in the user's selection. - */ - var currentEventSelection:Array<SongEventData> = []; - - /** - * The position where the user clicked to start a selection. - * `null` if the user isn't currently selecting anything. - * The selection box extends from this point to the current mouse position. - */ - var selectionBoxStartPos:Null<FlxPoint> = null; - - /** - * Whether the user's last mouse click was on the playhead scroll area. - */ - var gridPlayheadScrollAreaPressed:Bool = false; - - /** - * Where the user's last mouse click was on the note preview scroll area. - * `null` if the user isn't clicking on the note preview. - */ - var notePreviewScrollAreaStartPos:Null<FlxPoint> = null; - - /** - * The SongNoteData which is currently being placed. - * `null` if the user isn't currently placing a note. - * As the user drags, we will update this note's sustain length. - */ - var currentPlaceNoteData:Null<SongNoteData> = null; - - /** - * The Dialog components representing the currently available tool windows. - * Dialogs are retained here even when collapsed or hidden. - */ - var activeToolboxes:Map<String, CollapsibleDialog> = new Map<String, CollapsibleDialog>(); - /** * AUDIO AND SOUND DATA */ @@ -803,7 +829,7 @@ class ChartEditorState extends HaxeUIState /** * The chill audio track that plays when you open the Chart Editor. */ - public var welcomeMusic:FlxSound = new FlxSound(); + var welcomeMusic:FlxSound = new FlxSound(); /** * The audio track for the instrumental. @@ -1007,7 +1033,7 @@ class ChartEditorState extends HaxeUIState return currentSongChartData.events = value; } - public var currentSongNoteStyle(get, set):String; + var currentSongNoteStyle(get, set):String; function get_currentSongNoteStyle():String { @@ -1083,10 +1109,67 @@ class ChartEditorState extends HaxeUIState } /** - * SIGNALS + * The variation ID for the difficulty which is currently being edited. */ - // ============================== - // public var onDifficultyChange(default, never):FlxTypedSignal<ChartEditorState->Void> = new FlxTypedSignal<ChartEditorState->Void>(); + var selectedVariation(default, set):String = Constants.DEFAULT_VARIATION; + + /** + * Setter called when we are switching variations. + * We will likely need to switch instrumentals as well. + */ + function set_selectedVariation(value:String):String + { + // Don't update if we're already on the variation. + if (selectedVariation == value) return selectedVariation; + selectedVariation = value; + + // Make sure view is updated when the variation changes. + noteDisplayDirty = true; + notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; + + switchToCurrentInstrumental(); + + return selectedVariation; + } + + /** + * The difficulty ID for the difficulty which is currently being edited. + */ + var selectedDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; + + function set_selectedDifficulty(value:String):String + { + selectedDifficulty = value; + + // Make sure view is updated when the difficulty changes. + noteDisplayDirty = true; + notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; + + // Make sure the difficulty we selected is in the list of difficulties. + currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty); + + return selectedDifficulty; + } + + /** + * The instrumental ID which is currently selected. + */ + var currentInstrumentalId(get, set):String; + + function get_currentInstrumentalId():String + { + var instId:Null<String> = currentSongMetadata.playData.characters.instrumental; + if (instId == null || instId == '') instId = (selectedVariation == Constants.DEFAULT_VARIATION) ? '' : selectedVariation; + return instId; + } + + function set_currentInstrumentalId(value:String):String + { + return currentSongMetadata.playData.characters.instrumental = value; + } + /** * RENDER OBJECTS */ @@ -1123,6 +1206,9 @@ class ChartEditorState extends HaxeUIState */ var gridPlayhead:FlxSpriteGroup = new FlxSpriteGroup(); + /** + * The sprite for the scroll area under + */ var gridPlayheadScrollArea:Null<FlxSprite> = null; /** @@ -1203,12 +1289,6 @@ class ChartEditorState extends HaxeUIState */ var playbarNoteSnap:Null<Label> = null; - /** - * The current process that is lerping the scroll position. - * Used to cancel the previous lerp if the user scrolls again. - */ - var currentScrollEase:Null<VarTween>; - /** * The sprite group containing the note graphics. * Only displays a subset of the data from `currentSongChartNoteData`, @@ -1230,7 +1310,12 @@ class ChartEditorState extends HaxeUIState */ var renderedEvents:FlxTypedSpriteGroup<ChartEditorEventSprite> = new FlxTypedSpriteGroup<ChartEditorEventSprite>(); - var renderedSelectionSquares:FlxTypedSpriteGroup<FlxSprite> = new FlxTypedSpriteGroup<FlxSprite>(); + var renderedSelectionSquares:FlxTypedSpriteGroup<ChartEditorSelectionSquareSprite> = new FlxTypedSpriteGroup<ChartEditorSelectionSquareSprite>(); + + /** + * LIFE CYCLE FUNCTIONS + */ + // ============================== public function new() { @@ -1238,6 +1323,44 @@ class ChartEditorState extends HaxeUIState super(CHART_EDITOR_LAYOUT); } + public override function dispatchEvent(event:ScriptEvent):Void + { + super.dispatchEvent(event); + + // We can't use the ScriptedEventDispatcher with currentCharPlayer because we can't use the IScriptedClass interface on it. + if (currentPlayerCharacterPlayer != null) + { + switch (event.type) + { + case UPDATE: + currentPlayerCharacterPlayer.onUpdate(cast event); + case SONG_BEAT_HIT: + currentPlayerCharacterPlayer.onBeatHit(cast event); + case SONG_STEP_HIT: + currentPlayerCharacterPlayer.onStepHit(cast event); + case NOTE_HIT: + currentPlayerCharacterPlayer.onNoteHit(cast event); + default: // Continue + } + } + + if (currentOpponentCharacterPlayer != null) + { + switch (event.type) + { + case UPDATE: + currentOpponentCharacterPlayer.onUpdate(cast event); + case SONG_BEAT_HIT: + currentOpponentCharacterPlayer.onBeatHit(cast event); + case SONG_STEP_HIT: + currentOpponentCharacterPlayer.onStepHit(cast event); + case NOTE_HIT: + currentOpponentCharacterPlayer.onNoteHit(cast event); + default: // Continue + } + } + } + override function create():Void { // super.create() must be called first, the HaxeUI components get created here. @@ -1251,7 +1374,7 @@ class ChartEditorState extends HaxeUIState fixCamera(); // Get rid of any music from the previous state. - FlxG.sound.music.stop(); + if (FlxG.sound.music != null) FlxG.sound.music.stop(); // Play the welcome music. setupWelcomeMusic(); @@ -1260,7 +1383,7 @@ class ChartEditorState extends HaxeUIState buildBackground(); - ChartEditorThemeHandler.updateTheme(this); + this.updateTheme(); buildGrid(); // buildSpectrogram(audioInstTrack); @@ -1277,7 +1400,20 @@ class ChartEditorState extends HaxeUIState refresh(); - ChartEditorDialogHandler.openWelcomeDialog(this, false); + this.openWelcomeDialog(false); + } + + override function destroy():Void + { + super.destroy(); + + cleanupAutoSave(); + + // Hide the mouse cursor on other states. + Cursor.hide(); + + @:privateAccess + ChartEditorNoteSprite.noteFrameCollection = null; } function setupWelcomeMusic() @@ -1288,13 +1424,13 @@ class ChartEditorState extends HaxeUIState // fadeInWelcomeMusic(); } - public function fadeInWelcomeMusic():Void + function fadeInWelcomeMusic():Void { this.welcomeMusic.play(); this.welcomeMusic.fadeIn(4, 0, 1.0); } - public function stopWelcomeMusic():Void + function stopWelcomeMusic():Void { // this.welcomeMusic.fadeOut(4, 0); this.welcomeMusic.pause(); @@ -1406,6 +1542,23 @@ class ChartEditorState extends HaxeUIState healthIconBF.zIndex = 30; } + function buildNotePreview():Void + { + var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - PLAYBAR_HEIGHT - GRID_TOP_PAD - GRID_TOP_PAD; + notePreview = new ChartEditorNotePreview(height); + notePreview.x = 350; + notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; + add(notePreview); + + if (notePreviewViewport == null) throw 'ERROR: Tried to build note preview, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().'; + + notePreviewViewport.scrollFactor.set(0, 0); + add(notePreviewViewport); + notePreviewViewport.zIndex = 30; + + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + } + function buildSelectionBox():Void { if (selectionBoxSprite == null) throw 'ERROR: Tried to build selection box, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().'; @@ -1438,23 +1591,6 @@ class ChartEditorState extends HaxeUIState } } - function buildNotePreview():Void - { - var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - PLAYBAR_HEIGHT - GRID_TOP_PAD - GRID_TOP_PAD; - notePreview = new ChartEditorNotePreview(height); - notePreview.x = 350; - notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; - add(notePreview); - - if (notePreviewViewport == null) throw 'ERROR: Tried to build note preview, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().'; - - notePreviewViewport.scrollFactor.set(0, 0); - add(notePreviewViewport); - notePreviewViewport.zIndex = 30; - - setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); - } - function calculateNotePreviewViewportBounds():FlxRect { var bounds:FlxRect = new FlxRect(); @@ -1493,12 +1629,6 @@ class ChartEditorState extends HaxeUIState return bounds; } - public function switchToCurrentInstrumental():Void - { - ChartEditorAudioHandler.switchToInstrumental(this, currentInstrumentalId, currentSongMetadata.playData.characters.player, - currentSongMetadata.playData.characters.opponent); - } - function setNotePreviewViewportBounds(bounds:FlxRect = null):Void { if (notePreviewViewport == null) @@ -1635,11 +1765,11 @@ class ChartEditorState extends HaxeUIState // Add functionality to the menu items. - addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); - addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseWizard(this, true)); - addUIClickListener('menubarItemSaveChartAs', _ -> ChartEditorImportExportHandler.exportAllSongData(this)); - addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true)); - addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true)); + addUIClickListener('menubarItemNewChart', _ -> this.openWelcomeDialog(true)); + addUIClickListener('menubarItemOpenChart', _ -> this.openBrowseWizard(true)); + addUIClickListener('menubarItemSaveChartAs', _ -> this.exportAllSongData()); + addUIClickListener('menubarItemLoadInst', _ -> this.openUploadInstDialog(true)); + addUIClickListener('menubarItemImportChart', _ -> this.openImportChartDialog('legacy', true)); addUIClickListener('menubarItemExit', _ -> quitChartEditor()); addUIClickListener('menubarItemUndo', _ -> undoLastCommand()); @@ -1700,6 +1830,8 @@ class ChartEditorState extends HaxeUIState } }); + addUIClickListener('menubarItemFlipNotes', _ -> performCommand(new FlipNotesCommand(currentNoteSelection))); + addUIClickListener('menubarItemSelectAll', _ -> performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection))); addUIClickListener('menubarItemSelectInverse', _ -> performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection))); @@ -1722,10 +1854,10 @@ class ChartEditorState extends HaxeUIState currentLiveInputStyle = WASD; }); - addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this)); - addUIClickListener('menubarItemWelcomeDialog', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); + addUIClickListener('menubarItemAbout', _ -> this.openAboutDialog()); + addUIClickListener('menubarItemWelcomeDialog', _ -> this.openWelcomeDialog(true)); - addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this)); + addUIClickListener('menubarItemUserGuide', _ -> this.openUserGuideDialog()); addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value); setUICheckboxSelected('menubarItemDownscroll', isViewDownscroll); @@ -1748,8 +1880,8 @@ class ChartEditorState extends HaxeUIState addUIClickListener('menubarItemPlayPause', _ -> toggleAudioPlayback()); - addUIClickListener('menubarItemLoadInstrumental', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true)); - addUIClickListener('menubarItemLoadVocals', _ -> ChartEditorDialogHandler.openUploadVocalsDialog(this, true)); + addUIClickListener('menubarItemLoadInstrumental', _ -> this.openUploadInstDialog(true)); + addUIClickListener('menubarItemLoadVocals', _ -> this.openUploadVocalsDialog(true)); addUIChangeListener('menubarItemMetronomeEnabled', event -> isMetronomeEnabled = event.value); setUICheckboxSelected('menubarItemMetronomeEnabled', isMetronomeEnabled); @@ -1795,18 +1927,12 @@ class ChartEditorState extends HaxeUIState }); } - addUIChangeListener('menubarItemToggleToolboxDifficulty', - event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value)); - addUIChangeListener('menubarItemToggleToolboxMetadata', - event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value)); - addUIChangeListener('menubarItemToggleToolboxNotes', - event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value)); - addUIChangeListener('menubarItemToggleToolboxEvents', - event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value)); - addUIChangeListener('menubarItemToggleToolboxPlayerPreview', - event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value)); - addUIChangeListener('menubarItemToggleToolboxOpponentPreview', - event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value)); + addUIChangeListener('menubarItemToggleToolboxDifficulty', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value)); + addUIChangeListener('menubarItemToggleToolboxMetadata', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value)); + addUIChangeListener('menubarItemToggleToolboxNotes', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value)); + addUIChangeListener('menubarItemToggleToolboxEvents', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value)); + addUIChangeListener('menubarItemToggleToolboxPlayerPreview', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value)); + addUIChangeListener('menubarItemToggleToolboxOpponentPreview', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value)); // TODO: Pass specific HaxeUI components to add context menus to them. registerContextMenu(null, Paths.ui('chart-editor/context/test')); @@ -1838,38 +1964,8 @@ class ChartEditorState extends HaxeUIState } /** - * Called after 5 minutes without saving. + * UPDATE FUNCTIONS */ - function autoSave():Void - { - saveDataDirty = false; - - // Auto-save the chart. - - #if html5 - // Auto-save to local storage. - #else - // Auto-save to temp file. - ChartEditorImportExportHandler.exportAllSongData(this, true); - #end - } - - function onWindowClose(exitCode:Int):Void - { - trace('Window exited with exit code: $exitCode'); - trace('Should save chart? $saveDataDirty'); - - if (saveDataDirty) - { - ChartEditorImportExportHandler.exportAllSongData(this, true); - } - } - - function cleanupAutoSave():Void - { - WindowUtil.windowExit.remove(onWindowClose); - } - public override function update(elapsed:Float):Void { // Override F4 behavior to include the autosave. @@ -1909,12 +2005,6 @@ class ChartEditorState extends HaxeUIState #end } - function handleQuickWatch():Void - { - FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); - FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels); - } - /** * Beat hit while the song is playing. */ @@ -1952,9 +2042,451 @@ class ChartEditorState extends HaxeUIState return true; } + /** + * UPDATE HANDLERS + */ + // ==================== + + /** + * Handle syncronizing the conductor with the music playback. + */ + function handleMusicPlayback():Void + { + if (audioInstTrack != null && audioInstTrack.playing) + { + if (FlxG.mouse.pressedMiddle) + { + // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! + + var oldStepTime:Float = Conductor.currentStepTime; + 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; + } + var diffStepTime:Float = Conductor.currentStepTime - oldStepTime; + + // Move the playhead. + playheadPositionInPixels += diffStepTime * GRID_SIZE; + + // We don't move the song to scroll position, or update the note sprites. + } + else + { + // Else, move the entire view. + 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; + } + + // We need time in fractional steps here to allow the song to actually play. + // Also account for a potentially offset playhead. + scrollPositionInPixels = Conductor.currentStepTime * GRID_SIZE - playheadPositionInPixels; + + // DO NOT move song to scroll position here specifically. + + // We need to update the note sprites. + noteDisplayDirty = true; + + // Update the note preview viewport box. + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + } + } + + if (FlxG.keys.justPressed.SPACE && !isHaxeUIDialogOpen) + { + toggleAudioPlayback(); + } + } + + /** + * Handle the playback of hitsounds. + */ + function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void + { + if (!hitsoundsEnabled) return; + + // Assume notes are sorted by time. + for (noteData in currentSongChartNoteData) + { + // Check for notes between the old and new song positions. + + if (noteData.time < oldSongPosition) // Note is in the past. + continue; + + if (noteData.time > newSongPosition) // Note is in the future. + return; // Assume all notes are also in the future. + + // Note was just hit. + + // Character preview. + + // NoteScriptEvent takes a sprite, ehe. Need to rework that. + var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault()); + tempNote.noteData = noteData; + tempNote.scrollFactor.set(0, 0); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, tempNote, 1, true); + dispatchEvent(event); + + // Calling event.cancelEvent() skips all the other logic! Neat! + if (event.eventCanceled) continue; + + // Hitsounds. + switch (noteData.getStrumlineIndex()) + { + case 0: // Player + if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer')); + case 1: // Opponent + if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent')); + } + } + } + + /** + * Handle using `renderedNotes` to display notes from `currentSongChartNoteData`. + */ + function handleNoteDisplay():Void + { + if (noteDisplayDirty) + { + noteDisplayDirty = false; + + // Update for whether downscroll is enabled. + renderedNotes.flipX = (isViewDownscroll); + + // Calculate the top and bottom of the view area. + var viewAreaTopPixels:Float = MENU_BAR_HEIGHT; + var visibleGridHeightPixels:Float = FlxG.height - MENU_BAR_HEIGHT - PLAYBAR_HEIGHT; // The area underneath the menu bar and playbar is not visible. + var viewAreaBottomPixels:Float = viewAreaTopPixels + visibleGridHeightPixels; + + // Remove notes that are no longer visible and list the ones that are. + var displayedNoteData:Array<SongNoteData> = []; + for (noteSprite in renderedNotes.members) + { + if (noteSprite == null || noteSprite.noteData == null || !noteSprite.exists || !noteSprite.visible) continue; + + // Resolve an issue where dragging an event too far would cause it to be hidden. + var isSelectedAndDragged = currentNoteSelection.fastContains(noteSprite.noteData) && (dragTargetCurrentStep != 0); + + if ((noteSprite.isNoteVisible(viewAreaBottomPixels, viewAreaTopPixels) + && currentSongChartNoteData.fastContains(noteSprite.noteData)) + || isSelectedAndDragged) + { + // Note is already displayed and should remain displayed. + displayedNoteData.push(noteSprite.noteData); + + // Update the note sprite's position. + noteSprite.updateNotePosition(renderedNotes); + } + else + { + // This sprite is off-screen or was deleted. + // Kill the note sprite and recycle it. + noteSprite.noteData = null; + } + } + // Sort the note data array, using an algorithm that is fast on nearly-sorted data. + // We need this sorted to optimize indexing later. + displayedNoteData.insertionSort(SortUtil.noteDataByTime.bind(FlxSort.ASCENDING)); + + var displayedHoldNoteData:Array<SongNoteData> = []; + for (holdNoteSprite in renderedHoldNotes.members) + { + if (holdNoteSprite == null || holdNoteSprite.noteData == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; + + if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + { + holdNoteSprite.kill(); + } + else if (!currentSongChartNoteData.fastContains(holdNoteSprite.noteData) || holdNoteSprite.noteData.length == 0) + { + // This hold note was deleted. + // Kill the hold note sprite and recycle it. + holdNoteSprite.kill(); + } + else if (displayedHoldNoteData.fastContains(holdNoteSprite.noteData)) + { + // This hold note is a duplicate. + // Kill the hold note sprite and recycle it. + holdNoteSprite.kill(); + } + else + { + displayedHoldNoteData.push(holdNoteSprite.noteData); + // Update the event sprite's position. + holdNoteSprite.updateHoldNotePosition(renderedNotes); + } + } + // Sort the note data array, using an algorithm that is fast on nearly-sorted data. + // We need this sorted to optimize indexing later. + displayedHoldNoteData.insertionSort(SortUtil.noteDataByTime.bind(FlxSort.ASCENDING)); + + // Remove events that are no longer visible and list the ones that are. + var displayedEventData:Array<SongEventData> = []; + for (eventSprite in renderedEvents.members) + { + if (eventSprite == null || eventSprite.eventData == null || !eventSprite.exists || !eventSprite.visible) continue; + + // Resolve an issue where dragging an event too far would cause it to be hidden. + var isSelectedAndDragged = currentEventSelection.fastContains(eventSprite.eventData) && (dragTargetCurrentStep != 0); + + if ((eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD) + && currentSongChartEventData.fastContains(eventSprite.eventData)) + || isSelectedAndDragged) + { + // Event is already displayed and should remain displayed. + displayedEventData.push(eventSprite.eventData); + + // Update the event sprite's position. + eventSprite.updateEventPosition(renderedEvents); + } + else + { + // This event was deleted. + // Kill the event sprite and recycle it. + eventSprite.eventData = null; + } + } + // Sort the note data array, using an algorithm that is fast on nearly-sorted data. + // We need this sorted to optimize indexing later. + displayedEventData.insertionSort(SortUtil.eventDataByTime.bind(FlxSort.ASCENDING)); + + // Let's try testing only notes within a certain range of the view area. + // TODO: I don't think this messes up really long sustains, does it? + var viewAreaTopMs:Float = scrollPositionInMs - (Conductor.measureLengthMs * 2); // Is 2 measures enough? + var viewAreaBottomMs:Float = scrollPositionInMs + (Conductor.measureLengthMs * 2); // Is 2 measures enough? + + // Add notes that are now visible. + for (noteData in currentSongChartNoteData) + { + // Remember if we are already displaying this note. + if (noteData == null) continue; + // Check if we are outside a broad range around the view area. + if (noteData.time < viewAreaTopMs || noteData.time > viewAreaBottomMs) continue; + + if (displayedNoteData.fastContains(noteData)) + { + continue; + } + + if (!ChartEditorNoteSprite.wouldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData, + renderedNotes)) continue; // Else, this note is visible and we need to render it! + + // Get a note sprite from the pool. + // If we can reuse a deleted note, do so. + // If a new note is needed, call buildNoteSprite. + var noteSprite:ChartEditorNoteSprite = renderedNotes.recycle(() -> new ChartEditorNoteSprite(this)); + // trace('Creating new Note... (${renderedNotes.members.length})'); + noteSprite.parentState = this; + + // The note sprite handles animation playback and positioning. + noteSprite.noteData = noteData; + noteSprite.overrideStepTime = null; + noteSprite.overrideData = null; + + // Setting note data resets the position relative to the group! + // If we don't update the note position AFTER setting the note data, the note will be rendered offscreen at y=5000. + noteSprite.updateNotePosition(renderedNotes); + + // Add hold notes that are now visible (and not already displayed). + if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1) + { + var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this)); + // trace('Creating new HoldNote... (${renderedHoldNotes.members.length})'); + + var noteLengthPixels:Float = noteSprite.noteData.getStepLength() * GRID_SIZE; + + holdNoteSprite.noteData = noteSprite.noteData; + holdNoteSprite.noteDirection = noteSprite.noteData.getDirection(); + + holdNoteSprite.setHeightDirectly(noteLengthPixels); + + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + } + } + + // Add events that are now visible. + for (eventData in currentSongChartEventData) + { + // Remember if we are already displaying this event. + if (displayedEventData.indexOf(eventData) != -1) + { + continue; + } + + if (!ChartEditorEventSprite.wouldEventBeVisible(viewAreaBottomPixels, viewAreaTopPixels, eventData, renderedNotes)) continue; + + // Else, this event is visible and we need to render it! + + // Get an event sprite from the pool. + // If we can reuse a deleted event, do so. + // If a new event is needed, call buildEventSprite. + var eventSprite:ChartEditorEventSprite = renderedEvents.recycle(() -> new ChartEditorEventSprite(this), false, true); + eventSprite.parentState = this; + trace('Creating new Event... (${renderedEvents.members.length})'); + + // The event sprite handles animation playback and positioning. + eventSprite.eventData = eventData; + eventSprite.overrideStepTime = null; + + // Setting event data resets position relative to the grid so we fix that. + eventSprite.x += renderedEvents.x; + eventSprite.y += renderedEvents.y; + } + + // Add hold notes that have been made visible (but not their parents) + for (noteData in currentSongChartNoteData) + { + // Is the note a hold note? + if (noteData == null || noteData.length <= 0) continue; + + // Is the hold note rendered already? + if (displayedHoldNoteData.indexOf(noteData) != -1) continue; + + // Is the hold note offscreen? + if (!ChartEditorHoldNoteSprite.wouldHoldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData, renderedHoldNotes)) continue; + + // Hold note should be rendered. + var holdNoteFactory = function() { + // TODO: Print some kind of warning if `renderedHoldNotes.members` is too high? + return new ChartEditorHoldNoteSprite(this); + } + var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(holdNoteFactory); + + var noteLengthPixels:Float = noteData.getStepLength() * GRID_SIZE; + + holdNoteSprite.noteData = noteData; + holdNoteSprite.noteDirection = noteData.getDirection(); + + holdNoteSprite.setHeightDirectly(noteLengthPixels); + + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + + displayedHoldNoteData.push(noteData); + } + + // Destroy all existing selection squares. + for (member in renderedSelectionSquares.members) + { + // Killing the sprite is cheap because we can recycle it. + member.kill(); + } + + // Readd selection squares for selected notes. + // Recycle selection squares if possible. + for (noteSprite in renderedNotes.members) + { + // TODO: Handle selection of hold notes. + if (isNoteSelected(noteSprite.noteData)) + { + // Determine if the note is being dragged and offset the vertical position accordingly. + if (dragTargetCurrentStep != 0.0) + { + var stepTime:Float = (noteSprite.noteData == null) ? 0.0 : noteSprite.noteData.getStepTime(); + // Update the note's "ghost" step time. + noteSprite.overrideStepTime = (stepTime + dragTargetCurrentStep).clamp(0, songLengthInSteps - (1 * noteSnapRatio)); + // Then reapply the note sprite's position relative to the grid. + noteSprite.updateNotePosition(renderedNotes); + } + else + { + if (noteSprite.overrideStepTime != null) + { + // Reset the note's "ghost" step time. + noteSprite.overrideStepTime = null; + // Then reapply the note sprite's position relative to the grid. + noteSprite.updateNotePosition(renderedNotes); + } + } + + // Determine if the note is being dragged and offset the horizontal position accordingly. + if (dragTargetCurrentColumn != 0) + { + var data:Int = (noteSprite.noteData == null) ? 0 : noteSprite.noteData.data; + // Update the note's "ghost" column. + noteSprite.overrideData = gridColumnToNoteData((noteDataToGridColumn(data) + dragTargetCurrentColumn).clamp(0, + ChartEditorState.STRUMLINE_SIZE * 2 - 1)); + // Then reapply the note sprite's position relative to the grid. + noteSprite.updateNotePosition(renderedNotes); + } + else + { + if (noteSprite.overrideData != null) + { + // Reset the note's "ghost" column. + noteSprite.overrideData = null; + // Then reapply the note sprite's position relative to the grid. + noteSprite.updateNotePosition(renderedNotes); + } + } + + // Then, render the selection square. + var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare); + + // Set the position and size (because we might be recycling one with bad values). + selectionSquare.noteData = noteSprite.noteData; + selectionSquare.eventData = null; + selectionSquare.x = noteSprite.x; + selectionSquare.y = noteSprite.y; + selectionSquare.width = GRID_SIZE; + selectionSquare.height = GRID_SIZE; + } + } + + for (eventSprite in renderedEvents.members) + { + if (isEventSelected(eventSprite.eventData)) + { + // Determine if the note is being dragged and offset the position accordingly. + if (dragTargetCurrentStep > 0 || dragTargetCurrentColumn > 0) + { + var stepTime = (eventSprite.eventData == null) ? 0 : eventSprite.eventData.getStepTime(); + eventSprite.overrideStepTime = (stepTime + dragTargetCurrentStep).clamp(0, songLengthInSteps); + // Then reapply the note sprite's position relative to the grid. + eventSprite.updateEventPosition(renderedEvents); + } + else + { + if (eventSprite.overrideStepTime != null) + { + // Reset the note's "ghost" column. + eventSprite.overrideStepTime = null; + // Then reapply the note sprite's position relative to the grid. + eventSprite.updateEventPosition(renderedEvents); + } + } + + // Then, render the selection square. + var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare); + + // Set the position and size (because we might be recycling one with bad values). + selectionSquare.noteData = null; + selectionSquare.eventData = eventSprite.eventData; + selectionSquare.x = eventSprite.x; + selectionSquare.y = eventSprite.y; + selectionSquare.width = eventSprite.width; + selectionSquare.height = eventSprite.height; + } + } + + // Sort the notes DESCENDING. This keeps the sustain behind the associated note. + renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() + + // Sort the events DESCENDING. This keeps the sustain behind the associated note. + renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() + } + } + /** * Handle keybinds for scrolling the chart editor grid. - **/ + */ function handleScrollKeybinds():Void { // Don't scroll when the cursor is over the UI, unless a playbar button (the << >> ones) is pressed. @@ -2128,6 +2660,9 @@ class ChartEditorState extends HaxeUIState if (shouldPause) stopAudioPlayback(); } + /** + * Handle changing the note snapping level. + */ function handleSnap():Void { if (currentLiveInputStyle == None) @@ -2144,9 +2679,6 @@ class ChartEditorState extends HaxeUIState } } - var dragLengthCurrent:Float = 0; - var stretchySounds:Bool = false; - /** * Handle display of the mouse cursor. */ @@ -2157,9 +2689,13 @@ class ChartEditorState extends HaxeUIState if (FlxG.mouse.justReleased) FlxG.sound.play(Paths.sound("chartingSounds/ClickUp")); // Note: If a menu is open in HaxeUI, don't handle cursor behavior. - var shouldHandleCursor:Bool = !isCursorOverHaxeUI || (selectionBoxStartPos != null); + var shouldHandleCursor:Bool = !isCursorOverHaxeUI + || (selectionBoxStartPos != null) + || (dragTargetNote != null || dragTargetEvent != null); var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1; + trace('shouldHandleCursor: $shouldHandleCursor'); + if (shouldHandleCursor) { // Over the course of this big conditional block, @@ -2180,6 +2716,8 @@ class ChartEditorState extends HaxeUIState || (cursorX % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)) || (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2))); + var overlapsSelection:Bool = FlxG.mouse.overlaps(renderedSelectionSquares); + if (FlxG.mouse.justPressed) { if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) @@ -2197,14 +2735,10 @@ class ChartEditorState extends HaxeUIState // Drawing selection box. targetCursorMode = Crosshair; } - else + else if (overlapsSelection) { - // Deselect all items. - if (currentNoteSelection.length > 0 || currentEventSelection.length > 0) - { - trace('Clicked outside grid, deselecting all items.'); - performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); - } + // Do nothing + trace('Clicked on a selected note!'); } } @@ -2237,25 +2771,8 @@ class ChartEditorState extends HaxeUIState var cursorSnappedMs:Float = Conductor.getStepTimeInMs(cursorSnappedStep); // The direction value for the column at the cursor. - var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE); - if (cursorColumn < 0) cursorColumn = 0; - if (cursorColumn >= (STRUMLINE_SIZE * 2 + 1 - 1)) - { - // Don't invert the event column. - cursorColumn = (STRUMLINE_SIZE * 2 + 1 - 1); - } - else - { - // Invert player and opponent columns. - if (cursorColumn >= STRUMLINE_SIZE) - { - cursorColumn -= STRUMLINE_SIZE; - } - else - { - cursorColumn += STRUMLINE_SIZE; - } - } + var cursorGridPos:Int = Math.floor(cursorX / GRID_SIZE); + var cursorColumn:Int = gridColumnToNoteData(cursorGridPos); if (selectionBoxStartPos != null) { @@ -2491,6 +3008,108 @@ class ChartEditorState extends HaxeUIState scrollPositionInPixels = clickedPosInPixels; moveSongToScrollPosition(); } + else if (dragTargetNote != null || dragTargetEvent != null) + { + if (FlxG.mouse.justReleased) + { + // Perform the actual drag operation. + var dragDistanceSteps:Float = dragTargetCurrentStep; + var dragDistanceMs:Float = 0; + if (dragTargetNote != null && dragTargetNote.noteData != null) + { + dragDistanceMs = Conductor.getStepTimeInMs(dragTargetNote.noteData.getStepTime() + dragDistanceSteps) - dragTargetNote.noteData.time; + } + else if (dragTargetEvent != null && dragTargetEvent.eventData != null) + { + dragDistanceMs = Conductor.getStepTimeInMs(dragTargetEvent.eventData.getStepTime() + dragDistanceSteps) - dragTargetEvent.eventData.time; + } + var dragDistanceColumns:Int = dragTargetCurrentColumn; + + if (currentNoteSelection.length > 0 && currentEventSelection.length > 0) + { + // Both notes and events are selected. + performCommand(new MoveItemsCommand(currentNoteSelection, currentEventSelection, dragDistanceMs, dragDistanceColumns)); + } + else if (currentNoteSelection.length > 0) + { + // Only notes are selected. + performCommand(new MoveNotesCommand(currentNoteSelection, dragDistanceMs, dragDistanceColumns)); + } + else if (currentEventSelection.length > 0) + { + // Only events are selected. + performCommand(new MoveEventsCommand(currentEventSelection, dragDistanceMs)); + } + + // Finished dragging. Release the note at the new position. + dragTargetNote = null; + dragTargetEvent = null; + + noteDisplayDirty = true; + + dragTargetCurrentStep = 0; + dragTargetCurrentColumn = 0; + } + else + { + // Player is clicking and holding on a selected note or event to move the selection around. + targetCursorMode = Grabbing; + + // Scroll the screen if the mouse is above or below the grid. + if (FlxG.mouse.screenY < MENU_BAR_HEIGHT) + { + // Scroll up. + trace('Scroll up!'); + var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY; + scrollPositionInPixels -= diff * 0.5; // Too fast! + moveSongToScrollPosition(); + } + else if (FlxG.mouse.screenY > (playbarHeadLayout?.y ?? 0.0)) + { + // Scroll down. + trace('Scroll down!'); + var diff:Float = FlxG.mouse.screenY - (playbarHeadLayout?.y ?? 0.0); + scrollPositionInPixels += diff * 0.5; // Too fast! + moveSongToScrollPosition(); + } + + // Calculate distance between the position dragged to and the original position. + var stepTime:Float = 0; + if (dragTargetNote != null && dragTargetNote.noteData != null) + { + stepTime = dragTargetNote.noteData.getStepTime(); + } + else if (dragTargetEvent != null && dragTargetEvent.eventData != null) + { + stepTime = dragTargetEvent.eventData.getStepTime(); + } + var dragDistanceSteps:Float = Conductor.getTimeInSteps(cursorSnappedMs).clamp(0, songLengthInSteps - (1 * noteSnapRatio)) - stepTime; + var data:Int = 0; + var noteGridPos:Int = 0; + if (dragTargetNote != null && dragTargetNote.noteData != null) + { + data = dragTargetNote.noteData.data; + noteGridPos = noteDataToGridColumn(data); + } + else if (dragTargetEvent != null) + { + data = ChartEditorState.STRUMLINE_SIZE * 2 + 1; + } + var dragDistanceColumns:Int = cursorGridPos - noteGridPos; + + if (dragTargetCurrentStep != dragDistanceSteps || dragTargetCurrentColumn != dragDistanceColumns) + { + // Play a sound as we drag. + this.playSound(Paths.sound('chartingSounds/noteLay')); + + trace('Dragged ${dragDistanceColumns} X and ${dragDistanceSteps} Y.'); + dragTargetCurrentStep = dragDistanceSteps; + dragTargetCurrentColumn = dragDistanceColumns; + + noteDisplayDirty = true; + } + } + } else if (currentPlaceNoteData != null) { // Handle extending the note as you drag. @@ -2507,7 +3126,7 @@ class ChartEditorState extends HaxeUIState if (dragLengthCurrent != dragLengthSteps) { stretchySounds = !stretchySounds; - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI')); + this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI')); dragLengthCurrent = dragLengthSteps; } @@ -2530,7 +3149,7 @@ class ChartEditorState extends HaxeUIState { if (dragLengthSteps > 0) { - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/stretchSNAP_UI')); + this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI')); // Apply the new length. performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs)); } @@ -2596,19 +3215,37 @@ class ChartEditorState extends HaxeUIState { if (highlightedNote != null && highlightedNote.noteData != null) { - // Click a note to select it. - performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection)); + if (isNoteSelected(highlightedNote.noteData)) + { + // Clicked a selected event, start dragging. + trace('Ready to drag!'); + dragTargetNote = highlightedNote; + } + else + { + // If you click an unselected note, and aren't holding Control, deselect everything else. + performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection)); + } } else if (highlightedEvent != null && highlightedEvent.eventData != null) { - // Click an event to select it. - performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); + if (isEventSelected(highlightedEvent.eventData)) + { + // Clicked a selected event, start dragging. + trace('Ready to drag!'); + dragTargetEvent = highlightedEvent; + } + else + { + // If you click an unselected event, and aren't holding Control, deselect everything else. + performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); + } } else { // Click a blank space to place a note and select it. - if (cursorColumn == eventColumn) + if (cursorGridPos == eventColumn) { // Create an event and place it in the chart. // TODO: Figure out configuring event data. @@ -2671,12 +3308,13 @@ class ChartEditorState extends HaxeUIState } } + var isOrWillSelect = overlapsSelection || dragTargetNote != null || dragTargetEvent != null; // Handle grid cursor. - if (overlapsGrid && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed) + if (overlapsGrid && !isOrWillSelect && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed) { // Indicate that we can place a note here. - if (cursorColumn == eventColumn) + if (cursorGridPos == eventColumn) { if (gridGhostNote != null) gridGhostNote.visible = false; if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false; @@ -2732,6 +3370,10 @@ class ChartEditorState extends HaxeUIState { if (FlxG.mouse.pressed) { + if (overlapsSelection) + { + targetCursorMode = Grabbing; + } if (overlapsSelectionBorder) { targetCursorMode = Crosshair; @@ -2747,6 +3389,10 @@ class ChartEditorState extends HaxeUIState { targetCursorMode = Pointer; } + else if (overlapsSelection) + { + targetCursorMode = Pointer; + } else if (overlapsSelectionBorder) { targetCursorMode = Crosshair; @@ -2772,329 +3418,188 @@ class ChartEditorState extends HaxeUIState } /** - * Handle using `renderedNotes` to display notes from `currentSongChartNoteData`. + * Handles passive behavior of the menu bar, such as updating labels or enabled/disabled status. + * Does not handle onClick ACTIONS of the menubar. */ - function handleNoteDisplay():Void + function handleMenubar():Void { - if (noteDisplayDirty) + if (commandHistoryDirty) { - noteDisplayDirty = false; + commandHistoryDirty = false; - // Update for whether downscroll is enabled. - renderedNotes.flipX = (isViewDownscroll); + // Update the Undo and Redo buttons. + var undoButton:Null<MenuItem> = findComponent('menubarItemUndo', MenuItem); - // Calculate the top and bottom of the view area. - var viewAreaTopPixels:Float = MENU_BAR_HEIGHT; - var visibleGridHeightPixels:Float = FlxG.height - MENU_BAR_HEIGHT - PLAYBAR_HEIGHT; // The area underneath the menu bar and playbar is not visible. - var viewAreaBottomPixels:Float = viewAreaTopPixels + visibleGridHeightPixels; - - // Remove notes that are no longer visible and list the ones that are. - var displayedNoteData:Array<SongNoteData> = []; - for (noteSprite in renderedNotes.members) + if (undoButton != null) { - if (noteSprite == null || noteSprite.noteData == null || !noteSprite.exists || !noteSprite.visible) continue; - - if (!noteSprite.isNoteVisible(viewAreaBottomPixels, viewAreaTopPixels)) + if (undoHistory.length == 0) { - // This sprite is off-screen. - // Kill the note sprite and recycle it. - noteSprite.noteData = null; - } - else if (!currentSongChartNoteData.fastContains(noteSprite.noteData)) - { - // This note was deleted. - // Kill the note sprite and recycle it. - noteSprite.noteData = null; + // Disable the Undo button. + undoButton.disabled = true; + undoButton.text = 'Undo'; } else { - // Note is already displayed and should remain displayed. - displayedNoteData.push(noteSprite.noteData); - - // Update the note sprite's position. - noteSprite.updateNotePosition(renderedNotes); + // Change the label to the last command. + undoButton.disabled = false; + undoButton.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}'; } } - // Sort the note data array, using an algorithm that is fast on nearly-sorted data. - // We need this sorted to optimize indexing later. - displayedNoteData.insertionSort(SortUtil.noteDataByTime.bind(FlxSort.ASCENDING)); - - var displayedHoldNoteData:Array<SongNoteData> = []; - for (holdNoteSprite in renderedHoldNotes.members) + else { - if (holdNoteSprite == null || holdNoteSprite.noteData == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; + trace('undoButton is null'); + } - if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + var redoButton:Null<MenuItem> = findComponent('menubarItemRedo', MenuItem); + + if (redoButton != null) + { + if (redoHistory.length == 0) { - holdNoteSprite.kill(); - } - else if (!currentSongChartNoteData.fastContains(holdNoteSprite.noteData) || holdNoteSprite.noteData.length == 0) - { - // This hold note was deleted. - // Kill the hold note sprite and recycle it. - holdNoteSprite.kill(); - } - else if (displayedHoldNoteData.fastContains(holdNoteSprite.noteData)) - { - // This hold note is a duplicate. - // Kill the hold note sprite and recycle it. - holdNoteSprite.kill(); + // Disable the Redo button. + redoButton.disabled = true; + redoButton.text = 'Redo'; } else { - displayedHoldNoteData.push(holdNoteSprite.noteData); - // Update the event sprite's position. - holdNoteSprite.updateHoldNotePosition(renderedNotes); + // Change the label to the last command. + redoButton.disabled = false; + redoButton.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}'; } } - // Sort the note data array, using an algorithm that is fast on nearly-sorted data. - // We need this sorted to optimize indexing later. - displayedHoldNoteData.insertionSort(SortUtil.noteDataByTime.bind(FlxSort.ASCENDING)); - - // Remove events that are no longer visible and list the ones that are. - var displayedEventData:Array<SongEventData> = []; - for (eventSprite in renderedEvents.members) + else { - if (eventSprite == null || eventSprite.eventData == null || !eventSprite.exists || !eventSprite.visible) continue; - - if (!eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) - { - // This sprite is off-screen. - // Kill the event sprite and recycle it. - eventSprite.eventData = null; - } - else if (!currentSongChartEventData.fastContains(eventSprite.eventData)) - { - // This event was deleted. - // Kill the event sprite and recycle it. - eventSprite.eventData = null; - } - else - { - // Event is already displayed and should remain displayed. - displayedEventData.push(eventSprite.eventData); - - // Update the event sprite's position. - eventSprite.updateEventPosition(renderedEvents); - } + trace('redoButton is null'); } - // Sort the note data array, using an algorithm that is fast on nearly-sorted data. - // We need this sorted to optimize indexing later. - displayedEventData.insertionSort(SortUtil.eventDataByTime.bind(FlxSort.ASCENDING)); - - // Let's try testing only notes within a certain range of the view area. - // TODO: I don't think this messes up really long sustains, does it? - var viewAreaTopMs:Float = scrollPositionInMs - (Conductor.measureLengthMs * 2); // Is 2 measures enough? - var viewAreaBottomMs:Float = scrollPositionInMs + (Conductor.measureLengthMs * 2); // Is 2 measures enough? - - // Add notes that are now visible. - for (noteData in currentSongChartNoteData) - { - // Remember if we are already displaying this note. - if (noteData == null) continue; - // Check if we are outside a broad range around the view area. - if (noteData.time < viewAreaTopMs || noteData.time > viewAreaBottomMs) continue; - - if (displayedNoteData.fastContains(noteData)) - { - continue; - } - - if (!ChartEditorNoteSprite.wouldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData, - renderedNotes)) continue; // Else, this note is visible and we need to render it! - - // Get a note sprite from the pool. - // If we can reuse a deleted note, do so. - // If a new note is needed, call buildNoteSprite. - var noteSprite:ChartEditorNoteSprite = renderedNotes.recycle(() -> new ChartEditorNoteSprite(this)); - // trace('Creating new Note... (${renderedNotes.members.length})'); - noteSprite.parentState = this; - - // The note sprite handles animation playback and positioning. - noteSprite.noteData = noteData; - - // Setting note data resets the position relative to the group! - // If we don't update the note position AFTER setting the note data, the note will be rendered offscreen at y=5000. - noteSprite.updateNotePosition(renderedNotes); - - // Add hold notes that are now visible (and not already displayed). - if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1) - { - var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this)); - // trace('Creating new HoldNote... (${renderedHoldNotes.members.length})'); - - var noteLengthPixels:Float = noteSprite.noteData.getStepLength() * GRID_SIZE; - - holdNoteSprite.noteData = noteSprite.noteData; - holdNoteSprite.noteDirection = noteSprite.noteData.getDirection(); - - holdNoteSprite.setHeightDirectly(noteLengthPixels); - - holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); - } - } - - // Add events that are now visible. - for (eventData in currentSongChartEventData) - { - // Remember if we are already displaying this event. - if (displayedEventData.indexOf(eventData) != -1) - { - continue; - } - - if (!ChartEditorEventSprite.wouldEventBeVisible(viewAreaBottomPixels, viewAreaTopPixels, eventData, renderedNotes)) continue; - - // Else, this event is visible and we need to render it! - - // Get an event sprite from the pool. - // If we can reuse a deleted event, do so. - // If a new event is needed, call buildEventSprite. - var eventSprite:ChartEditorEventSprite = renderedEvents.recycle(() -> new ChartEditorEventSprite(this), false, true); - eventSprite.parentState = this; - trace('Creating new Event... (${renderedEvents.members.length})'); - - // The event sprite handles animation playback and positioning. - eventSprite.eventData = eventData; - - // Setting event data resets position relative to the grid so we fix that. - eventSprite.x += renderedEvents.x; - eventSprite.y += renderedEvents.y; - } - - // Add hold notes that have been made visible (but not their parents) - for (noteData in currentSongChartNoteData) - { - // Is the note a hold note? - if (noteData == null || noteData.length <= 0) continue; - - // Is the hold note rendered already? - if (displayedHoldNoteData.indexOf(noteData) != -1) continue; - - // Is the hold note offscreen? - if (!ChartEditorHoldNoteSprite.wouldHoldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData, renderedHoldNotes)) continue; - - // Hold note should be rendered. - var holdNoteFactory = function() { - // TODO: Print some kind of warning if `renderedHoldNotes.members` is too high? - return new ChartEditorHoldNoteSprite(this); - } - var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(holdNoteFactory); - - var noteLengthPixels:Float = noteData.getStepLength() * GRID_SIZE; - - holdNoteSprite.noteData = noteData; - holdNoteSprite.noteDirection = noteData.getDirection(); - - holdNoteSprite.setHeightDirectly(noteLengthPixels); - - holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); - - displayedHoldNoteData.push(noteData); - } - - // Destroy all existing selection squares. - for (member in renderedSelectionSquares.members) - { - // Killing the sprite is cheap because we can recycle it. - member.kill(); - } - - // Readd selection squares for selected notes. - // Recycle selection squares if possible. - for (noteSprite in renderedNotes.members) - { - // TODO: Handle selection of hold notes. - if (isNoteSelected(noteSprite.noteData)) - { - var selectionSquare:FlxSprite = renderedSelectionSquares.recycle(buildSelectionSquare); - - // Set the position and size (because we might be recycling one with bad values). - selectionSquare.x = noteSprite.x; - selectionSquare.y = noteSprite.y; - selectionSquare.width = GRID_SIZE; - selectionSquare.height = GRID_SIZE; - } - } - - for (eventSprite in renderedEvents.members) - { - if (isEventSelected(eventSprite.eventData)) - { - var selectionSquare:FlxSprite = renderedSelectionSquares.recycle(buildSelectionSquare); - - // Set the position and size (because we might be recycling one with bad values). - selectionSquare.x = eventSprite.x; - selectionSquare.y = eventSprite.y; - selectionSquare.width = eventSprite.width; - selectionSquare.height = eventSprite.height; - } - } - - // Sort the notes DESCENDING. This keeps the sustain behind the associated note. - renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() - - // Sort the events DESCENDING. This keeps the sustain behind the associated note. - renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() - } - - // Add a debug value which displays the current size of the note pool. - // The pool will grow as more notes need to be rendered at once. - // If this gets too big, something needs to be optimized somewhere! -Eric - FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length); - FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length); - FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length); - FlxG.watch.addQuick("notesSelected", currentNoteSelection.length); - FlxG.watch.addQuick("eventsSelected", currentEventSelection.length); - } - - /** - * Handle aligning the health icons next to the grid. - */ - function handleHealthIcons():Void - { - if (healthIconsDirty) - { - var charDataBF = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.player); - var charDataDad = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.opponent); - if (healthIconBF != null) - { - healthIconBF.configure(charDataBF?.healthIcon); - healthIconBF.size *= 0.5; // Make the icon smaller in Chart Editor. - healthIconBF.flipX = !healthIconBF.flipX; // BF faces the other way. - } - if (healthIconDad != null) - { - healthIconDad.configure(charDataDad?.healthIcon); - healthIconDad.size *= 0.5; // Make the icon smaller in Chart Editor. - } - healthIconsDirty = false; - } - - // Right align, and visibly center, the BF health icon. - if (healthIconBF != null) - { - // Base X position to the right of the grid. - healthIconBF.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 45 - (healthIconBF.width / 2)); - healthIconBF.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconBF.height / 2)); - } - - // Visibly center the Dad health icon. - if (healthIconDad != null) - { - healthIconDad.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x - 45 - (healthIconDad.width / 2)); - healthIconDad.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconDad.height / 2)); } } - function buildSelectionSquare():FlxSprite + function handleToolboxes():Void { - if (selectionSquareBitmap == null) - throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()"; + handleDifficultyToolbox(); + handlePlayerPreviewToolbox(); + handleOpponentPreviewToolbox(); + } - FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap"); + function handleDifficultyToolbox():Void + { + if (difficultySelectDirty) + { + difficultySelectDirty = false; - return new FlxSprite().loadGraphic(selectionSquareBitmap); + // Manage the Select Difficulty tree view. + var difficultyToolbox:Null<CollapsibleDialog> = this.getToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + if (difficultyToolbox == null) return; + + var treeView:Null<TreeView> = difficultyToolbox.findComponent('difficultyToolboxTree'); + if (treeView == null) return; + + // Clear the tree view so we can rebuild it. + treeView.clearNodes(); + + // , icon: 'haxeui-core/styles/default/haxeui_tiny.png' + var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName'}); + treeSong.expanded = true; + + for (curVariation in availableVariations) + { + var variationMetadata:Null<SongMetadata> = songMetadata.get(curVariation); + if (variationMetadata == null) continue; + + var treeVariation:TreeViewNode = treeSong.addNode( + { + id: 'stv_variation_$curVariation', + text: 'V: ${curVariation.toTitleCase()}' + }); + treeVariation.expanded = true; + + var difficultyList:Array<String> = variationMetadata.playData.difficulties; + + for (difficulty in difficultyList) + { + var _treeDifficulty:TreeViewNode = treeVariation.addNode( + { + id: 'stv_difficulty_${curVariation}_$difficulty', + text: 'D: ${difficulty.toTitleCase()}' + }); + } + } + + treeView.onChange = onChangeTreeDifficulty; + refreshDifficultyTreeSelection(treeView); + } + } + + function handlePlayerPreviewToolbox():Void + { + // Manage the Select Difficulty tree view. + var charPreviewToolbox:Null<CollapsibleDialog> = this.getToolbox(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); + if (charPreviewToolbox == null) return; + + // TODO: Re-enable the player preview once we figure out the performance issues. + var charPlayer:Null<CharacterPlayer> = null; // charPreviewToolbox.findComponent('charPlayer'); + if (charPlayer == null) return; + + currentPlayerCharacterPlayer = charPlayer; + + if (playerPreviewDirty) + { + playerPreviewDirty = false; + + if (currentSongMetadata.playData.characters.player != charPlayer.charId) + { + if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player; + + charPlayer.loadCharacter(currentSongMetadata.playData.characters.player); + 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:Null<CollapsibleDialog> = this.getToolbox(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); + if (charPreviewToolbox == null) return; + + // TODO: Re-enable the player preview once we figure out the performance issues. + var charPlayer:Null<CharacterPlayer> = null; // charPreviewToolbox.findComponent('charPlayer'); + if (charPlayer == null) return; + + currentOpponentCharacterPlayer = charPlayer; + + if (opponentPreviewDirty) + { + opponentPreviewDirty = false; + + if (currentSongMetadata.playData.characters.opponent != charPlayer.charId) + { + if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent; + + charPlayer.loadCharacter(currentSongMetadata.playData.characters.opponent); + 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; + } + } } /** @@ -3138,6 +3643,125 @@ class ChartEditorState extends HaxeUIState if (playbarNoteSnap != null && playbarNoteSnap.value != '1/${noteSnapQuant}') playbarNoteSnap.value = '1/${noteSnapQuant}'; } + function handlePlayhead():Void + { + // Place notes at the playhead. + switch (currentLiveInputStyle) + { + case ChartEditorLiveInputStyle.WASD: + if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); + if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5); + if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6); + if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7); + + if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0); + if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1); + if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2); + if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3); + case ChartEditorLiveInputStyle.NumberKeys: + // Flipped because Dad is on the left but represents data 0-3. + if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4); + if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5); + if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6); + if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7); + + if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0); + if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1); + if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2); + if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3); + case ChartEditorLiveInputStyle.None: + // Do nothing. + } + } + + function placeNoteAtPlayhead(column:Int):Void + { + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; + var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; + var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); + var playheadPosSnappedMs:Float = playheadPosStep * Conductor.stepLengthMs * noteSnapRatio; + + // Look for notes within 1 step of the playhead. + var notesAtPos:Array<SongNoteData> = SongDataUtils.getNotesInTimeRange(currentSongChartNoteData, playheadPosSnappedMs, + playheadPosSnappedMs + Conductor.stepLengthMs * noteSnapRatio); + notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]); + + if (notesAtPos.length == 0) + { + var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, selectedNoteKind); + performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); + } + else + { + trace('Already a note there.'); + } + } + + /** + * Handles the note preview/scroll area on the right side. + * Notes are rendered here as small bars. + * This function also handles: + * - Moving the viewport preview box around based on its current position. + * - Scrolling the note preview area down if the note preview is taller than the screen, + * and the viewport nears the end of the visible area. + */ + function handleNotePreview():Void + { + if (notePreviewDirty && notePreview != null) + { + notePreviewDirty = false; + + // TODO: Only update the notes that have changed. + notePreview.erase(); + notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); + notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); + } + + if (notePreviewViewportBoundsDirty) + { + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + } + } + + /** + * Handle aligning the health icons next to the grid. + */ + function handleHealthIcons():Void + { + if (healthIconsDirty) + { + var charDataBF = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.player); + var charDataDad = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.opponent); + if (healthIconBF != null) + { + healthIconBF.configure(charDataBF?.healthIcon); + healthIconBF.size *= 0.5; // Make the icon smaller in Chart Editor. + healthIconBF.flipX = !healthIconBF.flipX; // BF faces the other way. + } + if (healthIconDad != null) + { + healthIconDad.configure(charDataDad?.healthIcon); + healthIconDad.size *= 0.5; // Make the icon smaller in Chart Editor. + } + healthIconsDirty = false; + } + + // Right align, and visibly center, the BF health icon. + if (healthIconBF != null) + { + // Base X position to the right of the grid. + healthIconBF.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 45 - (healthIconBF.width / 2)); + healthIconBF.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconBF.height / 2)); + } + + // Visibly center the Dad health icon. + if (healthIconDad != null) + { + healthIconDad.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x - 45 - (healthIconDad.width / 2)); + healthIconDad.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconDad.height / 2)); + } + } + /** * Handle keybinds for File menu items. */ @@ -3146,19 +3770,19 @@ class ChartEditorState extends HaxeUIState // CTRL + N = New Chart if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.N) { - ChartEditorDialogHandler.openWelcomeDialog(this, true); + this.openWelcomeDialog(true); } // CTRL + O = Open Chart if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.O) { - ChartEditorDialogHandler.openBrowseWizard(this, true); + this.openBrowseWizard(true); } // CTRL + SHIFT + S = Save As if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.S) { - ChartEditorImportExportHandler.exportAllSongData(this, false); + this.exportAllSongData(false); } // CTRL + Q = Quit to Menu @@ -3168,13 +3792,6 @@ class ChartEditorState extends HaxeUIState } } - function quitChartEditor():Void - { - autoSave(); - stopWelcomeMusic(); - FlxG.switchState(new MainMenuState()); - } - /** * Handle keybinds for edit menu items. */ @@ -3289,6 +3906,383 @@ class ChartEditorState extends HaxeUIState } } + /** + * Handle keybinds for the Test menu items. + */ + function handleTestKeybinds():Void + { + if (!isHaxeUIDialogOpen && !isCursorOverHaxeUI && FlxG.keys.justPressed.ENTER) + { + var minimal = FlxG.keys.pressed.SHIFT; + this.hideAllToolboxes(); + testSongInPlayState(minimal); + } + } + + /** + * Handle keybinds for Help menu items. + */ + function handleHelpKeybinds():Void + { + // F1 = Open Help + if (FlxG.keys.justPressed.F1) this.openUserGuideDialog(); + } + + function handleQuickWatch():Void + { + FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); + FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels); + + FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length); + FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length); + FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length); + FlxG.watch.addQuick("notesSelected", currentNoteSelection.length); + FlxG.watch.addQuick("eventsSelected", currentEventSelection.length); + } + + /** + * PLAYTEST FUNCTIONS + */ + // ==================== + + /** + * Transitions to the Play State to test the song + */ + function testSongInPlayState(minimal:Bool = false):Void + { + autoSave(); + + var startTimestamp:Float = 0; + if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; + + var targetSong:Song = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); + + // TODO: Rework asset system so we can remove this. + switch (currentSongStage) + { + case 'mainStage': + Paths.setCurrentLevel('week1'); + case 'spookyMansion': + Paths.setCurrentLevel('week2'); + case 'phillyTrain': + Paths.setCurrentLevel('week3'); + case 'limoRide': + Paths.setCurrentLevel('week4'); + case 'mallXmas' | 'mallEvil': + Paths.setCurrentLevel('week5'); + case 'school' | 'schoolEvil': + Paths.setCurrentLevel('week6'); + case 'tankmanBattlefield': + Paths.setCurrentLevel('week7'); + case 'phillyStreets' | 'phillyBlazin': + Paths.setCurrentLevel('weekend1'); + } + + subStateClosed.add(fixCamera); + subStateClosed.add(resetConductorAfterTest); + + FlxTransitionableState.skipNextTransIn = false; + FlxTransitionableState.skipNextTransOut = false; + + var targetState = new PlayState( + { + targetSong: targetSong, + targetDifficulty: selectedDifficulty, + // TODO: Add this. + // targetCharacter: targetCharacter, + practiceMode: true, + minimalMode: minimal, + startTimestamp: startTimestamp, + overrideMusic: true, + }); + + // Override music. + if (audioInstTrack != null) FlxG.sound.music = audioInstTrack; + if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup; + + openSubState(targetState); + } + + /** + * COMMAND FUNCTIONS + */ + // ==================== + + /** + * Perform (or redo) a command, then add it to the undo stack. + * + * @param command The command to perform. + * @param purgeRedoStack If true, the redo stack will be cleared. + */ + function performCommand(command:ChartEditorCommand, purgeRedoStack:Bool = true):Void + { + command.execute(this); + undoHistory.push(command); + commandHistoryDirty = true; + if (purgeRedoStack) redoHistory = []; + } + + /** + * Undo a command, then add it to the redo stack. + * @param command The command to undo. + */ + function undoCommand(command:ChartEditorCommand):Void + { + command.undo(this); + redoHistory.push(command); + commandHistoryDirty = true; + } + + /** + * Undo the last command in the undo stack, then add it to the redo stack. + */ + function undoLastCommand():Void + { + var command:Null<ChartEditorCommand> = undoHistory.pop(); + if (command == null) + { + trace('No actions to undo.'); + return; + } + undoCommand(command); + } + + /** + * Redo the last command in the redo stack, then add it to the undo stack. + */ + function redoLastCommand():Void + { + var command:Null<ChartEditorCommand> = redoHistory.pop(); + if (command == null) + { + trace('No actions to redo.'); + return; + } + performCommand(command, false); + } + + /** + * SAVE, AUTOSAVE, QUIT FUNCTIONS + */ + // ==================== + + /** + * Called after 5 minutes without saving. + */ + function autoSave():Void + { + saveDataDirty = false; + + // Auto-save the chart. + + #if html5 + // Auto-save to local storage. + #else + // Auto-save to temp file. + this.exportAllSongData(true); + #end + } + + /** + * Called when the user presses the Quit button. + */ + function quitChartEditor():Void + { + autoSave(); + stopWelcomeMusic(); + FlxG.switchState(new MainMenuState()); + } + + /** + * Called when the window is closed while we are in the chart editor. + * @param exitCode The exit code of the window. + */ + function onWindowClose(exitCode:Int):Void + { + trace('Window exited with exit code: $exitCode'); + trace('Should save chart? $saveDataDirty'); + + if (saveDataDirty) + { + this.exportAllSongData(true); + } + } + + function cleanupAutoSave():Void + { + WindowUtil.windowExit.remove(onWindowClose); + } + + /** + * GRAPHICS FUNCTIONS + */ + // ==================== + + /** + * This is for the smaller green squares that appear over each note when you select them. + */ + function buildSelectionSquare():ChartEditorSelectionSquareSprite + { + if (selectionSquareBitmap == null) + throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()"; + + // FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap"); + var result = new ChartEditorSelectionSquareSprite(); + result.loadGraphic(selectionSquareBitmap); + return result; + } + + /** + * Fix a camera issue caused when closing the PlayState used when testing. + */ + function fixCamera(_:FlxSubState = null):Void + { + FlxG.cameras.reset(new FlxCamera()); + FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2)); + FlxG.camera.zoom = 1.0; + + add(this.component); + } + + /** + * AUDIO FUNCTIONS + */ + // ==================== + + function startAudioPlayback():Void + { + if (audioInstTrack != null) + { + audioInstTrack.play(false, audioInstTrack.time); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time); + } + + setComponentText('playbarPlay', '||'); + } + + function stopAudioPlayback():Void + { + if (audioInstTrack != null) audioInstTrack.pause(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); + + setComponentText('playbarPlay', '>'); + } + + function toggleAudioPlayback():Void + { + if (audioInstTrack == null) return; + + if (audioInstTrack.playing) + { + stopAudioPlayback(); + } + else + { + startAudioPlayback(); + } + } + + /** + * Play the metronome tick sound. + * @param high Whether to play the full beat sound rather than the quarter beat sound. + */ + function playMetronomeTick(high:Bool = false):Void + { + this.playSound(Paths.sound('chartingSounds/metronome${high ? '1' : '2'}')); + } + + function switchToCurrentInstrumental():Void + { + // ChartEditorAudioHandler + this.switchToInstrumental(currentInstrumentalId, currentSongMetadata.playData.characters.player, currentSongMetadata.playData.characters.opponent); + } + + function postLoadInstrumental():Void + { + if (audioInstTrack != null) + { + // Prevent the time from skipping back to 0 when the song ends. + audioInstTrack.onComplete = function() { + if (audioInstTrack != null) audioInstTrack.pause(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); + }; + + songLengthInMs = audioInstTrack.length; + + if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; + if (gridPlayheadScrollArea != null) + { + gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); + gridPlayheadScrollArea.updateHitbox(); + } + + buildSpectrogram(audioInstTrack); + } + else + { + trace('[WARN] Instrumental track was null!'); + } + + // Pretty much everything is going to need to be reset. + scrollPositionInPixels = 0; + playheadPositionInPixels = 0; + notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; + noteDisplayDirty = true; + healthIconsDirty = true; + moveSongToScrollPosition(); + } + + /** + * CHART DATA FUNCTIONS + */ + // ==================== + + function sortChartData():Void + { + // TODO: .insertionSort() + currentSongChartNoteData.sort(function(a:SongNoteData, b:SongNoteData):Int { + return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time); + }); + + // TODO: .insertionSort() + currentSongChartEventData.sort(function(a:SongEventData, b:SongEventData):Int { + return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time); + }); + } + + function isNoteSelected(note:Null<SongNoteData>):Bool + { + return note != null && currentNoteSelection.indexOf(note) != -1; + } + + function isEventSelected(event:Null<SongEventData>):Bool + { + return event != null && currentEventSelection.indexOf(event) != -1; + } + + function createDifficulty(variation:String, difficulty:String, scrollSpeed:Float = 1.0) + { + var variationMetadata:Null<SongMetadata> = songMetadata.get(variation); + if (variationMetadata == null) return; + + variationMetadata.playData.difficulties.push(difficulty); + + var resultChartData = songChartData.get(variation); + if (resultChartData == null) + { + resultChartData = new SongChartData([difficulty => scrollSpeed], [], [difficulty => []]); + songChartData.set(variation, resultChartData); + } + else + { + resultChartData.scrollSpeed.set(difficulty, scrollSpeed); + resultChartData.notes.set(difficulty, []); + } + + difficultySelectDirty = true; // Force the Difficulty toolbox to update. + } + function incrementDifficulty(change:Int):Void { var currentDifficultyIndex:Int = availableDifficulties.indexOf(selectedDifficulty); @@ -3382,117 +4376,101 @@ class ChartEditorState extends HaxeUIState title: 'Switch Difficulty', body: 'Switched difficulty to ${selectedDifficulty.toTitleCase()}', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end } /** - * Handle keybinds for the Test menu items. + * SCROLLING FUNCTIONS */ - function handleTestKeybinds():Void + // ==================== + + /** + * 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. + */ + function moveSongToScrollPosition():Void { - if (!isHaxeUIDialogOpen && !isCursorOverHaxeUI && FlxG.keys.justPressed.ENTER) + // Update the songPosition in the audio tracks. + if (audioInstTrack != null) { - var minimal = FlxG.keys.pressed.SHIFT; - ChartEditorToolboxHandler.hideAllToolboxes(this); - testSongInPlayState(minimal); + audioInstTrack.time = scrollPositionInMs + playheadPositionInMs; + // Update the songPosition in the Conductor. + Conductor.update(audioInstTrack.time); + } + if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = scrollPositionInMs + playheadPositionInMs; + + // We need to update the note sprites because we changed the scroll position. + noteDisplayDirty = true; + } + + /** + * Smoothly ease the song to a new scroll position over a duration. + * @param targetScrollPosition The desired value for the `scrollPositionInPixels`. + */ + 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 + }); + } + + /** + * Callback function executed every frame that the scroll position is being eased. + * @param _ + */ + function onScrollEaseUpdate(_:FlxTween):Void + { + moveSongToScrollPosition(); + } + + /** + * Callback function executed when cancelling an existing scroll position ease. + * Ensures that the ease is immediately cancelled and the scroll position is set to the target value. + */ + function cancelScrollEase(_:FlxTween):Void + { + if (currentScrollEase != null) + { + @:privateAccess + var targetScrollPosition:Float = currentScrollEase._properties.scrollPositionInPixels; + + currentScrollEase.cancel(); + currentScrollEase = null; + this.scrollPositionInPixels = targetScrollPosition; } } /** - * Handle keybinds for Help menu items. + * Fix the current scroll position after exiting the PlayState used when testing. */ - function handleHelpKeybinds():Void + function resetConductorAfterTest(_:FlxSubState = null):Void { - // F1 = Open Help - if (FlxG.keys.justPressed.F1) ChartEditorDialogHandler.openUserGuideDialog(this); + moveSongToScrollPosition(); } - function handleToolboxes():Void - { - handleDifficultyToolbox(); - handlePlayerPreviewToolbox(); - handleOpponentPreviewToolbox(); - } - - function handleDifficultyToolbox():Void - { - if (difficultySelectDirty) - { - difficultySelectDirty = false; - - // Manage the Select Difficulty tree view. - var difficultyToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); - if (difficultyToolbox == null) return; - - var treeView:Null<TreeView> = difficultyToolbox.findComponent('difficultyToolboxTree'); - if (treeView == null) return; - - // Clear the tree view so we can rebuild it. - treeView.clearNodes(); - - // , icon: 'haxeui-core/styles/default/haxeui_tiny.png' - var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName'}); - treeSong.expanded = true; - - for (curVariation in availableVariations) - { - var variationMetadata:Null<SongMetadata> = songMetadata.get(curVariation); - if (variationMetadata == null) continue; - - var treeVariation:TreeViewNode = treeSong.addNode( - { - id: 'stv_variation_$curVariation', - text: 'V: ${curVariation.toTitleCase()}' - }); - treeVariation.expanded = true; - - var difficultyList:Array<String> = variationMetadata.playData.difficulties; - - for (difficulty in difficultyList) - { - var _treeDifficulty:TreeViewNode = treeVariation.addNode( - { - id: 'stv_difficulty_${curVariation}_$difficulty', - text: 'D: ${difficulty.toTitleCase()}' - }); - } - } - - treeView.onChange = onChangeTreeDifficulty; - refreshDifficultyTreeSelection(treeView); - } - } - - public function createDifficulty(variation:String, difficulty:String, scrollSpeed:Float = 1.0) - { - var variationMetadata:Null<SongMetadata> = songMetadata.get(variation); - if (variationMetadata == null) return; - - variationMetadata.playData.difficulties.push(difficulty); - - var resultChartData = songChartData.get(variation); - if (resultChartData == null) - { - resultChartData = new SongChartData([difficulty => scrollSpeed], [], [difficulty => []]); - songChartData.set(variation, resultChartData); - } - else - { - resultChartData.scrollSpeed.set(difficulty, scrollSpeed); - resultChartData.notes.set(difficulty, []); - } - - difficultySelectDirty = true; // Force the Difficulty toolbox to update. - } + /** + * HAXEUI FUNCTIONS + */ + // ==================== + /** + * Set the currently selected item in the Difficulty tree view to the node representing the current difficulty. + * @param treeView The tree view to update. If `null`, the tree view will be found. + */ function refreshDifficultyTreeSelection(?treeView:TreeView):Void { if (treeView == null) { // Manage the Select Difficulty tree view. - var difficultyToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var difficultyToolbox:Null<CollapsibleDialog> = this.getToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (difficultyToolbox == null) return; treeView = difficultyToolbox.findComponent('difficultyToolboxTree'); @@ -3503,119 +4481,16 @@ class ChartEditorState extends HaxeUIState if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode; } - function handlePlayerPreviewToolbox():Void - { - // Manage the Select Difficulty tree view. - var charPreviewToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); - if (charPreviewToolbox == null) return; - - // TODO: Re-enable the player preview once we figure out the performance issues. - var charPlayer:Null<CharacterPlayer> = null; // charPreviewToolbox.findComponent('charPlayer'); - if (charPlayer == null) return; - - currentPlayerCharacterPlayer = charPlayer; - - if (playerPreviewDirty) - { - playerPreviewDirty = false; - - if (currentSongMetadata.playData.characters.player != charPlayer.charId) - { - if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player; - - charPlayer.loadCharacter(currentSongMetadata.playData.characters.player); - 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:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); - if (charPreviewToolbox == null) return; - - // TODO: Re-enable the player preview once we figure out the performance issues. - var charPlayer:Null<CharacterPlayer> = null; // charPreviewToolbox.findComponent('charPlayer'); - if (charPlayer == null) return; - - currentOpponentCharacterPlayer = charPlayer; - - if (opponentPreviewDirty) - { - opponentPreviewDirty = false; - - if (currentSongMetadata.playData.characters.opponent != charPlayer.charId) - { - if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent; - - charPlayer.loadCharacter(currentSongMetadata.playData.characters.opponent); - 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 - { - super.dispatchEvent(event); - - // We can't use the ScriptedEventDispatcher with currentCharPlayer because we can't use the IScriptedClass interface on it. - if (currentPlayerCharacterPlayer != null) - { - switch (event.type) - { - case ScriptEvent.UPDATE: - currentPlayerCharacterPlayer.onUpdate(cast event); - case ScriptEvent.SONG_BEAT_HIT: - currentPlayerCharacterPlayer.onBeatHit(cast event); - case ScriptEvent.SONG_STEP_HIT: - currentPlayerCharacterPlayer.onStepHit(cast event); - case ScriptEvent.NOTE_HIT: - currentPlayerCharacterPlayer.onNoteHit(cast event); - } - } - - if (currentOpponentCharacterPlayer != null) - { - switch (event.type) - { - case ScriptEvent.UPDATE: - currentOpponentCharacterPlayer.onUpdate(cast event); - case ScriptEvent.SONG_BEAT_HIT: - currentOpponentCharacterPlayer.onBeatHit(cast event); - case ScriptEvent.SONG_STEP_HIT: - currentOpponentCharacterPlayer.onStepHit(cast event); - case ScriptEvent.NOTE_HIT: - currentOpponentCharacterPlayer.onNoteHit(cast event); - } - } - } - + /** + * Retrieve the node representing the current difficulty in the Difficulty tree view. + * @param treeView The tree view to search. If `null`, the tree view will be found. + * @return The node representing the current difficulty, or `null` if not found. + */ function getCurrentTreeDifficultyNode(?treeView:TreeView = null):Null<TreeViewNode> { if (treeView == null) { - var difficultyToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var difficultyToolbox:Null<CollapsibleDialog> = this.getToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (difficultyToolbox == null) return null; treeView = difficultyToolbox.findComponent('difficultyToolboxTree'); @@ -3678,7 +4553,7 @@ class ChartEditorState extends HaxeUIState */ function refreshMetadataToolbox():Void { - var toolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); + var toolbox:Null<CollapsibleDialog> = this.getToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); if (toolbox == null) return; var inputSongName:Null<TextField> = toolbox.findComponent('inputSongName', TextField); @@ -3749,605 +4624,104 @@ class ChartEditorState extends HaxeUIState } /** - * Handle the player preview/gameplay test area on the left side. + * STATIC FUNCTIONS */ - function handlePlayerDisplay():Void {} + // ==================== /** - * Handles the note preview/scroll area on the right side. - * Notes are rendered here as small bars. - * This function also handles: - * - Moving the viewport preview box around based on its current position. - * - Scrolling the note preview area down if the note preview is taller than the screen, - * and the viewport nears the end of the visible area. + * Dismiss any existing HaxeUI notifications, if there are any. */ - function handleNotePreview():Void - { - if (notePreviewDirty && notePreview != null) - { - notePreviewDirty = false; - - // TODO: Only update the notes that have changed. - notePreview.erase(); - notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); - notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); - } - - if (notePreviewViewportBoundsDirty) - { - setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); - } - } - - /** - * Handles passive behavior of the menu bar, such as updating labels or enabled/disabled status. - * Does not handle onClick ACTIONS of the menubar. - */ - function handleMenubar():Void - { - if (commandHistoryDirty) - { - commandHistoryDirty = false; - - // Update the Undo and Redo buttons. - var undoButton:Null<MenuItem> = findComponent('menubarItemUndo', MenuItem); - - if (undoButton != null) - { - if (undoHistory.length == 0) - { - // Disable the Undo button. - undoButton.disabled = true; - undoButton.text = 'Undo'; - } - else - { - // Change the label to the last command. - undoButton.disabled = false; - undoButton.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}'; - } - } - else - { - trace('undoButton is null'); - } - - var redoButton:Null<MenuItem> = findComponent('menubarItemRedo', MenuItem); - - if (redoButton != null) - { - if (redoHistory.length == 0) - { - // Disable the Redo button. - redoButton.disabled = true; - redoButton.text = 'Redo'; - } - else - { - // Change the label to the last command. - redoButton.disabled = false; - redoButton.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}'; - } - } - else - { - trace('redoButton is null'); - } - } - } - - /** - * Handle syncronizing the conductor with the music playback. - */ - function handleMusicPlayback():Void - { - if (audioInstTrack != null && audioInstTrack.playing) - { - if (FlxG.mouse.pressedMiddle) - { - // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! - - var oldStepTime:Float = Conductor.currentStepTime; - 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; - } - var diffStepTime:Float = Conductor.currentStepTime - oldStepTime; - - // Move the playhead. - playheadPositionInPixels += diffStepTime * GRID_SIZE; - - // We don't move the song to scroll position, or update the note sprites. - } - else - { - // Else, move the entire view. - 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; - } - - // We need time in fractional steps here to allow the song to actually play. - // Also account for a potentially offset playhead. - scrollPositionInPixels = Conductor.currentStepTime * GRID_SIZE - playheadPositionInPixels; - - // DO NOT move song to scroll position here specifically. - - // We need to update the note sprites. - noteDisplayDirty = true; - - // Update the note preview viewport box. - setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); - } - } - - if (FlxG.keys.justPressed.SPACE && !isHaxeUIDialogOpen) - { - toggleAudioPlayback(); - } - } - - /** - * Handle the playback of hitsounds. - */ - function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void - { - if (!hitsoundsEnabled) return; - - // Assume notes are sorted by time. - for (noteData in currentSongChartNoteData) - { - // Check for notes between the old and new song positions. - - if (noteData.time < oldSongPosition) // Note is in the past. - continue; - - if (noteData.time > newSongPosition) // Note is in the future. - return; // Assume all notes are also in the future. - - // Note was just hit. - - // Character preview. - - // NoteScriptEvent takes a sprite, ehe. Need to rework that. - var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault()); - tempNote.noteData = noteData; - tempNote.scrollFactor.set(0, 0); - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, tempNote, 1, true); - dispatchEvent(event); - - // Calling event.cancelEvent() skips all the other logic! Neat! - if (event.eventCanceled) continue; - - // Hitsounds. - switch (noteData.getStrumlineIndex()) - { - case 0: // Player - if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/hitNotePlayer')); - case 1: // Opponent - if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/hitNoteOpponent')); - } - } - } - - function startAudioPlayback():Void - { - if (audioInstTrack != null) - { - audioInstTrack.play(false, audioInstTrack.time); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time); - } - - setComponentText('playbarPlay', '||'); - } - - function stopAudioPlayback():Void - { - if (audioInstTrack != null) audioInstTrack.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); - - setComponentText('playbarPlay', '>'); - } - - function toggleAudioPlayback():Void - { - if (audioInstTrack == null) return; - - if (audioInstTrack.playing) - { - stopAudioPlayback(); - } - else - { - startAudioPlayback(); - } - } - - function handlePlayhead():Void - { - // Place notes at the playhead. - switch (currentLiveInputStyle) - { - case LiveInputStyle.WASD: - if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4); - if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5); - if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6); - if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7); - - if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0); - if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1); - if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2); - if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3); - case LiveInputStyle.NumberKeys: - // Flipped because Dad is on the left but represents data 0-3. - if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4); - if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5); - if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6); - if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7); - - if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0); - if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1); - if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2); - if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3); - case LiveInputStyle.None: - // Do nothing. - } - } - - function placeNoteAtPlayhead(column:Int):Void - { - var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; - var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; - var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); - var playheadPosSnappedMs:Float = playheadPosStep * Conductor.stepLengthMs * noteSnapRatio; - - // Look for notes within 1 step of the playhead. - var notesAtPos:Array<SongNoteData> = SongDataUtils.getNotesInTimeRange(currentSongChartNoteData, playheadPosSnappedMs, - playheadPosSnappedMs + Conductor.stepLengthMs * noteSnapRatio); - notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]); - - if (notesAtPos.length == 0) - { - var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, selectedNoteKind); - performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); - } - else - { - trace('Already a note there.'); - } - } - - function set_scrollPositionInPixels(value:Float):Float - { - if (value < 0) - { - // If we're scrolling up, and we hit the top, - // but the playhead is in the middle, move the playhead up. - if (playheadPositionInPixels > 0) - { - var amount:Float = scrollPositionInPixels - value; - playheadPositionInPixels -= amount; - } - - value = 0; - } - - if (value > songLengthInPixels) value = songLengthInPixels; - - if (value == scrollPositionInPixels) return value; - - // Difference in pixels. - var diff:Float = value - scrollPositionInPixels; - - this.scrollPositionInPixels = value; - - // Move the grid sprite to the correct position. - if (gridTiledSprite != null && gridPlayheadScrollArea != null) - { - if (isViewDownscroll) - { - gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); - gridPlayheadScrollArea.y = gridTiledSprite.y; - } - else - { - gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); - gridPlayheadScrollArea.y = gridTiledSprite.y; - } - } - - // Move the rendered notes to the correct position. - renderedNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); - renderedHoldNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); - renderedEvents.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); - renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); - // Offset the selection box start position, if we are dragging. - if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; - // Update the note preview viewport box. - setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); - return this.scrollPositionInPixels; - } - - /** - * Transitions to the Play State to test the song - */ - public function testSongInPlayState(minimal:Bool = false):Void - { - autoSave(); - - var startTimestamp:Float = 0; - if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; - - var targetSong:Song = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); - - // TODO: Rework asset system so we can remove this. - switch (currentSongStage) - { - case 'mainStage': - Paths.setCurrentLevel('week1'); - case 'spookyMansion': - Paths.setCurrentLevel('week2'); - case 'phillyTrain': - Paths.setCurrentLevel('week3'); - case 'limoRide': - Paths.setCurrentLevel('week4'); - case 'mallXmas' | 'mallEvil': - Paths.setCurrentLevel('week5'); - case 'school' | 'schoolEvil': - Paths.setCurrentLevel('week6'); - case 'tankmanBattlefield': - Paths.setCurrentLevel('week7'); - case 'phillyStreets' | 'phillyBlazin': - Paths.setCurrentLevel('weekend1'); - } - - subStateClosed.add(fixCamera); - subStateClosed.add(resetConductorAfterTest); - - FlxTransitionableState.skipNextTransIn = false; - FlxTransitionableState.skipNextTransOut = false; - - var targetState = new PlayState( - { - targetSong: targetSong, - targetDifficulty: selectedDifficulty, - // TODO: Add this. - // targetCharacter: targetCharacter, - practiceMode: true, - minimalMode: minimal, - startTimestamp: startTimestamp, - overrideMusic: true, - }); - - // Override music. - if (audioInstTrack != null) FlxG.sound.music = audioInstTrack; - if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup; - - openSubState(targetState); - } - - function fixCamera(_:FlxSubState = null):Void - { - FlxG.cameras.reset(new FlxCamera()); - FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2)); - FlxG.camera.zoom = 1.0; - - add(this.component); - } - - function resetConductorAfterTest(_:FlxSubState = null):Void - { - moveSongToScrollPosition(); - } - - public function postLoadInstrumental():Void - { - if (audioInstTrack != null) - { - // Prevent the time from skipping back to 0 when the song ends. - audioInstTrack.onComplete = function() { - if (audioInstTrack != null) audioInstTrack.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); - }; - - songLengthInMs = audioInstTrack.length; - - if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; - if (gridPlayheadScrollArea != null) - { - gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); - gridPlayheadScrollArea.updateHitbox(); - } - - buildSpectrogram(audioInstTrack); - } - else - { - trace('[WARN] Instrumental track was null!'); - } - - // Pretty much everything is going to need to be reset. - scrollPositionInPixels = 0; - playheadPositionInPixels = 0; - notePreviewDirty = true; - notePreviewViewportBoundsDirty = true; - noteDisplayDirty = true; - healthIconsDirty = true; - moveSongToScrollPosition(); - } - - /** - * Clear the voices group. - */ - public function clearVocals():Void - { - if (audioVocalTrackGroup != null) 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. - */ - function moveSongToScrollPosition():Void - { - // Update the songPosition in the audio tracks. - if (audioInstTrack != null) - { - audioInstTrack.time = scrollPositionInMs + playheadPositionInMs; - // Update the songPosition in the Conductor. - Conductor.update(audioInstTrack.time); - } - if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = scrollPositionInMs + playheadPositionInMs; - - // We need to update the note sprites because we changed the scroll position. - noteDisplayDirty = true; - } - - 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. - * - * @param command The command to perform. - * @param purgeRedoStack If true, the redo stack will be cleared. - */ - function performCommand(command:ChartEditorCommand, purgeRedoStack:Bool = true):Void - { - command.execute(this); - undoHistory.push(command); - commandHistoryDirty = true; - if (purgeRedoStack) redoHistory = []; - } - - /** - * Undo a command, then add it to the redo stack. - * @param command The command to undo. - */ - function undoCommand(command:ChartEditorCommand):Void - { - command.undo(this); - redoHistory.push(command); - commandHistoryDirty = true; - } - - /** - * Undo the last command in the undo stack, then add it to the redo stack. - */ - function undoLastCommand():Void - { - var command:Null<ChartEditorCommand> = undoHistory.pop(); - if (command == null) - { - trace('No actions to undo.'); - return; - } - undoCommand(command); - } - - /** - * Redo the last command in the redo stack, then add it to the undo stack. - */ - function redoLastCommand():Void - { - var command:Null<ChartEditorCommand> = redoHistory.pop(); - if (command == null) - { - trace('No actions to redo.'); - return; - } - performCommand(command, false); - } - - function sortChartData():Void - { - // TODO: .insertionSort() - currentSongChartNoteData.sort(function(a:SongNoteData, b:SongNoteData):Int { - return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time); - }); - - // TODO: .insertionSort() - currentSongChartEventData.sort(function(a:SongEventData, b:SongEventData):Int { - return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time); - }); - } - - function playMetronomeTick(high:Bool = false):Void - { - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/metronome${high ? '1' : '2'}')); - } - - function isNoteSelected(note:Null<SongNoteData>):Bool - { - return note != null && currentNoteSelection.indexOf(note) != -1; - } - - function isEventSelected(event:Null<SongEventData>):Bool - { - return event != null && currentEventSelection.indexOf(event) != -1; - } - - override function destroy():Void - { - super.destroy(); - - cleanupAutoSave(); - - // Hide the mouse cursor on other states. - Cursor.hide(); - - @:privateAccess - ChartEditorNoteSprite.noteFrameCollection = null; - } - - /** - * Dismiss any existing notifications, if there are any. - */ - function dismissNotifications():Void + public static function dismissNotifications():Void { NotificationManager.instance.clearNotifications(); } + + /** + * Convert a note data value into a chart editor grid column number. + */ + public static function noteDataToGridColumn(input:Int):Int + { + if (input < 0) input = 0; + if (input >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1)) + { + // Don't invert the Event column. + input = (ChartEditorState.STRUMLINE_SIZE * 2 + 1); + } + else + { + // Invert player and opponent columns. + if (input >= ChartEditorState.STRUMLINE_SIZE) + { + input -= ChartEditorState.STRUMLINE_SIZE; + } + else + { + input += ChartEditorState.STRUMLINE_SIZE; + } + } + return input; + } + + /** + * Convert a chart editor grid column number into a note data value. + */ + public static function gridColumnToNoteData(input:Int):Int + { + if (input < 0) input = 0; + if (input >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1)) + { + // Don't invert the Event column. + input = (ChartEditorState.STRUMLINE_SIZE * 2 + 1); + } + else + { + // Invert player and opponent columns. + if (input >= ChartEditorState.STRUMLINE_SIZE) + { + input -= ChartEditorState.STRUMLINE_SIZE; + } + else + { + input += ChartEditorState.STRUMLINE_SIZE; + } + } + return input; + } } -enum LiveInputStyle +/** + * Available input modes for the chart editor state. + */ +enum ChartEditorLiveInputStyle { + /** + * No hotkeys to place notes at the playbar. + */ None; + + /** + * 1/2/3/4 to place notes on opponent's side, 5/6/7/8 to place notes on player's side. + */ NumberKeys; + + /** + * WASD to place notes on opponent's side, arrow keys to place notes on player's side. + */ WASD; } + +/** + * Available themes for the chart editor state. + */ +enum ChartEditorTheme +{ + /** + * The default theme for the chart editor. + */ + Light; + + /** + * A theme which introduces darker colors. + */ + Dark; +} diff --git a/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx b/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx new file mode 100644 index 000000000..9bf8ec3db --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx @@ -0,0 +1,67 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; + +/** + * Adds the given events to the current chart in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class AddEventsCommand implements ChartEditorCommand +{ + var events:Array<SongEventData>; + var appendToSelection:Bool; + + public function new(events:Array<SongEventData>, appendToSelection:Bool = false) + { + this.events = events; + this.appendToSelection = appendToSelection; + } + + public function execute(state:ChartEditorState):Void + { + for (event in events) + { + state.currentSongChartEventData.push(event); + } + + if (appendToSelection) + { + state.currentEventSelection = state.currentEventSelection.concat(events); + } + else + { + state.currentNoteSelection = []; + state.currentEventSelection = events; + } + + state.playSound(Paths.sound('chartingSounds/noteLay')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); + + state.currentNoteSelection = []; + state.currentEventSelection = []; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + var len:Int = events.length; + return 'Add $len Events'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx b/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx new file mode 100644 index 000000000..ce4e73ea2 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx @@ -0,0 +1,72 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; + +/** + * Adds the given notes to the current chart in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class AddNotesCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + var appendToSelection:Bool; + + public function new(notes:Array<SongNoteData>, appendToSelection:Bool = false) + { + this.notes = notes; + this.appendToSelection = appendToSelection; + } + + public function execute(state:ChartEditorState):Void + { + for (note in notes) + { + state.currentSongChartNoteData.push(note); + } + + if (appendToSelection) + { + state.currentNoteSelection = state.currentNoteSelection.concat(notes); + } + else + { + state.currentNoteSelection = notes; + state.currentEventSelection = []; + } + + state.playSound(Paths.sound('chartingSounds/noteLay')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); + state.currentNoteSelection = []; + state.currentEventSelection = []; + state.playSound(Paths.sound('chartingSounds/undo')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + if (notes.length == 1) + { + var dir:String = notes[0].getDirectionName(); + return 'Add $dir Note'; + } + + return 'Add ${notes.length} Notes'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx new file mode 100644 index 000000000..cfa169908 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx @@ -0,0 +1,30 @@ +package funkin.ui.debug.charting.commands; + +/** + * Actions in the chart editor are backed by the Command pattern + * (see Bob Nystrom's book "Game Programming Patterns" for more info) + * + * To make a functionality compatible with the undo/redo history, create a new class + * that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())` + */ +interface ChartEditorCommand +{ + /** + * Calling this function should perform the action that this command represents. + * @param state The ChartEditorState to perform the action on. + */ + public function execute(state:ChartEditorState):Void; + + /** + * Calling this function should perform the inverse of the action that this command represents, + * effectively undoing the action. Assume that the original action was the last action performed. + * @param state The ChartEditorState to undo the action on. + */ + public function undo(state:ChartEditorState):Void; + + /** + * Get a short description of the action (for the UI). + * For example, return `Add Left Note` to display `Undo Add Left Note` in the menu. + */ + public function toString():String; +} diff --git a/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx b/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx new file mode 100644 index 000000000..d0301b1ec --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx @@ -0,0 +1,68 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; + +/** + * Command that copies a given set of notes and song events to the clipboard, + * and then deletes them from the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class CutItemsCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + var events:Array<SongEventData>; + + public function new(notes:Array<SongNoteData>, events:Array<SongEventData>) + { + this.notes = notes; + this.events = events; + } + + public function execute(state:ChartEditorState):Void + { + // Copy the notes. + SongDataUtils.writeItemsToClipboard( + { + notes: SongDataUtils.buildNoteClipboard(notes), + events: SongDataUtils.buildEventClipboard(events) + }); + + // Delete the notes. + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); + state.currentNoteSelection = []; + state.currentEventSelection = []; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); + state.currentSongChartEventData = state.currentSongChartEventData.concat(events); + + state.currentNoteSelection = notes; + state.currentEventSelection = events; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + state.sortChartData(); + } + + public function toString():String + { + var len:Int = notes.length + events.length; + + if (notes.length == 0) return 'Cut $len Events to Clipboard'; + else if (events.length == 0) return 'Cut $len Notes to Clipboard'; + else + return 'Cut $len Items to Clipboard'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx b/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx new file mode 100644 index 000000000..cbde0ab3d --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx @@ -0,0 +1,42 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; + +/** + * Command that deselects all selected notes and events in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class DeselectAllItemsCommand implements ChartEditorCommand +{ + var previousNoteSelection:Array<SongNoteData>; + var previousEventSelection:Array<SongEventData>; + + public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>) + { + this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; + this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; + } + + public function execute(state:ChartEditorState):Void + { + state.currentNoteSelection = []; + state.currentEventSelection = []; + + state.noteDisplayDirty = true; + } + + public function undo(state:ChartEditorState):Void + { + state.currentNoteSelection = previousNoteSelection; + state.currentEventSelection = previousEventSelection; + + state.noteDisplayDirty = true; + } + + public function toString():String + { + return 'Deselect All Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx new file mode 100644 index 000000000..d679b5363 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx @@ -0,0 +1,60 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; + +/** + * Command to deselect a specific set of notes and events in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class DeselectItemsCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + var events:Array<SongEventData>; + + public function new(notes:Array<SongNoteData>, events:Array<SongEventData>) + { + this.notes = notes; + this.events = events; + } + + public function execute(state:ChartEditorState):Void + { + state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes); + state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events); + + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + } + + public function undo(state:ChartEditorState):Void + { + for (note in this.notes) + { + state.currentNoteSelection.push(note); + } + + for (event in this.events) + { + state.currentEventSelection.push(event); + } + + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + } + + public function toString():String + { + var noteCount = notes.length + events.length; + + if (noteCount == 1) + { + var dir:String = notes[0].getDirectionName(); + return 'Deselect $dir Items'; + } + + return 'Deselect ${noteCount} Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx new file mode 100644 index 000000000..47da0dde5 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx @@ -0,0 +1,52 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; + +/** + * Command that modifies the length of a hold note in the chart editor. + * If it is not a hold note, it will become one, and if it is already a hold note, its length will change. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class ExtendNoteLengthCommand implements ChartEditorCommand +{ + var note:SongNoteData; + var oldLength:Float; + var newLength:Float; + + public function new(note:SongNoteData, newLength:Float) + { + this.note = note; + this.oldLength = note.length; + this.newLength = newLength; + } + + public function execute(state:ChartEditorState):Void + { + note.length = newLength; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + state.playSound(Paths.sound('chartingSounds/undo')); + + note.length = oldLength; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + return 'Extend Note Length'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx b/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx new file mode 100644 index 000000000..da8ec7fbc --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx @@ -0,0 +1,59 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; + +/** + * Command that flips a given array of notes from the player's side of the chart editor to the opponent's side, or vice versa. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class FlipNotesCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData> = []; + var flippedNotes:Array<SongNoteData> = []; + + public function new(notes:Array<SongNoteData>) + { + this.notes = notes; + this.flippedNotes = SongDataUtils.flipNotes(notes); + } + + public function execute(state:ChartEditorState):Void + { + // Delete the notes. + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); + + // Add the flipped notes. + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(flippedNotes); + + state.currentNoteSelection = flippedNotes; + state.currentEventSelection = []; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, flippedNotes); + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); + + state.currentNoteSelection = notes; + state.currentEventSelection = []; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + var len:Int = notes.length; + return 'Flip $len Notes'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx b/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx new file mode 100644 index 000000000..6e37bcc03 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx @@ -0,0 +1,43 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; + +/** + * Command to deselect all items that are currently selected in the chart editor, + * then select all the items that were previously unselected. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class InvertSelectedItemsCommand implements ChartEditorCommand +{ + var previousNoteSelection:Array<SongNoteData>; + var previousEventSelection:Array<SongEventData>; + + public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>) + { + this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; + this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; + } + + public function execute(state:ChartEditorState):Void + { + state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection); + state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection); + state.noteDisplayDirty = true; + } + + public function undo(state:ChartEditorState):Void + { + state.currentNoteSelection = previousNoteSelection; + state.currentEventSelection = previousEventSelection; + + state.noteDisplayDirty = true; + } + + public function toString():String + { + return 'Invert Selected Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx new file mode 100644 index 000000000..09b81c4d3 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx @@ -0,0 +1,72 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; + +/** + * Move the given events by the given offset and shift them by the given number of columns in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class MoveEventsCommand implements ChartEditorCommand +{ + var events:Array<SongEventData>; + var movedEvents:Array<SongEventData>; + var offset:Float; + + public function new(notes:Array<SongEventData>, offset:Float) + { + // Clone the notes to prevent editing from affecting the history. + this.events = [for (event in events) event.clone()]; + this.offset = offset; + this.movedEvents = []; + } + + public function execute(state:ChartEditorState):Void + { + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); + + movedEvents = []; + + for (event in events) + { + // Clone the notes to prevent editing from affecting the history. + var resultEvent = event.clone(); + resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); + + movedEvents.push(resultEvent); + } + + state.currentSongChartEventData = state.currentSongChartEventData.concat(movedEvents); + state.currentEventSelection = movedEvents; + + state.playSound(Paths.sound('chartingSounds/noteLay')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, movedEvents); + state.currentSongChartEventData = state.currentSongChartEventData.concat(events); + + state.currentEventSelection = events; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + var len:Int = events.length; + return 'Move $len Events'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx new file mode 100644 index 000000000..2eedbbf03 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx @@ -0,0 +1,96 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; + +/** + * Move the given notes by the given offset and shift them by the given number of columns in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class MoveItemsCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + var movedNotes:Array<SongNoteData>; + var events:Array<SongEventData>; + var movedEvents:Array<SongEventData>; + var offset:Float; + var columns:Int; + + public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, offset:Float, columns:Int) + { + // Clone the notes to prevent editing from affecting the history. + this.notes = [for (note in notes) note.clone()]; + this.events = [for (event in events) event.clone()]; + this.offset = offset; + this.columns = columns; + this.movedNotes = []; + this.movedEvents = []; + } + + public function execute(state:ChartEditorState):Void + { + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); + + movedNotes = []; + movedEvents = []; + + for (note in notes) + { + // Clone the notes to prevent editing from affecting the history. + var resultNote = note.clone(); + resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); + resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0, + ChartEditorState.STRUMLINE_SIZE * 2 - 1)); + + movedNotes.push(resultNote); + } + + for (event in events) + { + // Clone the notes to prevent editing from affecting the history. + var resultEvent = event.clone(); + resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); + + movedEvents.push(resultEvent); + } + + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(movedNotes); + state.currentSongChartEventData = state.currentSongChartEventData.concat(movedEvents); + state.currentNoteSelection = movedNotes; + state.currentEventSelection = movedEvents; + + state.playSound(Paths.sound('chartingSounds/noteLay')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, movedNotes); + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, movedEvents); + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); + state.currentSongChartEventData = state.currentSongChartEventData.concat(events); + + state.currentNoteSelection = notes; + state.currentEventSelection = events; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + var len:Int = notes.length + events.length; + return 'Move $len Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx new file mode 100644 index 000000000..8bce747a1 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx @@ -0,0 +1,75 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; + +/** + * Move the given notes by the given offset and shift them by the given number of columns in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class MoveNotesCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + var movedNotes:Array<SongNoteData>; + var offset:Float; + var columns:Int; + + public function new(notes:Array<SongNoteData>, offset:Float, columns:Int) + { + // Clone the notes to prevent editing from affecting the history. + this.notes = [for (note in notes) note.clone()]; + this.offset = offset; + this.columns = columns; + this.movedNotes = []; + } + + public function execute(state:ChartEditorState):Void + { + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); + + movedNotes = []; + + for (note in notes) + { + // Clone the notes to prevent editing from affecting the history. + var resultNote = note.clone(); + resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); + resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0, + ChartEditorState.STRUMLINE_SIZE * 2 - 1)); + + movedNotes.push(resultNote); + } + + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(movedNotes); + state.currentNoteSelection = movedNotes; + + state.playSound(Paths.sound('chartingSounds/noteLay')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, movedNotes); + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); + + state.currentNoteSelection = notes; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + var len:Int = notes.length; + return 'Move $len Notes'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx new file mode 100644 index 000000000..12115ba8a --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -0,0 +1,99 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; +import funkin.data.song.SongDataUtils.SongClipboardItems; +import haxe.ui.notifications.NotificationManager; +import haxe.ui.notifications.NotificationType; + +/** + * A command which inserts the contents of the clipboard into the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class PasteItemsCommand implements ChartEditorCommand +{ + var targetTimestamp:Float; + // Notes we added with this command, for undo. + var addedNotes:Array<SongNoteData> = []; + var addedEvents:Array<SongEventData> = []; + + public function new(targetTimestamp:Float) + { + this.targetTimestamp = targetTimestamp; + } + + public function execute(state:ChartEditorState):Void + { + var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard(); + + if (currentClipboard.valid != true) + { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Failed to Paste', + body: 'Could not parse clipboard contents.', + type: NotificationType.Error, + expiryMs: Constants.NOTIFICATION_DISMISS_TIME + }); + #end + return; + } + + trace(currentClipboard.notes); + + addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp)); + addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); + + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); + state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); + state.currentNoteSelection = addedNotes.copy(); + state.currentEventSelection = addedEvents.copy(); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Paste Successful', + body: 'Successfully pasted clipboard contents.', + type: NotificationType.Success, + expiryMs: Constants.NOTIFICATION_DISMISS_TIME + }); + #end + } + + public function undo(state:ChartEditorState):Void + { + state.playSound(Paths.sound('chartingSounds/undo')); + + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents); + state.currentNoteSelection = []; + state.currentEventSelection = []; + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard(); + + var len:Int = currentClipboard.notes.length + currentClipboard.events.length; + + if (currentClipboard.notes.length == 0) return 'Paste $len Events'; + else if (currentClipboard.events.length == 0) return 'Paste $len Notes'; + else + return 'Paste $len Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx new file mode 100644 index 000000000..7e620c210 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx @@ -0,0 +1,60 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; + +/** + * Deletes the given events from the current chart in the chart editor. + * Use only when ONLY events are being deleted. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class RemoveEventsCommand implements ChartEditorCommand +{ + var events:Array<SongEventData>; + + public function new(events:Array<SongEventData>) + { + this.events = events; + } + + public function execute(state:ChartEditorState):Void + { + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); + state.currentEventSelection = []; + + state.playSound(Paths.sound('chartingSounds/noteErase')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + for (event in events) + { + state.currentSongChartEventData.push(event); + } + state.currentEventSelection = events; + state.playSound(Paths.sound('chartingSounds/undo')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + if (events.length == 1 && events[0] != null) + { + return 'Remove Event'; + } + + return 'Remove ${events.length} Events'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx new file mode 100644 index 000000000..77184209e --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx @@ -0,0 +1,69 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; + +/** + * Deletes the given notes and events from the current chart in the chart editor. + * Use only when BOTH notes and events are being deleted. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class RemoveItemsCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + var events:Array<SongEventData>; + + public function new(notes:Array<SongNoteData>, events:Array<SongEventData>) + { + this.notes = notes; + this.events = events; + } + + public function execute(state:ChartEditorState):Void + { + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); + state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); + + state.currentNoteSelection = []; + state.currentEventSelection = []; + + state.playSound(Paths.sound('chartingSounds/noteErase')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + for (note in notes) + { + state.currentSongChartNoteData.push(note); + } + + for (event in events) + { + state.currentSongChartEventData.push(event); + } + + state.currentNoteSelection = notes; + state.currentEventSelection = events; + + state.playSound(Paths.sound('chartingSounds/undo')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + return 'Remove ${notes.length + events.length} Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx new file mode 100644 index 000000000..e189be83e --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx @@ -0,0 +1,63 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; + +/** + * Deletes the given notes from the current chart in the chart editor. + * Use only when ONLY notes are being deleted. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class RemoveNotesCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + + public function new(notes:Array<SongNoteData>) + { + this.notes = notes; + } + + public function execute(state:ChartEditorState):Void + { + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); + state.currentNoteSelection = []; + state.currentEventSelection = []; + + state.playSound(Paths.sound('chartingSounds/noteErase')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function undo(state:ChartEditorState):Void + { + for (note in notes) + { + state.currentSongChartNoteData.push(note); + } + state.currentNoteSelection = notes; + state.currentEventSelection = []; + state.playSound(Paths.sound('chartingSounds/undo')); + + state.saveDataDirty = true; + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + + state.sortChartData(); + } + + public function toString():String + { + if (notes.length == 1 && notes[0] != null) + { + var dir:String = notes[0].getDirectionName(); + return 'Remove $dir Note'; + } + + return 'Remove ${notes.length} Notes'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx new file mode 100644 index 000000000..e1a4dceaa --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx @@ -0,0 +1,42 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; + +/** + * Command to set the selection to all notes and events in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class SelectAllItemsCommand implements ChartEditorCommand +{ + var previousNoteSelection:Array<SongNoteData>; + var previousEventSelection:Array<SongEventData>; + + public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>) + { + this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; + this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; + } + + public function execute(state:ChartEditorState):Void + { + state.currentNoteSelection = state.currentSongChartNoteData; + state.currentEventSelection = state.currentSongChartEventData; + + state.noteDisplayDirty = true; + } + + public function undo(state:ChartEditorState):Void + { + state.currentNoteSelection = previousNoteSelection; + state.currentEventSelection = previousEventSelection; + + state.noteDisplayDirty = true; + } + + public function toString():String + { + return 'Select All Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx new file mode 100644 index 000000000..abe8b9e35 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx @@ -0,0 +1,78 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; + +/** + * Appends one or more items to the selection in the chart editor. + * This does not deselect any items that are already selected, if any. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class SelectItemsCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + var events:Array<SongEventData>; + + public function new(notes:Array<SongNoteData>, events:Array<SongEventData>) + { + this.notes = notes; + this.events = events; + } + + public function execute(state:ChartEditorState):Void + { + for (note in this.notes) + { + state.currentNoteSelection.push(note); + } + + for (event in this.events) + { + state.currentEventSelection.push(event); + } + + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + } + + public function undo(state:ChartEditorState):Void + { + state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes); + state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events); + + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + } + + public function toString():String + { + var len:Int = notes.length + events.length; + + if (notes.length == 0) + { + if (events.length == 1) + { + return 'Select Event'; + } + else + { + return 'Select ${events.length} Events'; + } + } + else if (events.length == 0) + { + if (notes.length == 1) + { + return 'Select Note'; + } + else + { + return 'Select ${notes.length} Notes'; + } + } + + return 'Select ${len} Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx new file mode 100644 index 000000000..a06aefabc --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx @@ -0,0 +1,48 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; + +/** + * Command to set the current selection in the chart editor (rather than appending it). + * Deselects any notes that are not in the new selection. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class SetItemSelectionCommand implements ChartEditorCommand +{ + var notes:Array<SongNoteData>; + var events:Array<SongEventData>; + var previousNoteSelection:Array<SongNoteData>; + var previousEventSelection:Array<SongEventData>; + + public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>, + previousEventSelection:Array<SongEventData>) + { + this.notes = notes; + this.events = events; + this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; + this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; + } + + public function execute(state:ChartEditorState):Void + { + state.currentNoteSelection = notes; + state.currentEventSelection = events; + + state.noteDisplayDirty = true; + } + + public function undo(state:ChartEditorState):Void + { + state.currentNoteSelection = previousNoteSelection; + state.currentEventSelection = previousEventSelection; + + state.noteDisplayDirty = true; + } + + public function toString():String + { + return 'Select ${notes.length} Items'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx b/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx new file mode 100644 index 000000000..75e7e5afe --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx @@ -0,0 +1,45 @@ +package funkin.ui.debug.charting.commands; + +/** + * Switch the current difficulty (and possibly variation) of the chart in the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class SwitchDifficultyCommand implements ChartEditorCommand +{ + var prevDifficulty:String; + var newDifficulty:String; + var prevVariation:String; + var newVariation:String; + + public function new(prevDifficulty:String, newDifficulty:String, prevVariation:String, newVariation:String) + { + this.prevDifficulty = prevDifficulty; + this.newDifficulty = newDifficulty; + this.prevVariation = prevVariation; + this.newVariation = newVariation; + } + + public function execute(state:ChartEditorState):Void + { + state.selectedVariation = newVariation != null ? newVariation : prevVariation; + state.selectedDifficulty = newDifficulty != null ? newDifficulty : prevDifficulty; + + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + } + + public function undo(state:ChartEditorState):Void + { + state.selectedVariation = prevVariation != null ? prevVariation : newVariation; + state.selectedDifficulty = prevDifficulty != null ? prevDifficulty : newDifficulty; + + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + } + + public function toString():String + { + return 'Switch Difficulty'; + } +} diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx similarity index 91% rename from source/funkin/ui/debug/charting/ChartEditorEventSprite.hx rename to source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx index 021abde0f..2bd719df2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx @@ -1,4 +1,4 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.components; import funkin.data.event.SongEventData.SongEventParser; import flixel.graphics.frames.FlxAtlasFrames; @@ -13,7 +13,7 @@ import flixel.math.FlxPoint; import funkin.data.song.SongData.SongEventData; /** - * A event sprite that can be used to display a song event in a chart. + * A sprite that can be used to display a song event in a chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ @:nullSafety @@ -34,6 +34,17 @@ class ChartEditorEventSprite extends FlxSprite */ static var eventSpriteBasic:Null<BitmapData> = null; + public var overrideStepTime(default, set):Null<Float> = null; + + function set_overrideStepTime(value:Null<Float>):Null<Float> + { + if (overrideStepTime == value) return overrideStepTime; + + overrideStepTime = value; + updateEventPosition(); + return overrideStepTime; + } + public function new(parent:ChartEditorState) { super(); @@ -146,7 +157,7 @@ class ChartEditorEventSprite extends FlxSprite this.x = (ChartEditorState.STRUMLINE_SIZE * 2 + 1 - 1) * ChartEditorState.GRID_SIZE; - var stepTime:Float = inline eventData.getStepTime(); + var stepTime:Float = (overrideStepTime != null) ? overrideStepTime : eventData.getStepTime(); this.y = stepTime * ChartEditorState.GRID_SIZE; if (origin != null) diff --git a/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx similarity index 97% rename from source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx rename to source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index 59d84647a..0e8c02758 100644 --- a/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -1,4 +1,4 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.components; import funkin.play.notes.Strumline; import funkin.data.notestyle.NoteStyleRegistry; @@ -11,7 +11,7 @@ import funkin.play.notes.SustainTrail; import funkin.data.song.SongData.SongNoteData; /** - * A hold note sprite that can be used to display a note in a chart. + * A sprite that can be used to display the trail of a hold note in a chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ @:nullSafety diff --git a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx similarity index 98% rename from source/funkin/ui/debug/charting/ChartEditorNotePreview.hx rename to source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx index 6119141cc..7decc8988 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx @@ -1,4 +1,4 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.components; import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; diff --git a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx similarity index 85% rename from source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx rename to source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx index 77954087b..cd403c6f8 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx @@ -1,4 +1,4 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.components; import flixel.FlxObject; import flixel.FlxSprite; @@ -10,10 +10,11 @@ import flixel.math.FlxPoint; import funkin.data.song.SongData.SongNoteData; /** - * A note sprite that can be used to display a note in a chart. + * A sprite that can be used to display a note in a chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ @:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorNoteSprite extends FlxSprite { /** @@ -37,6 +38,28 @@ class ChartEditorNoteSprite extends FlxSprite */ public var noteStyle(get, never):String; + public var overrideStepTime(default, set):Null<Float> = null; + + function set_overrideStepTime(value:Null<Float>):Null<Float> + { + if (overrideStepTime == value) return overrideStepTime; + + overrideStepTime = value; + updateNotePosition(); + return overrideStepTime; + } + + public var overrideData(default, set):Null<Int> = null; + + function set_overrideData(value:Null<Int>):Null<Int> + { + if (overrideData == value) return overrideData; + + overrideData = value; + playNoteAnimation(); + return overrideData; + } + public function new(parent:ChartEditorState) { super(); @@ -147,32 +170,15 @@ class ChartEditorNoteSprite extends FlxSprite { if (this.noteData == null) return; - var cursorColumn:Int = this.noteData.data; + var cursorColumn:Int = (overrideData != null) ? overrideData : this.noteData.data; - if (cursorColumn < 0) cursorColumn = 0; - if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1)) - { - cursorColumn = (ChartEditorState.STRUMLINE_SIZE * 2 + 1); - } - else - { - // Invert player and opponent columns. - if (cursorColumn >= ChartEditorState.STRUMLINE_SIZE) - { - cursorColumn -= ChartEditorState.STRUMLINE_SIZE; - } - else - { - cursorColumn += ChartEditorState.STRUMLINE_SIZE; - } - } + cursorColumn = ChartEditorState.noteDataToGridColumn(cursorColumn); this.x = cursorColumn * ChartEditorState.GRID_SIZE; // Notes far in the song will start far down, but the group they belong to will have a high negative offset. // noteData.getStepTime() returns a calculated value which accounts for BPM changes - var stepTime:Float = - inline this.noteData.getStepTime(); + var stepTime:Float = (overrideStepTime != null) ? overrideStepTime : noteData.getStepTime(); if (stepTime >= 0) { this.y = stepTime * ChartEditorState.GRID_SIZE; @@ -199,7 +205,8 @@ class ChartEditorNoteSprite extends FlxSprite var baseAnimationName:String = 'tap'; // Play the appropriate animation for the type, direction, and skin. - var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle.toTitleCase()}'; + var dirName:String = overrideData != null ? SongNoteData.buildDirectionName(overrideData) : this.noteData.getDirectionName(); + var animationName:String = '${baseAnimationName}${dirName}${this.noteStyle.toTitleCase()}'; this.animation.play(animationName); diff --git a/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx new file mode 100644 index 000000000..8f7c4aaec --- /dev/null +++ b/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx @@ -0,0 +1,20 @@ +package funkin.ui.debug.charting.components; + +import flixel.FlxSprite; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; + +/** + * A sprite that can be used to display a square over a selected note or event in the chart. + * Designed to be used and reused efficiently. Has no gameplay functionality. + */ +class ChartEditorSelectionSquareSprite extends FlxSprite +{ + public var noteData:Null<SongNoteData>; + public var eventData:Null<SongEventData>; + + public function new() + { + super(); + } +} diff --git a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx similarity index 75% rename from source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx rename to source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index b5a6f36be..072004a43 100644 --- a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -1,22 +1,21 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.handlers; import flixel.system.FlxAssets.FlxSoundAsset; import flixel.system.FlxSound; -import flixel.system.FlxSound; import funkin.audio.VoicesGroup; import funkin.play.character.BaseCharacter.CharacterType; import funkin.util.FileUtil; +import funkin.util.assets.SoundUtil; import haxe.io.Bytes; import haxe.io.Path; import openfl.utils.Assets; /** * Functions for loading audio for the chart editor. + * Handlers split up the functionality of the Chart Editor into different classes based on focus to limit the amount of code in each class. */ @:nullSafety -@:allow(funkin.ui.debug.charting.ChartEditorState) -@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler) -@:allow(funkin.ui.debug.charting.ChartEditorImportExportHandler) +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorAudioHandler { /** @@ -27,7 +26,7 @@ class ChartEditorAudioHandler * @param instId The instrumental this vocal track will be for. * @return Success or failure. */ - static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool + public static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool { #if sys var fileBytes:Bytes = sys.io.File.getBytes(path.toString()); @@ -46,7 +45,7 @@ class ChartEditorAudioHandler * @param instId The instrumental this vocal track will be for. * @return Success or failure. */ - static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool + public static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool { var trackData:Null<Bytes> = Assets.getBytes(path); if (trackData != null) @@ -63,7 +62,7 @@ class ChartEditorAudioHandler * @param charId The character this vocal track will be for. * @param instId The instrumental this vocal track will be for. */ - static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool + public static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool { var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}'; state.audioVocalTrackData.set(trackId, bytes); @@ -77,7 +76,7 @@ class ChartEditorAudioHandler * @param instId The instrumental this vocal track will be for. * @return Success or failure. */ - static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool + public static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool { #if sys var fileBytes:Bytes = sys.io.File.getBytes(path.toString()); @@ -95,7 +94,7 @@ class ChartEditorAudioHandler * @param instId The instrumental this vocal track will be for. * @return Success or failure. */ - static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool + public static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool { var trackData:Null<Bytes> = Assets.getBytes(path); if (trackData != null) @@ -112,7 +111,7 @@ class ChartEditorAudioHandler * @param charId The character this vocal track will be for. * @param instId The instrumental this vocal track will be for. */ - static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool + public static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool { if (instId == '') instId = 'default'; state.audioInstTrackData.set(instId, bytes); @@ -136,11 +135,11 @@ class ChartEditorAudioHandler /** * Tell the Chart Editor to select a specific instrumental track, that is already loaded. */ - static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool + public static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool { if (instId == '') instId = 'default'; var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId); - var instTrack:Null<FlxSound> = buildFlxSoundFromBytes(instTrackData); + var instTrack:Null<FlxSound> = SoundUtil.buildFlxSoundFromBytes(instTrackData); if (instTrack == null) return false; stopExistingInstrumental(state); @@ -149,7 +148,7 @@ class ChartEditorAudioHandler return true; } - static function stopExistingInstrumental(state:ChartEditorState):Void + public static function stopExistingInstrumental(state:ChartEditorState):Void { if (state.audioInstTrack != null) { @@ -162,11 +161,11 @@ class ChartEditorAudioHandler /** * Tell the Chart Editor to select a specific vocal track, that is already loaded. */ - static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool + public static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool { var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}'; var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId); - var vocalTrack:Null<FlxSound> = buildFlxSoundFromBytes(vocalTrackData); + var vocalTrack:Null<FlxSound> = SoundUtil.buildFlxSoundFromBytes(vocalTrackData); if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup(); @@ -190,7 +189,7 @@ class ChartEditorAudioHandler return false; } - static function stopExistingVocals(state:ChartEditorState):Void + public static function stopExistingVocals(state:ChartEditorState):Void { if (state.audioVocalTrackGroup != null) { @@ -203,7 +202,7 @@ class ChartEditorAudioHandler * Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance. * @param path The path to the sound effect. Use `Paths` to build this. */ - public static function playSound(path:String):Void + public static function playSound(_state:ChartEditorState, path:String):Void { var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound(); var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path); @@ -219,22 +218,11 @@ class ChartEditorAudioHandler } /** - * Convert byte data into a playable sound. - * - * @param input The byte data. - * @return The playable sound, or `null` if loading failed. + * Create a list of ZIP file entries from the current loaded instrumental tracks in the chart eidtor. + * @param state The chart editor state. + * @return `Array<haxe.zip.Entry>` */ - public static function buildFlxSoundFromBytes(input:Null<Bytes>):Null<FlxSound> - { - if (input == null) return null; - - var openflSound:openfl.media.Sound = new openfl.media.Sound(); - openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length); - var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false); - return output; - } - - static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array<haxe.zip.Entry> + public static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array<haxe.zip.Entry> { var zipEntries = []; @@ -257,7 +245,12 @@ class ChartEditorAudioHandler return zipEntries; } - static function makeZIPEntriesFromVocals(state:ChartEditorState):Array<haxe.zip.Entry> + /** + * Create a list of ZIP file entries from the current loaded vocal tracks in the chart eidtor. + * @param state The chart editor state. + * @return `Array<haxe.zip.Entry>` + */ + public static function makeZIPEntriesFromVocals(state:ChartEditorState):Array<haxe.zip.Entry> { var zipEntries = []; diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx similarity index 95% rename from source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx rename to source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index c26f6c805..529707156 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -1,6 +1,5 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.handlers; -import funkin.ui.haxeui.components.FunkinDropDown; import flixel.util.FlxTimer; import funkin.data.song.importer.FNFLegacyData; import funkin.data.song.importer.FNFLegacyImporter; @@ -15,6 +14,8 @@ import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.song.Song; import funkin.play.stage.StageData; +import funkin.ui.debug.charting.util.ChartEditorDropdowns; +import funkin.ui.haxeui.components.FunkinDropDown; import funkin.ui.haxeui.components.FunkinLink; import funkin.util.Constants; import funkin.util.FileUtil; @@ -47,8 +48,10 @@ using Lambda; * Handles dialogs for the new Chart Editor. */ @:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorDialogHandler { + // Paths to HaxeUI layout files for each dialog. static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about'); static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome'); static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); @@ -67,7 +70,7 @@ class ChartEditorDialogHandler * @param state The current chart editor state. * @return The dialog that was opened. */ - public static inline function openAboutDialog(state:ChartEditorState):Null<Dialog> + public static function openAboutDialog(state:ChartEditorState):Null<Dialog> { return openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true); } @@ -158,7 +161,7 @@ class ChartEditorDialogHandler state.stopWelcomeMusic(); // Load song from template - ChartEditorImportExportHandler.loadSongAsTemplate(state, targetSongId); + state.loadSongAsTemplate(targetSongId); } splashTemplateContainer.addComponent(linkTemplateSong); @@ -402,7 +405,7 @@ class ChartEditorDialogHandler {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) { if (selectedFile != null && selectedFile.bytes != null) { - if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId)) + if (state.loadInstFromBytes(selectedFile.bytes, instId)) { #if !mac NotificationManager.instance.addNotification( @@ -410,7 +413,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end @@ -426,7 +429,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end } @@ -437,7 +440,7 @@ class ChartEditorDialogHandler onDropFile = function(pathStr:String) { var path:Path = new Path(pathStr); trace('Dropped file (${path})'); - if (ChartEditorAudioHandler.loadInstFromPath(state, path, instId)) + if (state.loadInstFromPath(path, instId)) { // Tell the user the load was successful. #if !mac @@ -446,7 +449,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end @@ -472,7 +475,7 @@ class ChartEditorDialogHandler title: 'Failure', body: message, type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end } @@ -483,66 +486,6 @@ class ChartEditorDialogHandler return dialog; } - static var dropHandlers:Array< - { - component:Component, - handler:(String->Void) - }> = []; - - /** - * Add a callback for when a file is dropped on a component. - * - * On OS X you can’t drop on the application window, but rather only the app icon - * (either in the dock while running or the icon on the hard drive) so this must be disabled - * and UI updated appropriately. - * @param component - * @param handler - */ - static function addDropHandler(component:Component, handler:String->Void):Void - { - #if desktop - if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile); - - dropHandlers.push( - { - component: component, - handler: handler - }); - #else - trace('addDropHandler not implemented for this platform'); - #end - } - - static function removeDropHandler(handler:String->Void):Void - { - #if desktop - FlxG.stage.window.onDropFile.remove(handler); - #end - } - - static function clearDropHandlers():Void - { - #if desktop - dropHandlers = []; - FlxG.stage.window.onDropFile.remove(onDropFile); - #end - } - - static function onDropFile(path:String):Void - { - // a VERY short timer to wait for the mouse position to update - new FlxTimer().start(0.01, function(_) { - for (handler in dropHandlers) - { - if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY)) - { - handler.handler(path); - return; - } - } - }); - } - /** * Opens the dialog in the wizard where the user can set song metadata like name and artist and BPM. * @param state The ChartEditorState instance. @@ -722,7 +665,7 @@ class ChartEditorDialogHandler if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog'; dialogNoVocals.onClick = function(_event) { // Dismiss - ChartEditorAudioHandler.stopExistingVocals(state); + state.stopExistingVocals(); dialog.hideDialog(DialogButton.APPLY); }; @@ -749,10 +692,10 @@ class ChartEditorDialogHandler if (!hasClearedVocals) { hasClearedVocals = true; - ChartEditorAudioHandler.stopExistingVocals(state); + state.stopExistingVocals(); } - if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey, instId)) + if (state.loadVocalsFromPath(path, charKey, instId)) { // Tell the user the load was successful. #if !mac @@ -761,7 +704,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end #if FILE_DROP_SUPPORTED @@ -784,7 +727,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end @@ -805,9 +748,9 @@ class ChartEditorDialogHandler if (!hasClearedVocals) { hasClearedVocals = true; - ChartEditorAudioHandler.stopExistingVocals(state); + state.stopExistingVocals(); } - if (ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey, instId)) + if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId)) { // Tell the user the load was successful. #if !mac @@ -816,7 +759,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end #if FILE_DROP_SUPPORTED @@ -837,7 +780,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end @@ -897,7 +840,7 @@ class ChartEditorDialogHandler var buttonContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); if (buttonContinue == null) throw 'Could not locate dialogContinue button in Open Chart dialog'; buttonContinue.onClick = function(_event) { - ChartEditorImportExportHandler.loadSong(state, songMetadata, songChartData); + state.loadSong(songMetadata, songChartData); dialog.hideDialog(DialogButton.APPLY); } @@ -996,7 +939,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Could not parse metadata file version (${path.file}.${path.ext})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end return; @@ -1014,7 +957,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Could not load metadata file (${path.file}.${path.ext})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end return; @@ -1029,7 +972,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded metadata file (${path.file}.${path.ext})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end @@ -1061,7 +1004,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Could not parse metadata file version (${selectedFile.name})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end return; @@ -1081,7 +1024,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded metadata file (${selectedFile.name})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end @@ -1102,7 +1045,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Failed to load metadata file (${selectedFile.name})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end } @@ -1126,7 +1069,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Could not parse chart data file version (${path.file}.${path.ext})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end return; @@ -1149,7 +1092,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded chart data file (${path.file}.${path.ext})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end @@ -1168,7 +1111,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Failed to load chart data file (${path.file}.${path.ext})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end } @@ -1193,7 +1136,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Could not parse chart data file version (${selectedFile.name})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end return; @@ -1216,7 +1159,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded chart data file (${selectedFile.name})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end @@ -1319,7 +1262,7 @@ class ChartEditorDialogHandler title: 'Failure', body: 'Failed to parse FNF chart file (${selectedFile.name})', type: NotificationType.Error, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end return; @@ -1328,7 +1271,7 @@ class ChartEditorDialogHandler var songMetadata:SongMetadata = FNFLegacyImporter.migrateMetadata(fnfLegacyData); var songChartData:SongChartData = FNFLegacyImporter.migrateChartData(fnfLegacyData); - ChartEditorImportExportHandler.loadSong(state, [Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); + state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); dialog.hideDialog(DialogButton.APPLY); #if !mac @@ -1337,7 +1280,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded chart file (${selectedFile.name})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end } @@ -1351,7 +1294,7 @@ class ChartEditorDialogHandler var songMetadata:SongMetadata = FNFLegacyImporter.migrateMetadata(selectedFileData); var songChartData:SongChartData = FNFLegacyImporter.migrateChartData(selectedFileData); - ChartEditorImportExportHandler.loadSong(state, [Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); + state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); dialog.hideDialog(DialogButton.APPLY); #if !mac @@ -1360,7 +1303,7 @@ class ChartEditorDialogHandler title: 'Success', body: 'Loaded chart file (${path.file}.${path.ext})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end }; @@ -1376,35 +1319,11 @@ class ChartEditorDialogHandler * @param state The current chart editor state. * @return The dialog that was opened. */ - public static inline function openUserGuideDialog(state:ChartEditorState):Null<Dialog> + public static function openUserGuideDialog(state:ChartEditorState):Null<Dialog> { return openDialog(state, CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT, true, true); } - /** - * Builds and opens a dialog from a given layout path. - * @param modal Makes the background uninteractable while the dialog is open. - * @param closable Hides the close button on the dialog, preventing it from being closed unless the user interacts with the dialog. - */ - static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null<Dialog> - { - var dialog:Null<Dialog> = cast state.buildComponent(key); - if (dialog == null) return null; - - dialog.destroyOnClose = true; - dialog.closable = closable; - dialog.showDialog(modal); - - state.isHaxeUIDialogOpen = true; - dialog.onDialogClosed = function(event:UIEvent) { - state.isHaxeUIDialogOpen = false; - }; - - dialog.zIndex = 1000; - - return dialog; - } - /** * Builds and opens a dialog where the user can add a new variation for a song. * @param state The current chart editor state. @@ -1561,4 +1480,91 @@ class ChartEditorDialogHandler return dialog; } + + /** + * Builds and opens a dialog from a given layout path. + * @param modal Makes the background uninteractable while the dialog is open. + * @param closable Hides the close button on the dialog, preventing it from being closed unless the user interacts with the dialog. + */ + static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null<Dialog> + { + var dialog:Null<Dialog> = cast state.buildComponent(key); + if (dialog == null) return null; + + dialog.destroyOnClose = true; + dialog.closable = closable; + dialog.showDialog(modal); + + state.isHaxeUIDialogOpen = true; + dialog.onDialogClosed = function(event:UIEvent) { + state.isHaxeUIDialogOpen = false; + }; + + dialog.zIndex = 1000; + + return dialog; + } + + // ========== + // DROP HANDLERS + // ========== + static var dropHandlers:Array< + { + component:Component, + handler:(String->Void) + }> = []; + + /** + * Add a callback for when a file is dropped on a component. + * + * On OS X you can’t drop on the application window, but rather only the app icon + * (either in the dock while running or the icon on the hard drive) so this must be disabled + * and UI updated appropriately. + * @param component + * @param handler + */ + static function addDropHandler(component:Component, handler:String->Void):Void + { + #if desktop + if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile); + + dropHandlers.push( + { + component: component, + handler: handler + }); + #else + trace('addDropHandler not implemented for this platform'); + #end + } + + static function removeDropHandler(handler:String->Void):Void + { + #if desktop + FlxG.stage.window.onDropFile.remove(handler); + #end + } + + static function clearDropHandlers():Void + { + #if desktop + dropHandlers = []; + FlxG.stage.window.onDropFile.remove(onDropFile); + #end + } + + static function onDropFile(path:String):Void + { + // a VERY short timer to wait for the mouse position to update + new FlxTimer().start(0.01, function(_) { + for (handler in dropHandlers) + { + if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY)) + { + handler.handler(path); + return; + } + } + }); + } } diff --git a/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx similarity index 87% rename from source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx rename to source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index 4d8ff18cb..2e3306769 100644 --- a/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -1,4 +1,4 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.handlers; import haxe.ui.notifications.NotificationType; import funkin.util.DateUtil; @@ -16,7 +16,7 @@ import funkin.data.song.SongRegistry; * Contains functions for importing, loading, saving, and exporting charts. */ @:nullSafety -@:allow(funkin.ui.debug.charting.ChartEditorState) +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorImportExportHandler { /** @@ -50,18 +50,18 @@ class ChartEditorImportExportHandler state.sortChartData(); - state.clearVocals(); + state.stopExistingVocals(); var variations:Array<String> = state.availableVariations; for (variation in variations) { if (variation == Constants.DEFAULT_VARIATION) { - ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId)); + state.loadInstFromAsset(Paths.inst(songId)); } else { - ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId, '-$variation'), variation); + state.loadInstFromAsset(Paths.inst(songId, '-$variation'), variation); } } @@ -75,12 +75,12 @@ class ChartEditorImportExportHandler if (voiceList.length == 2) { - ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId); - ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], diff.characters.opponent, instId); + state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId); + state.loadVocalsFromAsset(voiceList[1], diff.characters.opponent, instId); } else if (voiceList.length == 1) { - ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId); + state.loadVocalsFromAsset(voiceList[0], diff.characters.player, instId); } else { @@ -98,7 +98,7 @@ class ChartEditorImportExportHandler title: 'Success', body: 'Loaded song (${rawSongMetadata[0].songName})', type: NotificationType.Success, - expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end } @@ -169,8 +169,8 @@ class ChartEditorImportExportHandler } } - if (state.audioInstTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state)); - if (state.audioVocalTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state)); + if (state.audioInstTrackData != null) zipEntries.concat(state.makeZIPEntriesFromInstrumentals()); + if (state.audioVocalTrackData != null) zipEntries.concat(state.makeZIPEntriesFromVocals()); trace('Exporting ${zipEntries.length} files to ZIP...'); diff --git a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx similarity index 98% rename from source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx rename to source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index 8a9bb8b03..4197ebdd3 100644 --- a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -1,26 +1,19 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.handlers; -import flixel.FlxSprite; import flixel.addons.display.FlxGridOverlay; import flixel.addons.display.FlxSliceSprite; +import flixel.FlxSprite; import flixel.math.FlxRect; import flixel.util.FlxColor; +import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; import openfl.display.BitmapData; import openfl.geom.Rectangle; -/** - * Available themes for the chart editor state. - */ -enum ChartEditorTheme -{ - Light; - Dark; -} - /** * Static functions which handle building themed UI elements for a provided ChartEditorState. */ @:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorThemeHandler { // TODO: There's probably a better system of organization for these colors. diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx similarity index 92% rename from source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx rename to source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index 7cee1edde..f0c634666 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -1,53 +1,43 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.handlers; -import funkin.ui.haxeui.components.FunkinDropDown; -import funkin.play.stage.StageData.StageDataParser; -import funkin.play.stage.StageData; -import funkin.play.character.CharacterData; -import funkin.play.character.CharacterData.CharacterDataParser; -import haxe.ui.components.HorizontalSlider; -import haxe.ui.containers.TreeView; -import haxe.ui.containers.TreeViewNode; -import funkin.play.character.BaseCharacter.CharacterType; -import funkin.play.event.SongEvent; import funkin.data.event.SongEventData; import funkin.data.song.SongData.SongTimeChange; +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.CharacterData; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.play.event.SongEvent; import funkin.play.song.SongSerializer; +import funkin.play.stage.StageData; +import funkin.play.stage.StageData.StageDataParser; +import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.ui.haxeui.components.CharacterPlayer; +import funkin.ui.haxeui.components.FunkinDropDown; import funkin.util.FileUtil; import haxe.ui.components.Button; import haxe.ui.components.CheckBox; import haxe.ui.components.DropDown; +import haxe.ui.components.HorizontalSlider; import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; import haxe.ui.containers.Box; -import haxe.ui.containers.Grid; -import haxe.ui.containers.Group; -import haxe.ui.containers.VBox; -import haxe.ui.containers.Frame; import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.dialogs.Dialog.DialogButton; import haxe.ui.containers.dialogs.Dialog.DialogEvent; +import haxe.ui.containers.Frame; +import haxe.ui.containers.Grid; +import haxe.ui.containers.TreeView; +import haxe.ui.containers.TreeViewNode; import haxe.ui.core.Component; import haxe.ui.data.ArrayDataSource; import haxe.ui.events.UIEvent; -/** - * Available tools for the chart editor state. - */ -enum ChartEditorToolMode -{ - Select; - Place; -} - /** * Static functions which handle building themed UI elements for a provided ChartEditorState. */ @:nullSafety -@:allow(funkin.ui.debug.charting.ChartEditorState) +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorToolboxHandler { public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void @@ -72,12 +62,10 @@ class ChartEditorToolboxHandler { toolbox.showDialog(false); - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/openWindow')); + state.playSound(Paths.sound('chartingSounds/openWindow')); switch (id) { - case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: - onShowToolboxTools(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: onShowToolboxNoteData(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: @@ -111,12 +99,10 @@ class ChartEditorToolboxHandler { toolbox.hideDialog(DialogButton.CANCEL); - ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/exitWindow')); + state.playSound(Paths.sound('chartingSounds/exitWindow')); switch (id) { - case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: - onHideToolboxTools(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: onHideToolboxNoteData(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: @@ -175,8 +161,6 @@ class ChartEditorToolboxHandler var toolbox:Null<CollapsibleDialog> = null; switch (id) { - case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: - toolbox = buildToolboxToolsLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: toolbox = buildToolboxNoteDataLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: @@ -223,44 +207,6 @@ class ChartEditorToolboxHandler return toolbox; } - static function buildToolboxToolsLayout(state:ChartEditorState):Null<CollapsibleDialog> - { - var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT); - - if (toolbox == null) return null; - - // Starting position. - toolbox.x = 50; - toolbox.y = 50; - - toolbox.onDialogClosed = function(event:DialogEvent) { - state.setUICheckboxSelected('menubarItemToggleToolboxTools', false); - } - - var toolsGroup:Null<Group> = toolbox.findComponent('toolboxToolsGroup', Group); - if (toolsGroup == null) throw 'ChartEditorToolboxHandler.buildToolboxToolsLayout() - Could not find toolboxToolsGroup component.'; - - if (toolsGroup == null) return null; - - toolsGroup.onChange = function(event:UIEvent) { - switch (event.target.id) - { - case 'toolboxToolsGroupSelect': - state.currentToolMode = ChartEditorToolMode.Select; - case 'toolboxToolsGroupPlace': - state.currentToolMode = ChartEditorToolMode.Place; - default: - trace('ChartEditorToolboxHandler.buildToolboxToolsLayout() - Unknown toolbox tool selected: $event.target.id'); - } - } - - return toolbox; - } - - static function onShowToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - - static function onHideToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT); @@ -483,11 +429,11 @@ class ChartEditorToolboxHandler throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.'; difficultyToolboxAddVariation.onClick = function(_:UIEvent) { - ChartEditorDialogHandler.openAddVariationDialog(state, true); + state.openAddVariationDialog(true); }; difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) { - ChartEditorDialogHandler.openAddDifficultyDialog(state, true); + state.openAddDifficultyDialog(true); }; difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) { diff --git a/source/funkin/ui/debug/charting/import.hx b/source/funkin/ui/debug/charting/import.hx new file mode 100644 index 000000000..56660c37a --- /dev/null +++ b/source/funkin/ui/debug/charting/import.hx @@ -0,0 +1,10 @@ +package funkin.ui.debug.charting; + +#if !macro +// Apply handlers so they can be called as though they were functions in ChartEditorState +using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler; +using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler; +using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler; +using funkin.ui.debug.charting.handlers.ChartEditorThemeHandler; +using funkin.ui.debug.charting.handlers.ChartEditorToolboxHandler; +#end diff --git a/source/funkin/ui/debug/charting/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx similarity index 89% rename from source/funkin/ui/debug/charting/ChartEditorDropdowns.hx rename to source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx index ec41de9c0..dfa0408d3 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDropdowns.hx +++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx @@ -1,4 +1,4 @@ -package funkin.ui.debug.charting; +package funkin.ui.debug.charting.util; import funkin.data.notestyle.NoteStyleRegistry; import funkin.play.notes.notestyle.NoteStyle; @@ -10,13 +10,16 @@ import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData.CharacterDataParser; /** - * This class contains functions for populating dropdowns based on game data. + * Functions for populating dropdowns based on game data. * These get used by both dialogs and toolboxes so they're in their own class to prevent "reaching over." */ @:nullSafety -@:access(ChartEditorState) +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorDropdowns { + /** + * Populate a dropdown with a list of characters. + */ public static function populateDropdownWithCharacters(dropDown:DropDown, charType:CharacterType, startingCharId:String):DropDownEntry { dropDown.dataSource.clear(); @@ -50,6 +53,9 @@ class ChartEditorDropdowns return returnValue; } + /** + * Populate a dropdown with a list of stages. + */ public static function populateDropdownWithStages(dropDown:DropDown, startingStageId:String):DropDownEntry { dropDown.dataSource.clear(); @@ -74,6 +80,9 @@ class ChartEditorDropdowns return returnValue; } + /** + * Populate a dropdown with a list of note styles. + */ public static function populateDropdownWithNoteStyles(dropDown:DropDown, startingStyleId:String):DropDownEntry { dropDown.dataSource.clear(); @@ -98,6 +107,9 @@ class ChartEditorDropdowns return returnValue; } + /** + * Populate a dropdown with a list of song variations. + */ public static function populateDropdownWithVariations(dropDown:DropDown, state:ChartEditorState, includeNone:Bool = true):DropDownEntry { dropDown.dataSource.clear(); @@ -122,6 +134,9 @@ class ChartEditorDropdowns } } +/** + * An entry in a dropdown. + */ typedef DropDownEntry = { id:String, diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index edd95f946..ad3b59f6f 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -251,6 +251,16 @@ class Constants */ public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC; + /** + * Duration, in milliseconds, until toast notifications are automatically hidden. + */ + public static final NOTIFICATION_DISMISS_TIME:Int = 5 * MS_PER_SEC; + + /** + * Duration to wait before autosaving the chart. + */ + public static final AUTOSAVE_TIMER_DELAY_SEC:Float = 5.0 * SECS_PER_MIN; + /** * Number of steps in a beat. * One step is one 16th note and one beat is one quarter note. @@ -392,7 +402,8 @@ class Constants public static final GHOST_TAPPING:Bool = false; /** - * The separator between an asset library and the asset path. + * The separator between an asset library and the asset path. + */ public static final LIBRARY_SEPARATOR:String = ':'; diff --git a/source/funkin/util/assets/SoundUtil.hx b/source/funkin/util/assets/SoundUtil.hx new file mode 100644 index 000000000..872a61609 --- /dev/null +++ b/source/funkin/util/assets/SoundUtil.hx @@ -0,0 +1,23 @@ +package funkin.util.assets; + +import haxe.io.Bytes; +import flixel.system.FlxSound; + +class SoundUtil +{ + /** + * Convert byte data into a playable sound. + * + * @param input The byte data. + * @return The playable sound, or `null` if loading failed. + */ + public static function buildFlxSoundFromBytes(input:Null<Bytes>):Null<FlxSound> + { + if (input == null) return null; + + var openflSound:openfl.media.Sound = new openfl.media.Sound(); + openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length); + var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false); + return output; + } +} diff --git a/source/funkin/util/tools/FloatTools.hx b/source/funkin/util/tools/FloatTools.hx new file mode 100644 index 000000000..e07ae5cb9 --- /dev/null +++ b/source/funkin/util/tools/FloatTools.hx @@ -0,0 +1,15 @@ +package funkin.util.tools; + +/** + * Utilities for performing common math operations. + */ +class FloatTools +{ + /** + * Constrain a float between a minimum and maximum value. + */ + public static function clamp(value:Float, min:Float, max:Float):Float + { + return Math.max(min, Math.min(max, value)); + } +} diff --git a/source/funkin/util/tools/IntTools.hx b/source/funkin/util/tools/IntTools.hx new file mode 100644 index 000000000..1d660ad1b --- /dev/null +++ b/source/funkin/util/tools/IntTools.hx @@ -0,0 +1,16 @@ +package funkin.util.tools; + +/** + * Utilities for performing common math operations. + */ +class IntTools +{ + /** + * Constrain an integer between a minimum and maximum value. + */ + public static function clamp(value:Int, min:Int, max:Int):Int + { + // Don't use Math.min because it returns a Float. + return value < min ? min : value > max ? max : value; + } +}