mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-14 11:15:24 -05:00
Updated chartformat v7
This commit is contained in:
parent
36a49affee
commit
175908f827
6 changed files with 748 additions and 66 deletions
18
hmm.json
18
hmm.json
|
@ -11,8 +11,8 @@
|
|||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "0f3ae2d",
|
||||
"url": "https://github.com/haxeflixel/flixel"
|
||||
"ref": "dev",
|
||||
"url": "https://github.com/MasterEric/flixel"
|
||||
},
|
||||
{
|
||||
"name": "flixel-addons",
|
||||
|
@ -40,11 +40,16 @@
|
|||
"ref": "18b2060",
|
||||
"url": "https://github.com/Dot-Stuff/flxanimate"
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"type": "haxelib",
|
||||
"version": null
|
||||
},
|
||||
{
|
||||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "master",
|
||||
"ref": "53f5cc24",
|
||||
"url": "https://github.com/haxeui/haxeui-core/"
|
||||
},
|
||||
{
|
||||
|
@ -74,6 +79,11 @@
|
|||
"type": "haxelib",
|
||||
"version": "1.2.4"
|
||||
},
|
||||
{
|
||||
"name": "hxp",
|
||||
"type": "haxelib",
|
||||
"version": null
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "git",
|
||||
|
@ -101,4 +111,4 @@
|
|||
"version": "0.2.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -190,8 +190,8 @@
|
|||
* - e: A string specifying the event type. See below for more info.
|
||||
* - v: The (optional) value for this event. Its type depends on the associated event (it could be a number, bool, string, array, object...)
|
||||
*
|
||||
* This list is assumed to be in timestamp order, and unexpected behavior may occur if it is not.
|
||||
*
|
||||
* This list is assumed to be in timestamp order, and unexpected behavior may occur if it is not.
|
||||
*
|
||||
* This list will be iterated through before the song is loaded, allowing for modules to mutate them before the song starts.
|
||||
* It also serves to allow for pre-loading certain assets that may be added to the game mid-song through events.
|
||||
*
|
||||
|
@ -246,6 +246,15 @@
|
|||
* - k: Kind of this note. If unspecified, defaults to `"normal"`.
|
||||
* This can allow the note to either include custom behavior defined in a module script,
|
||||
* or have a custom appearance defined by the noteSkin (or both).
|
||||
* - el: Editor layer (index) which this note is placed on.
|
||||
* Optional, defaults to 0.
|
||||
* The user can use the UI to toggle visibility of notes placed on different layers.
|
||||
* - ep: Editor pattern child note.
|
||||
* Optional.
|
||||
* If specified, this note is in the chart because it is part of a placed pattern.
|
||||
* The value is the ID of the pattern the note was copied from.
|
||||
* The final chart should be able to be fully recreated by deleting all notes with non-empty `ep`,
|
||||
* then applying the pattern placement data from the `editor` object.
|
||||
*/
|
||||
"notes": {
|
||||
"easy": [
|
||||
|
@ -1198,6 +1207,94 @@
|
|||
]
|
||||
},
|
||||
|
||||
/**
|
||||
* Data used only by the chart editor.
|
||||
* This object is optional, and the chart file ought to be fully playable when it is excluded.
|
||||
*/
|
||||
"editor": {
|
||||
/**
|
||||
* An map of patterns which exist for this chart.
|
||||
* You can browse through these using the interface on the right side,
|
||||
* place them in the chart in multiple places, create them from an existing selection,
|
||||
* and double click to enter the pattern for editing.
|
||||
*
|
||||
* Editing a pattern will affect the grouped notes in all locations that the pattern was placed.
|
||||
* If a user destroys a pattern from the UI, they will be prompted to either
|
||||
* ungroup the notes (leaving them in the chart) or remove the grouped notes from the chart.
|
||||
*
|
||||
* The key for the map is a randomized unique ID for the pattern, used to quickly reference it elsewhere in the chart.
|
||||
*/
|
||||
"patternDefinitions": {
|
||||
"a7be4f8": {
|
||||
/**
|
||||
* A label to identify the pattern.
|
||||
* Should be customizable by the user, and does not need to be unique (the key for the map keeps patterns separate)
|
||||
*/
|
||||
"label": "Secondary Motif",
|
||||
/**
|
||||
* A color to highlight the grouped notes with in the UI.
|
||||
* Should be customizable by the user (HaxeUI has a color picker, yay!).
|
||||
*/
|
||||
"color": "#FF3333",
|
||||
/**
|
||||
* The notes placed in the pattern.
|
||||
* These are of the same format as the main `notes` object, with the exception that
|
||||
* editor attributes (such as layer and pattern child) are not allowed.
|
||||
*
|
||||
* `t` acts as an offset with 0 representing the timestamp the pattern was placed.
|
||||
*/
|
||||
"notes": [
|
||||
{"t":300, "d":4},
|
||||
{"t":600, "d":2},
|
||||
{"t":900, "d":1},
|
||||
{"t":1300, "d":3},
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Defines where in the chart that patterns are currently placed.
|
||||
* Add one key for each difficulty in the chart.
|
||||
*
|
||||
* When placing a pattern in a chart, an element should be added to the appropriate array below,
|
||||
* then the pattern should be fetched, and the pattern notes (with `t = pattern time + offset)
|
||||
* copied to the chart, with `ep` set so the chart editor knows the notes are part of a pattern.
|
||||
*/
|
||||
"patternPlacements": {
|
||||
"easy": [],
|
||||
"normal": [
|
||||
{
|
||||
"t": 3000,
|
||||
"p": "a7be4f8",
|
||||
"f": false,
|
||||
"el": 2
|
||||
}
|
||||
],
|
||||
"hard": [
|
||||
{
|
||||
"t": 3000,
|
||||
"p": "a7be4f8",
|
||||
"f": false,
|
||||
"el": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/**
|
||||
* Metadata about the layers in the chart.
|
||||
* The first layer (0) is always named "Default" and it cannot be rearranged or deleted.
|
||||
* If there are no other layers in the chart, this array will be empty.
|
||||
*/
|
||||
"layers": [
|
||||
{
|
||||
/**
|
||||
* The name this note layer has in the interface.
|
||||
*/
|
||||
"label": "Test"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Not used by anything, but just a note to keep this value free so you can keep track of tool versions to help with troubleshooting.
|
||||
"generatedBy": "FNF SongConverter v69"
|
||||
}
|
||||
|
|
|
@ -435,6 +435,42 @@ abstract SongNoteData(RawSongNoteData)
|
|||
value = null;
|
||||
return this.k = value;
|
||||
}
|
||||
|
||||
@:op(A == B)
|
||||
public function op_equals(other:SongNoteData):Bool
|
||||
{
|
||||
return this.t == other.t && this.d == other.d && this.l == other.l && this.k == other.k;
|
||||
}
|
||||
|
||||
@:op(A != B)
|
||||
public function op_notEquals(other:SongNoteData):Bool
|
||||
{
|
||||
return !this.op_equals(other);
|
||||
}
|
||||
|
||||
@:op(A > B)
|
||||
public function op_greaterThan(other:SongNoteData):Bool
|
||||
{
|
||||
return this.t > other.t;
|
||||
}
|
||||
|
||||
@:op(A < B)
|
||||
public function op_lessThan(other:SongNoteData):Bool
|
||||
{
|
||||
return this.t < other.t;
|
||||
}
|
||||
|
||||
@:op(A >= B)
|
||||
public function op_greaterThanOrEquals(other:SongNoteData):Bool
|
||||
{
|
||||
return this.t >= other.t;
|
||||
}
|
||||
|
||||
@:op(A <= B)
|
||||
public function op_lessThanOrEquals(other:SongNoteData):Bool
|
||||
{
|
||||
return this.t <= other.t;
|
||||
}
|
||||
}
|
||||
|
||||
typedef RawSongEventData =
|
||||
|
@ -535,6 +571,42 @@ abstract SongEventData(RawSongEventData)
|
|||
{
|
||||
return cast this.v;
|
||||
}
|
||||
|
||||
@:op(A == B)
|
||||
public function op_equals(other:SongEventData):Bool
|
||||
{
|
||||
return this.t == other.t && this.e == other.e && this.v == other.v;
|
||||
}
|
||||
|
||||
@:op(A != B)
|
||||
public function op_notEquals(other:SongEventData):Bool
|
||||
{
|
||||
return !this.op_equals(other);
|
||||
}
|
||||
|
||||
@:op(A > B)
|
||||
public function op_greaterThan(other:SongEventData):Bool
|
||||
{
|
||||
return this.t > other.t;
|
||||
}
|
||||
|
||||
@:op(A < B)
|
||||
public function op_lessThan(other:SongEventData):Bool
|
||||
{
|
||||
return this.t < other.t;
|
||||
}
|
||||
|
||||
@:op(A >= B)
|
||||
public function op_greaterThanOrEquals(other:SongEventData):Bool
|
||||
{
|
||||
return this.t >= other.t;
|
||||
}
|
||||
|
||||
@:op(A <= B)
|
||||
public function op_lessThanOrEquals(other:SongEventData):Bool
|
||||
{
|
||||
return this.t <= other.t;
|
||||
}
|
||||
}
|
||||
|
||||
abstract SongPlayableChar(RawSongPlayableChar)
|
||||
|
|
65
source/funkin/play/song/SongDataUtils.hx
Normal file
65
source/funkin/play/song/SongDataUtils.hx
Normal file
|
@ -0,0 +1,65 @@
|
|||
package funkin.play.song;
|
||||
|
||||
using Lambda;
|
||||
|
||||
class SongDataUtils {
|
||||
/**
|
||||
* Given an array of SongNoteData objects, return a new array of SongNoteData objects
|
||||
* whose timestamps are shifted by the given amount.
|
||||
*
|
||||
* @param notes The notes to modify.
|
||||
* @param offset The time difference to apply in milliseconds.
|
||||
*/
|
||||
public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Int):Array<SongNoteData> {
|
||||
return notes.map(function(note:SongNoteData):SongNoteData {
|
||||
return new SongNoteData(note.time + offset, note.data, note.length, note.kind);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a certain subset of notes from an array of SongNoteData objects.
|
||||
*
|
||||
* @param notes The array of notes to be subtracted from.
|
||||
* @param subtrahend The notes to remove from the `notes` array. Yes, subtrahend is a real word.
|
||||
*/
|
||||
public static function subtractNotes(notes:Array<SongNoteData>, subtrahend:Array<SongNoteData>) {
|
||||
if (notes.length == 0 || subtrahend.length == 0)
|
||||
return notes;
|
||||
|
||||
if (subtrahend.length == 1)
|
||||
return notes.remove(subtrahend[0]);
|
||||
|
||||
return notes.filter(function(note:SongNoteData):Bool {
|
||||
// SongNoteData's == operation has been overridden so that this will work.
|
||||
return !subtrahend.has(note);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a certain subset of events from an array of SongEventData objects.
|
||||
*
|
||||
* @param events The array of events to be subtracted from.
|
||||
* @param subtrahend The events to remove from the `events` array. Yes, subtrahend is a real word.
|
||||
*/
|
||||
public static function subtractEvents(events:Array<SongEventData>, subtrahend:Array<SongEventData>) {
|
||||
if (events.length == 0 || subtrahend.length == 0)
|
||||
return events;
|
||||
|
||||
if (subtrahend.length == 1)
|
||||
return events.remove(subtrahend[0]);
|
||||
|
||||
return events.filter(function(event:SongEventData):Bool {
|
||||
// SongEventData's == operation has been overridden so that this will work.
|
||||
return !subtrahend.has(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an array of notes to be used as the clipboard data.
|
||||
*
|
||||
* Offset the provided array of notes such that the first note is at 0 milliseconds.
|
||||
*/
|
||||
public static function buildClipboard(notes:Array<SongNoteData>):Array<SongNoteData> {
|
||||
return offsetSongNoteData(notes, -notes[0].time);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.play.song.SongDataUtils;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
import funkin.play.song.SongData.SongEventData;
|
||||
|
||||
using Lambda;
|
||||
|
||||
/**
|
||||
* Actions in the chart editor are backed by the Command pattern
|
||||
|
@ -31,36 +35,50 @@ interface ChartEditorCommand
|
|||
public function toString():String;
|
||||
}
|
||||
|
||||
class AddNoteCommand implements ChartEditorCommand
|
||||
class AddNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
private var note:SongNoteData;
|
||||
private var notes:SongNoteData;
|
||||
|
||||
public function new(note:SongNoteData)
|
||||
public function new(notes:SongNoteData)
|
||||
{
|
||||
this.note = note;
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartNoteData.push(note);
|
||||
for (note in notes) {
|
||||
state.currentSongChartNoteData.push(note);
|
||||
}
|
||||
|
||||
state.currentSelection = notes;
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartNoteData.remove(note);
|
||||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
|
||||
state.currentSelection = [];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var dir:String = note.getDirectionName();
|
||||
|
||||
return 'Add $dir Note';
|
||||
if (notes.length == 1) {
|
||||
var dir:String = notes[0].getDirectionName();
|
||||
return 'Add $dir Note';
|
||||
}
|
||||
|
||||
return 'Add ${notes.length} Notes';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,24 +94,35 @@ class RemoveNoteCommand implements ChartEditorCommand
|
|||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartNoteData.remove(note);
|
||||
state.currentSelection = [];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartNoteData.push(note);
|
||||
state.currentSelection = [note];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var dir:String = note.getDirectionName();
|
||||
|
||||
return 'Remove $dir Note';
|
||||
if (notes.length == 1) {
|
||||
var dir:String = notes[0].getDirectionName();
|
||||
return 'Remove $dir Note';
|
||||
}
|
||||
|
||||
return 'Remove ${notes.length} Notes';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,3 +164,296 @@ class SwitchDifficultyCommand implements ChartEditorCommand
|
|||
return 'Switch Difficulty';
|
||||
}
|
||||
}
|
||||
|
||||
class SelectNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
private var notes:Array<SongNoteData>;
|
||||
|
||||
public function new(notes:Array<SongNoteData>)
|
||||
{
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
for (note in this.notes) {
|
||||
state.currentSelection.push(note);
|
||||
}
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSelection = SongDataUtils.subtractNotes(state.currentSelection, this.notes);
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
if (notes.length == 1) {
|
||||
var dir:String = notes[0].getDirectionName();
|
||||
return 'Select $dir Note';
|
||||
}
|
||||
|
||||
return 'Select ${notes.length} Notes';
|
||||
}
|
||||
}
|
||||
|
||||
class DeselectNoteCommand implements ChartEditorCommand
|
||||
{
|
||||
private var notes:Array<SongNoteData>;
|
||||
|
||||
public function new(notes:Array<SongNoteData>)
|
||||
{
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSelection = SongDataUtils.subtractNotes(state.currentSelection, this.notes);
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
for (note in this.notes) {
|
||||
state.currentSelection.push(note);
|
||||
}
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
if (notes.length == 1) {
|
||||
var dir:String = notes[0].getDirectionName();
|
||||
return 'Deselect $dir Note';
|
||||
}
|
||||
|
||||
return 'Deselect ${notes.length} Notes';
|
||||
}
|
||||
}
|
||||
|
||||
class SelectAllNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
private var previousSelection:Array<SongNoteData>;
|
||||
|
||||
public function new(?previousSelection:Array<SongNoteData>)
|
||||
{
|
||||
this.previousSelection = previousSelection == null ? [] : previousSelection;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSelection = state.currentSongChartNoteData
|
||||
;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSelection = previousSelection;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Select All Notes';
|
||||
}
|
||||
}
|
||||
|
||||
class DeselectAllNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
private var previousSelection:Array<SongNoteData>;
|
||||
|
||||
public function new(?previousSelection:Array<SongNoteData>)
|
||||
{
|
||||
this.previousSelection = previousSelection == null ? [] : previousSelection;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSelection = [];
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSelection = previousSelection;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Deselect All Notes';
|
||||
}
|
||||
}
|
||||
|
||||
class CopyNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
private var notes:Array<SongNoteData>;
|
||||
private var previousSelection:Array<SongNoteData>;
|
||||
|
||||
public function new(notes:Array<SongNoteData>, ?previousSelection:Array<SongNoteData>)
|
||||
{
|
||||
this.notes = notes;
|
||||
this.previousSelection = previousSelection == null ? [] : previousSelection;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentClipboard = SongDataUtils.buildClipboard(notes);
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentClipboard = previousSelection;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length;
|
||||
return 'Copy $len Notes to Clipboard';
|
||||
}
|
||||
}
|
||||
|
||||
class CutNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
private var notes:Array<SongNoteData>;
|
||||
private var previousSelection:Array<SongNoteData>;
|
||||
|
||||
public function new(notes:Array<SongNoteData>, ?previousSelection:Array<SongNoteData> = [])
|
||||
{
|
||||
this.notes = notes;
|
||||
this.previousSelection = previousSelection == null ? [] : previousSelection;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
// Copy the notes.
|
||||
state.currentClipboard = SongDataUtils.buildClipboard(notes);
|
||||
|
||||
// Delete the notes.
|
||||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
|
||||
state.currentSelection = [];
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentClipboard = previousSelection;
|
||||
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
|
||||
state.currentSelection = notes;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length;
|
||||
return 'Cut $len Notes to Clipboard';
|
||||
}
|
||||
}
|
||||
|
||||
class PasteNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
private var targetTimestamp:Int;
|
||||
|
||||
public function new(targetTimestamp:Int)
|
||||
{
|
||||
this.targetTimestamp = targetTimestamp;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
var notesToAdd = SongDataUtils.offsetSongNoteData(state.currentClipboard, targetTimestamp);
|
||||
|
||||
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notesToAdd);
|
||||
state.currentSelection = notesToAdd;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
// NOTE: We can assume that the previous action
|
||||
// defined the clipboard, so we don't need to redundantly it here... right?
|
||||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, state.currentClipboard);
|
||||
state.currentSelection = [];
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length;
|
||||
return 'Paste $len Notes from Clipboard';
|
||||
}
|
||||
}
|
||||
|
||||
class AddEventsCommand implements ChartEditorCommand
|
||||
{
|
||||
private var events:Array<SongEventData>;
|
||||
// private var previousSelection:Array<SongEventData>;
|
||||
|
||||
public function new(events:Array<SongEventData>, ?previousSelection:Array<SongEventData>)
|
||||
{
|
||||
this.events = events;
|
||||
// this.previousSelection = previousSelection == null ? [] : previousSelection;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartEventData = state.currentSongChartEventData.concat(events);
|
||||
// TODO: Allow selecting events.
|
||||
// state.currentSelection = events;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
|
||||
// TODO: Allow selecting events.
|
||||
// state.currentSelection = previousSelection;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = events.length;
|
||||
return 'Add $len Events';
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.containers.TreeViewNode;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.addons.display.FlxGridOverlay;
|
||||
import flixel.addons.display.FlxTiledSprite;
|
||||
|
@ -51,6 +53,7 @@ class ChartEditorState extends HaxeUIState
|
|||
public static final STRUMLINE_SIZE = 4;
|
||||
static final MENU_BAR_HEIGHT = 32;
|
||||
static final GRID_TOP_PAD:Int = 8;
|
||||
static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
|
||||
|
||||
// UI Element Colors
|
||||
static final BG_COLOR:FlxColor = 0xFF673AB7;
|
||||
|
@ -63,7 +66,9 @@ class ChartEditorState extends HaxeUIState
|
|||
static final PREVIEW_BG_COLOR:FlxColor = 0xFF303030;
|
||||
static final PLAYHEAD_COLOR:FlxColor = 0xC0808080;
|
||||
static final SPECTROGRAM_COLOR:FlxColor = 0xFFFF0000;
|
||||
|
||||
static final SELECTION_SQUARE_BORDER_COLOR:FlxColor = 0xFF339933;
|
||||
static final SELECTION_SQUARE_FILL_COLOR:FlxColor = 0x4033FF33;
|
||||
|
||||
/**
|
||||
* INSTANCE DATA
|
||||
*/
|
||||
|
@ -164,6 +169,19 @@ class ChartEditorState extends HaxeUIState
|
|||
*/
|
||||
var shouldPlayMetronome:Bool = true;
|
||||
|
||||
/**
|
||||
* Whether the current view is in downscroll mode.
|
||||
*/
|
||||
var isViewDownscroll(default, set):Bool = false;
|
||||
|
||||
function set_isViewDownscroll(value:Bool):Bool {
|
||||
// Make sure view is updated.
|
||||
noteDisplayDirty = true;
|
||||
notePreviewDirty = true;
|
||||
|
||||
return isViewDownscroll = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current variation ID.
|
||||
*/
|
||||
|
@ -208,6 +226,17 @@ class ChartEditorState extends HaxeUIState
|
|||
*/
|
||||
var commandHistoryDirty:Bool = true;
|
||||
|
||||
/**
|
||||
* The notes which are currently in the selection.
|
||||
*/
|
||||
var currentSelection:Array<SongNoteData> = [];
|
||||
|
||||
/**
|
||||
* The user's current clipboard. Contains a full list of the notes they have copied or cut.
|
||||
* TODO: Replace this with serialization in the real clipboard.
|
||||
*/
|
||||
var currentClipboard:Array<SongNoteData> = [];
|
||||
|
||||
/**
|
||||
* AUDIO AND SOUND DATA
|
||||
*/
|
||||
|
@ -418,6 +447,11 @@ class ChartEditorState extends HaxeUIState
|
|||
* The IMAGE used for the grid.
|
||||
*/
|
||||
var gridBitmap:BitmapData;
|
||||
|
||||
/**
|
||||
* The IMAGE used for the selection squares.
|
||||
*/
|
||||
var selectionSquareBitmap:BitmapData = null;
|
||||
|
||||
/**
|
||||
* The tiled sprite used to display the grid.
|
||||
|
@ -475,6 +509,8 @@ class ChartEditorState extends HaxeUIState
|
|||
*/
|
||||
var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite>;
|
||||
|
||||
var renderedNoteSelectionSquares:FlxTypedSpriteGroup<FlxSprite>;
|
||||
|
||||
public function new()
|
||||
{
|
||||
// Load the HaxeUI XML file.
|
||||
|
@ -541,6 +577,15 @@ class ChartEditorState extends HaxeUIState
|
|||
dark ? GRID_COLOR_1_DARK : GRID_COLOR_1, dark ? GRID_COLOR_2_DARK : GRID_COLOR_2);
|
||||
}
|
||||
|
||||
function makeSelectionSquareBitmap() {
|
||||
selectionSquareBitmap = new BitmapData(GRID_SIZE, GRID_SIZE, true);
|
||||
|
||||
selectionSquareBitmap.fillRect(new Rectangle(0, 0, GRID_SIZE, GRID_SIZE), SELECTION_SQUARE_BORDER_COLOR);
|
||||
selectionSquareBitmap.fillRect(new Rectangle(SELECTION_SQUARE_BORDER_WIDTH, SELECTION_SQUARE_BORDER_WIDTH,
|
||||
GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2), GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2)),
|
||||
SELECTION_SQUARE_FILL_COLOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and displays the chart editor grid, including the playhead and cursor.
|
||||
*/
|
||||
|
@ -548,6 +593,8 @@ class ChartEditorState extends HaxeUIState
|
|||
{
|
||||
makeGridBitmap(false);
|
||||
|
||||
makeSelectionSquareBitmap();
|
||||
|
||||
// Draw dividers between the strumlines.
|
||||
var dividerLineAX = GRID_SIZE * (STRUMLINE_SIZE) - 1;
|
||||
gridBitmap.fillRect(new Rectangle(dividerLineAX, 0, 2, gridBitmap.height), 0xFF000000);
|
||||
|
@ -657,23 +704,22 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
addUIChangeListener('menubarItemToggleSidebar', (event:UIEvent) ->
|
||||
{
|
||||
var sidebar:MenuCheckBox = findComponent('sidebar', MenuCheckBox);
|
||||
var sidebar:Component = findComponent('sidebar', Component);
|
||||
|
||||
if (event.value)
|
||||
{
|
||||
sidebar.show();
|
||||
}
|
||||
sidebar.visible = event.value;
|
||||
});
|
||||
setUISelected('menubarItemToggleSidebar', true);
|
||||
|
||||
addUIChangeListener('menubarItemDownscroll', (event:UIEvent) -> {
|
||||
isViewDownscroll = event.value;
|
||||
});
|
||||
setUISelected('menubarItemDownscroll', isViewDownscroll);
|
||||
|
||||
addUIChangeListener('menubarItemMetronomeEnabled', (event:UIEvent) ->
|
||||
{
|
||||
shouldPlayMetronome = event.value;
|
||||
});
|
||||
var metronomeEnabledCheckbox:MenuCheckBox = findComponent('menubarItemMetronomeEnabled', MenuCheckBox);
|
||||
if (metronomeEnabledCheckbox != null)
|
||||
{
|
||||
metronomeEnabledCheckbox.selected = shouldPlayMetronome;
|
||||
}
|
||||
setUISelected('menubarItemMetronomeEnabled', shouldPlayMetronome);
|
||||
|
||||
addUIChangeListener('menubarItemVolumeInstrumental', (event:UIEvent) ->
|
||||
{
|
||||
|
@ -880,12 +926,12 @@ class ChartEditorState extends HaxeUIState
|
|||
// Middle Mouse + Drag = Scroll but move the playhead the same amount.
|
||||
if (FlxG.mouse.pressedMiddle)
|
||||
{
|
||||
if (FlxG.mouse.diffY != 0)
|
||||
if (FlxG.mouse.deltaY != 0)
|
||||
{
|
||||
// Scroll down by the amount dragged.
|
||||
scrollAmount += -FlxG.mouse.diffY;
|
||||
scrollAmount += -FlxG.mouse.deltaY;
|
||||
// Move the playhead by the same amount in the other direction so it is stationary.
|
||||
playheadAmount += FlxG.mouse.diffY;
|
||||
playheadAmount += FlxG.mouse.deltaY;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -977,39 +1023,47 @@ class ChartEditorState extends HaxeUIState
|
|||
// Left click.
|
||||
if (FlxG.mouse.justPressed)
|
||||
{
|
||||
var eventColumn = (STRUMLINE_SIZE * 2 + 1) - 1;
|
||||
if (cursorColumn == eventColumn)
|
||||
{
|
||||
// Place an event.
|
||||
// Find the first note that is at the cursor position.
|
||||
var highlightedNote:ChartEditorNoteSprite = renderedNotes.find(function(note:ChartEditorNoteSprite):Bool {
|
||||
// return note.step == cursorStep && note.column == cursorColumn;
|
||||
return FlxG.mouse.overlaps(note);
|
||||
});
|
||||
|
||||
/*
|
||||
var newEventData:SongEvent = new SongEventData(cursorMs, cursorColumn, 0, selectedNoteKind);
|
||||
currentSongChartEventData.push(newEventData);
|
||||
sortChartData();
|
||||
*/
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a note and place it in the chart.
|
||||
var cursorMs = cursorStep * Conductor.stepCrochet;
|
||||
|
||||
var newNoteData:SongNoteData = new SongNoteData(cursorMs, cursorColumn, 0, selectedNoteKind);
|
||||
|
||||
performCommand(new AddNoteCommand(newNoteData));
|
||||
}
|
||||
}
|
||||
|
||||
// Right click.
|
||||
if (FlxG.mouse.justPressedRight)
|
||||
{
|
||||
for (noteSprite in renderedNotes.members)
|
||||
{
|
||||
if (noteSprite == null || !noteSprite.exists || !noteSprite.visible)
|
||||
continue;
|
||||
|
||||
if (noteSprite.overlapsPoint(FlxG.mouse.getPosition()))
|
||||
{
|
||||
performCommand(new RemoveNoteCommand(noteSprite.noteData));
|
||||
if (FlxG.keys.pressed.CONTROL) {
|
||||
if (highlightedNote != null) {
|
||||
if (isNoteSelected(highlightedNote.noteData)) {
|
||||
performCommand(new SelectNotesCommand([highlightedNote.noteData]));
|
||||
} else {
|
||||
performCommand(new DeselectNotesCommand([highlightedNote.noteData]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (highlightedNote != null) {
|
||||
// Remove the note.
|
||||
performCommand(new RemoveNotesCommand([highlightedNote.noteData]));
|
||||
} else {
|
||||
// Place a note.
|
||||
var eventColumn = (STRUMLINE_SIZE * 2 + 1) - 1;
|
||||
if (cursorColumn == eventColumn)
|
||||
{
|
||||
// Create an event and place it in the chart.
|
||||
var cursorMs = cursorStep * Conductor.stepCrochet;
|
||||
|
||||
// TODO: Allow configuring the event to place from the sidebar.
|
||||
var newEventData:SongEventData = new SongEventData(cursorMs, "test", {});
|
||||
|
||||
performCommand(new AddEventsCommand([newEventData]));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a note and place it in the chart.
|
||||
var cursorMs = cursorStep * Conductor.stepCrochet;
|
||||
|
||||
var newNoteData:SongNoteData = new SongNoteData(cursorMs, cursorColumn, 0, selectedNoteKind);
|
||||
|
||||
performCommand(new AddNotesCommand([newNoteData]));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1031,6 +1085,9 @@ class ChartEditorState extends HaxeUIState
|
|||
{
|
||||
noteDisplayDirty = false;
|
||||
|
||||
// Update for whether downscroll is enabled.
|
||||
renderedNotes.flipX = (isViewDownscroll);
|
||||
|
||||
// Calculate the view bounds.
|
||||
var viewAreaTop:Float = this.scrollPosition - GRID_TOP_PAD;
|
||||
var viewHeight:Float = (FlxG.height - MENU_BAR_HEIGHT);
|
||||
|
@ -1090,6 +1147,22 @@ class ChartEditorState extends HaxeUIState
|
|||
noteSprite.x += renderedNotes.x;
|
||||
noteSprite.y += renderedNotes.y;
|
||||
}
|
||||
|
||||
// Handle selection squares.
|
||||
for (member in renderedNoteSelectionSquares.members) {
|
||||
member.kill();
|
||||
}
|
||||
|
||||
for (noteSprite in renderedNotes.members) {
|
||||
if (isNoteSelected(noteSprite.noteData)) {
|
||||
var selectionSquare:FlxSprite = renderedNoteSelectionSquares.recycle(FlxSprite).loadGraphic(selectionSquareBitmap);
|
||||
|
||||
selectionSquare.x = noteSprite.x;
|
||||
selectionSquare.y = noteSprite.y;
|
||||
selectionSquare.width = noteSprite.width;
|
||||
selectionSquare.height = noteSprite.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1185,10 +1258,20 @@ class ChartEditorState extends HaxeUIState
|
|||
text: "D: Erect",
|
||||
icon: "haxeui-core/styles/default/haxeui_tiny.png"
|
||||
});
|
||||
|
||||
treeView.onChange = onChangeTreeDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeTreeDifficulty(event:UIEvent):Void
|
||||
{
|
||||
// Get the selected node.
|
||||
var target:TreeView = cast event.target;
|
||||
var targetNode:TreeViewNode = target.selectedNode;
|
||||
trace('Selected node: ${targetNode.id}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the player preview/gameplay test area on the left side.
|
||||
*/
|
||||
|
@ -1391,7 +1474,11 @@ class ChartEditorState extends HaxeUIState
|
|||
this.scrollPosition = value;
|
||||
|
||||
// Move the grid sprite to the correct position.
|
||||
gridTiledSprite.y = -scrollPosition + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
|
||||
if (isViewDownscroll) {
|
||||
gridTiledSprite.y = -scrollPosition + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
|
||||
} else {
|
||||
gridTiledSprite.y = -scrollPosition + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
|
||||
}
|
||||
// Move the rendered notes to the correct position.
|
||||
renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
|
||||
|
||||
|
@ -1424,7 +1511,7 @@ class ChartEditorState extends HaxeUIState
|
|||
// Set visibility while syncing the checkbox.
|
||||
if (sidebar != null)
|
||||
{
|
||||
sidebar.hidden = setUIValue('menubarItemToggleSidebar', !sidebar.hidden);
|
||||
sidebar.visible = setUISelected('menubarItemToggleSidebar', !sidebar.visible);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1555,6 +1642,30 @@ class ChartEditorState extends HaxeUIState
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a HaxeUI checkbox,
|
||||
* since that's on 'selected' instead of 'value'.
|
||||
*/
|
||||
function setUISelected<T>(key:String, value:Bool):Bool
|
||||
{
|
||||
var targetA:CheckBox = findComponent(key, CheckBox);
|
||||
|
||||
if (targetA != null)
|
||||
{
|
||||
return targetA.selected = value;
|
||||
}
|
||||
|
||||
var targetB:MenuCheckBox = findComponent(key, MenuCheckBox);
|
||||
if (targetB != null)
|
||||
{
|
||||
return targetB.selected = value;
|
||||
}
|
||||
|
||||
// Gracefully handle the case where the item can't be located.
|
||||
trace('WARN: Could not locate check box: $key');
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform (or redo) a command, then add it to the undo stack.
|
||||
* @param command The command to perform.
|
||||
|
@ -1628,6 +1739,11 @@ class ChartEditorState extends HaxeUIState
|
|||
playSound(Paths.sound('pianoStuff/piano-${high ? '001' : '008'}'));
|
||||
}
|
||||
|
||||
function isNoteSelected(note:SongNoteData):Bool
|
||||
{
|
||||
return currentSelection.indexOf(note) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound effect.
|
||||
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
|
||||
|
|
Loading…
Reference in a new issue