Updated chartformat v7

This commit is contained in:
Eric Myllyoja 2022-10-10 20:04:09 -04:00
parent 36a49affee
commit 175908f827
6 changed files with 748 additions and 66 deletions

View file

@ -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"
}
]
}
}

View file

@ -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"
}

View file

@ -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)

View 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);
}
}

View file

@ -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';
}
}

View file

@ -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.