mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-27 01:55:52 -05:00
6564 lines
213 KiB
Haxe
6564 lines
213 KiB
Haxe
package funkin.ui.debug.charting;
|
|
|
|
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.FlxGroup.FlxTypedGroup;
|
|
import flixel.group.FlxSpriteGroup;
|
|
import flixel.input.gamepad.FlxGamepadInputID;
|
|
import flixel.input.keyboard.FlxKey;
|
|
import flixel.input.mouse.FlxMouseEvent;
|
|
import flixel.math.FlxMath;
|
|
import flixel.math.FlxPoint;
|
|
import flixel.math.FlxRect;
|
|
import flixel.sound.FlxSound;
|
|
import flixel.system.debug.log.LogStyle;
|
|
import flixel.system.FlxAssets.FlxSoundAsset;
|
|
import flixel.text.FlxText;
|
|
import flixel.tweens.FlxEase;
|
|
import flixel.tweens.FlxTween;
|
|
import flixel.tweens.misc.VarTween;
|
|
import flixel.util.FlxColor;
|
|
import flixel.util.FlxSort;
|
|
import flixel.util.FlxTimer;
|
|
import funkin.audio.FunkinSound;
|
|
import funkin.audio.visualize.PolygonSpectogram;
|
|
import funkin.audio.VoicesGroup;
|
|
import funkin.audio.waveform.WaveformSprite;
|
|
import funkin.data.notestyle.NoteStyleRegistry;
|
|
import funkin.data.song.SongData.SongCharacterData;
|
|
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.SongData.SongOffsets;
|
|
import funkin.data.song.SongDataUtils;
|
|
import funkin.data.song.SongRegistry;
|
|
import funkin.data.stage.StageData;
|
|
import funkin.graphics.FunkinCamera;
|
|
import funkin.graphics.FunkinSprite;
|
|
import funkin.input.Cursor;
|
|
import funkin.input.TurboActionHandler;
|
|
import funkin.input.TurboButtonHandler;
|
|
import funkin.input.TurboKeyHandler;
|
|
import funkin.modding.events.ScriptEvent;
|
|
import funkin.play.character.BaseCharacter.CharacterType;
|
|
import funkin.play.character.CharacterData;
|
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
|
import funkin.play.components.HealthIcon;
|
|
import funkin.play.notes.NoteSprite;
|
|
import funkin.play.PlayState;
|
|
import funkin.play.PlayStatePlaylist;
|
|
import funkin.play.song.Song;
|
|
import funkin.save.Save;
|
|
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.CopyItemsCommand;
|
|
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.ChartEditorMeasureTicks;
|
|
import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
|
|
import funkin.ui.debug.charting.components.ChartEditorNotePreview;
|
|
import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
|
|
import funkin.ui.debug.charting.components.ChartEditorPlaybarHead;
|
|
import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite;
|
|
import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
|
|
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
|
|
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
|
|
import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox;
|
|
import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox;
|
|
import funkin.ui.haxeui.components.CharacterPlayer;
|
|
import funkin.ui.haxeui.HaxeUIState;
|
|
import funkin.ui.mainmenu.MainMenuState;
|
|
import funkin.ui.transition.LoadingState;
|
|
import funkin.util.Constants;
|
|
import funkin.util.FileUtil;
|
|
import funkin.util.logging.CrashHandler;
|
|
import funkin.util.SortUtil;
|
|
import funkin.util.WindowUtil;
|
|
import haxe.DynamicAccess;
|
|
import haxe.io.Bytes;
|
|
import haxe.io.Path;
|
|
import haxe.ui.backend.flixel.UIRuntimeState;
|
|
import haxe.ui.backend.flixel.UIState;
|
|
import haxe.ui.components.Button;
|
|
import haxe.ui.components.DropDown;
|
|
import haxe.ui.components.Image;
|
|
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.dialogs.CollapsibleDialog;
|
|
import haxe.ui.containers.Frame;
|
|
import haxe.ui.containers.Grid;
|
|
import haxe.ui.containers.HBox;
|
|
import haxe.ui.containers.menus.Menu;
|
|
import haxe.ui.containers.menus.MenuBar;
|
|
import haxe.ui.containers.menus.MenuCheckBox;
|
|
import haxe.ui.containers.menus.MenuItem;
|
|
import haxe.ui.containers.ScrollView;
|
|
import haxe.ui.containers.TreeView;
|
|
import haxe.ui.containers.TreeViewNode;
|
|
import haxe.ui.core.Component;
|
|
import haxe.ui.core.Screen;
|
|
import haxe.ui.events.DragEvent;
|
|
import haxe.ui.events.MouseEvent;
|
|
import haxe.ui.events.UIEvent;
|
|
import haxe.ui.focus.FocusManager;
|
|
import haxe.ui.Toolkit;
|
|
import openfl.display.BitmapData;
|
|
|
|
using Lambda;
|
|
|
|
/**
|
|
* A state dedicated to allowing the user to create and edit song charts.
|
|
* Built with HaxeUI for use by both developers and modders.
|
|
*
|
|
* Some functionality is split into handler classes to help maintain my sanity.
|
|
*
|
|
* @author EliteMasterEric
|
|
*/
|
|
// @:nullSafety
|
|
|
|
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/main-view.xml"))
|
|
class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|
{
|
|
/**
|
|
* CONSTANTS
|
|
*/
|
|
// ==============================
|
|
// Layouts
|
|
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');
|
|
public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
|
|
public static final CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT:String = Paths.ui('chart-editor/toolbox/offsets');
|
|
public static final CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/note-data');
|
|
public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/event-data');
|
|
public static final CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT:String = Paths.ui('chart-editor/toolbox/freeplay');
|
|
public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties');
|
|
|
|
// Validation
|
|
public static final SUPPORTED_MUSIC_FORMATS:Array<String> = #if sys ['ogg'] #else ['mp3'] #end;
|
|
|
|
// 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 = Std.int(GRID_SIZE);
|
|
|
|
/**
|
|
* 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 height of the note selection buttons above the grid.
|
|
*/
|
|
public static final NOTE_SELECT_BUTTON_HEIGHT:Int = 24;
|
|
|
|
/**
|
|
* The amount of padding between the menu bar and the chart grid when fully scrolled up.
|
|
*/
|
|
public static final GRID_TOP_PAD:Int = NOTE_SELECT_BUTTON_HEIGHT + 12;
|
|
|
|
/**
|
|
* The initial vertical position of the chart grid.
|
|
*/
|
|
public static final GRID_INITIAL_Y_POS:Int = MENU_BAR_HEIGHT + GRID_TOP_PAD;
|
|
|
|
/**
|
|
* The X position of the note preview area.
|
|
*/
|
|
public static final NOTE_PREVIEW_X_POS:Int = 320;
|
|
|
|
/**
|
|
* The Y position of the note preview area.
|
|
*/
|
|
public static final NOTE_PREVIEW_Y_POS:Int = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 4;
|
|
|
|
/**
|
|
* The X position of the note grid.
|
|
*/
|
|
public static var GRID_X_POS(get, never):Float;
|
|
|
|
static function get_GRID_X_POS():Float
|
|
{
|
|
return FlxG.width / 2 - GRID_SIZE * STRUMLINE_SIZE;
|
|
}
|
|
|
|
// 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;
|
|
|
|
/**
|
|
* How many pixels far the user needs to move the mouse before the cursor is considered to be dragged rather than clicked.
|
|
*/
|
|
public static final DRAG_THRESHOLD:Float = 16.0;
|
|
|
|
/**
|
|
* Precisions of notes you can snap to.
|
|
*/
|
|
public static final SNAP_QUANTS:Array<Int> = [4, 8, 12, 16, 20, 24, 32, 48, 64, 96, 192];
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* The duration before the welcome music starts to fade back in after the user stops playing music in the chart editor.
|
|
*/
|
|
public static final WELCOME_MUSIC_FADE_IN_DELAY:Float = 30.0;
|
|
|
|
/**
|
|
* The duration of the welcome music fade in.
|
|
*/
|
|
public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0;
|
|
|
|
/**
|
|
* INSTANCE DATA
|
|
*/
|
|
// ==============================
|
|
// Song Length
|
|
|
|
/**
|
|
* The length of the current instrumental, 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;
|
|
|
|
updateGridHeight();
|
|
|
|
return this.songLengthInMs;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
var songLengthInSteps(get, set):Float;
|
|
|
|
function get_songLengthInSteps():Float
|
|
{
|
|
return Conductor.instance.getTimeInSteps(songLengthInMs);
|
|
}
|
|
|
|
function set_songLengthInSteps(value:Float):Float
|
|
{
|
|
// Getting a reasonable result from setting songLengthInSteps requires that Conductor.instance.mapBPMChanges be called first.
|
|
songLengthInMs = Conductor.instance.getStepTimeInMs(value);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* 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 && measureTicks != null)
|
|
{
|
|
if (isViewDownscroll)
|
|
{
|
|
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
|
|
measureTicks.y = gridTiledSprite.y;
|
|
}
|
|
else
|
|
{
|
|
gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS);
|
|
measureTicks.y = gridTiledSprite.y;
|
|
|
|
for (member in audioWaveforms.members)
|
|
{
|
|
member.time = scrollPositionInMs / Constants.MS_PER_SEC;
|
|
|
|
// Doing this desyncs the waveforms from the grid.
|
|
// member.y = Math.max(this.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
|
|
refreshNotePreviewPlayheadPosition();
|
|
|
|
// Update the measure tick display.
|
|
if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0;
|
|
return this.scrollPositionInPixels;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
function get_scrollPositionInSteps():Float
|
|
{
|
|
return scrollPositionInPixels / GRID_SIZE;
|
|
}
|
|
|
|
function set_scrollPositionInSteps(value:Float):Float
|
|
{
|
|
scrollPositionInPixels = value * GRID_SIZE;
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
function get_scrollPositionInMs():Float
|
|
{
|
|
return Conductor.instance.getStepTimeInMs(scrollPositionInSteps);
|
|
}
|
|
|
|
function set_scrollPositionInMs(value:Float):Float
|
|
{
|
|
scrollPositionInSteps = Conductor.instance.getTimeInSteps(value);
|
|
return value;
|
|
}
|
|
|
|
// Playhead (on the grid)
|
|
|
|
/**
|
|
* 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;
|
|
|
|
function set_playheadPositionInPixels(value:Float):Float
|
|
{
|
|
// Make sure playhead doesn't go outside the song.
|
|
if (value + scrollPositionInPixels < 0) value = -scrollPositionInPixels;
|
|
if (value + scrollPositionInPixels > songLengthInPixels) value = songLengthInPixels - scrollPositionInPixels;
|
|
|
|
this.playheadPositionInPixels = value;
|
|
|
|
// Move the playhead sprite to the correct position.
|
|
gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS;
|
|
|
|
updatePlayheadGhostHoldNotes();
|
|
refreshNotePreviewPlayheadPosition();
|
|
|
|
return this.playheadPositionInPixels;
|
|
}
|
|
|
|
/**
|
|
* playheadPosition, converted to steps.
|
|
* NOT dependant on BPM, because the size of a grid square does not change with BPM.
|
|
*/
|
|
var playheadPositionInSteps(get, set):Float;
|
|
|
|
function get_playheadPositionInSteps():Float
|
|
{
|
|
return playheadPositionInPixels / GRID_SIZE;
|
|
}
|
|
|
|
function set_playheadPositionInSteps(value:Float):Float
|
|
{
|
|
playheadPositionInPixels = value * GRID_SIZE;
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* playheadPosition, converted to milliseconds.
|
|
* DEPENDANT on BPM, because the duration of a grid square changes with BPM.
|
|
*/
|
|
var playheadPositionInMs(get, set):Float;
|
|
|
|
function get_playheadPositionInMs():Float
|
|
{
|
|
return Conductor.instance.getStepTimeInMs(playheadPositionInSteps);
|
|
}
|
|
|
|
function set_playheadPositionInMs(value:Float):Float
|
|
{
|
|
playheadPositionInSteps = Conductor.instance.getTimeInSteps(value);
|
|
|
|
return value;
|
|
}
|
|
|
|
// Playbar (at the bottom)
|
|
|
|
/**
|
|
* Whether a skip button has been pressed on the playbar, and which one.
|
|
* `null` if no button has been pressed.
|
|
* This will be used to update the scrollPosition (in the same function that handles the scroll wheel), then cleared.
|
|
*/
|
|
var playbarButtonPressed:Null<String> = null;
|
|
|
|
/**
|
|
* Whether the head of the playbar is currently being dragged with the mouse by the user.
|
|
*/
|
|
var playbarHeadDragging:Bool = false;
|
|
|
|
/**
|
|
* Whether music was playing before we started dragging the playbar head.
|
|
* If so, then when we stop dragging the playbar head, we should resume song playback.
|
|
*/
|
|
var playbarHeadDraggingWasPlaying:Bool = false;
|
|
|
|
// Tools Status
|
|
|
|
/**
|
|
* The note kind to use for notes being placed in the chart. Defaults to `null`.
|
|
*/
|
|
var noteKindToPlace:Null<String> = null;
|
|
|
|
/**
|
|
* The event type to use for events being placed in the chart. Defaults to `''`.
|
|
*/
|
|
var eventKindToPlace:String = 'FocusCamera';
|
|
|
|
/**
|
|
* The event data to use for events being placed in the chart.
|
|
*/
|
|
var eventDataToPlace:DynamicAccess<Dynamic> = {};
|
|
|
|
/**
|
|
* 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 noteSnapQuantIndex:Int = BASE_QUANT_INDEX;
|
|
|
|
/**
|
|
* The current note snapping value.
|
|
* For example, `32` when snapping to 32nd notes.
|
|
*/
|
|
var noteSnapQuant(get, never):Int;
|
|
|
|
function get_noteSnapQuant():Int
|
|
{
|
|
return SNAP_QUANTS[noteSnapQuantIndex];
|
|
}
|
|
|
|
/**
|
|
* The ratio of the current note snapping value to the default.
|
|
* For example, `32` becomes `0.5` when snapping to 16th notes.
|
|
*/
|
|
var noteSnapRatio(get, never):Float;
|
|
|
|
function get_noteSnapRatio():Float
|
|
{
|
|
return BASE_QUANT / noteSnapQuant;
|
|
}
|
|
|
|
/**
|
|
* The currently selected live input style.
|
|
*/
|
|
var currentLiveInputStyle:ChartEditorLiveInputStyle = None;
|
|
|
|
/**
|
|
* If true, playtesting a chart will skip to the current playhead position.
|
|
*/
|
|
var playtestStartTime:Bool = false;
|
|
|
|
/**
|
|
* If true, playtesting a chart will let you "gameover" / die when you lose ur health!
|
|
*/
|
|
var playtestPracticeMode:Bool = false;
|
|
|
|
/**
|
|
* If true, playtesting a chart will make the computer do it for you!
|
|
*/
|
|
var playtestBotPlayMode:Bool = false;
|
|
|
|
/**
|
|
* Enables or disables the "debugger" popup that appears when you run into a flixel error.
|
|
*/
|
|
var enabledDebuggerPopup:Bool = true;
|
|
|
|
/**
|
|
* Whether song scripts should be enabled during playtesting.
|
|
* You should probably check the box if the song has custom mechanics.
|
|
*/
|
|
var playtestSongScripts:Bool = true;
|
|
|
|
// Visuals
|
|
|
|
/**
|
|
* Whether the current view is in downscroll mode.
|
|
*/
|
|
var isViewDownscroll(default, set):Bool = false;
|
|
|
|
function set_isViewDownscroll(value:Bool):Bool
|
|
{
|
|
isViewDownscroll = value;
|
|
|
|
// Make sure view is updated when we change view modes.
|
|
noteDisplayDirty = true;
|
|
notePreviewDirty = true;
|
|
notePreviewViewportBoundsDirty = true;
|
|
this.scrollPositionInPixels = this.scrollPositionInPixels;
|
|
// Characters have probably changed too.
|
|
healthIconsDirty = true;
|
|
|
|
return isViewDownscroll;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
this.updateTheme();
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* The character sprite in the Player Preview window.
|
|
* `null` until accessed.
|
|
*/
|
|
var currentPlayerCharacterPlayer:Null<CharacterPlayer> = null;
|
|
|
|
/**
|
|
* The character sprite in the Opponent Preview window.
|
|
* `null` until accessed.
|
|
*/
|
|
var currentOpponentCharacterPlayer:Null<CharacterPlayer> = null;
|
|
|
|
// HaxeUI
|
|
|
|
/**
|
|
* Whether the user is focused on an input in the Haxe UI, and inputs are being fed into it.
|
|
* If the user clicks off the input, focus will leave.
|
|
*/
|
|
var isHaxeUIFocused(get, never):Bool;
|
|
|
|
function get_isHaxeUIFocused():Bool
|
|
{
|
|
return FocusManager.instance.focus != null;
|
|
}
|
|
|
|
/**
|
|
* Whether the user's mouse cursor is hovering over a SOLID component of the HaxeUI.
|
|
* If so, we can ignore certain mouse events underneath.
|
|
*/
|
|
var isCursorOverHaxeUI(get, never):Bool;
|
|
|
|
function get_isCursorOverHaxeUI():Bool
|
|
{
|
|
return Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
|
}
|
|
|
|
/**
|
|
* The value of `isCursorOverHaxeUI` from the previous frame.
|
|
* This is useful because we may have just clicked a menu item, causing the menu to disappear.
|
|
*/
|
|
var wasCursorOverHaxeUI:Bool = false;
|
|
|
|
/**
|
|
* Set by ChartEditorDialogHandler, used to prevent background interaction while the dialog is open.
|
|
*/
|
|
var isHaxeUIDialogOpen:Bool = false;
|
|
|
|
/**
|
|
* 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>();
|
|
|
|
/**
|
|
* The camera component we're using for this state.
|
|
*/
|
|
var uiCamera:FlxCamera;
|
|
|
|
// Audio
|
|
|
|
/**
|
|
* Whether to play a metronome sound while the playhead is moving, and what volume.
|
|
*/
|
|
var metronomeVolume:Float = 1.0;
|
|
|
|
/**
|
|
* The volume to play the player's hitsounds at.
|
|
*/
|
|
var hitsoundVolumePlayer:Float = 1.0;
|
|
|
|
/**
|
|
* The volume to play the opponent's hitsounds at.
|
|
*/
|
|
var hitsoundVolumeOpponent:Float = 1.0;
|
|
|
|
/**
|
|
* Whether hitsounds are enabled for at least one character.
|
|
*/
|
|
var hitsoundsEnabled(get, never):Bool;
|
|
|
|
function get_hitsoundsEnabled():Bool
|
|
{
|
|
return hitsoundVolumePlayer + hitsoundVolumeOpponent > 0;
|
|
}
|
|
|
|
// Auto-save
|
|
|
|
/**
|
|
* A timer used to auto-save the chart after a period of inactivity.
|
|
*/
|
|
var autoSaveTimer:Null<FlxTimer> = null;
|
|
|
|
// Scrolling
|
|
|
|
/**
|
|
* 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 current process that is lerping the scroll position.
|
|
* Used to cancel the previous lerp if the user scrolls again.
|
|
*/
|
|
var currentScrollEase:Null<VarTween>;
|
|
|
|
/**
|
|
* The position where the user middle clicked to place a scroll anchor.
|
|
* Scroll each frame with speed based on the distance between the mouse and the scroll anchor.
|
|
* `null` if no scroll anchor is present.
|
|
*/
|
|
var scrollAnchorScreenPos:Null<FlxPoint> = null;
|
|
|
|
// 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(default, set):Null<SongNoteData> = null;
|
|
|
|
function set_currentPlaceNoteData(value:Null<SongNoteData>):Null<SongNoteData>
|
|
{
|
|
noteDisplayDirty = true;
|
|
|
|
return currentPlaceNoteData = value;
|
|
}
|
|
|
|
/**
|
|
* The SongNoteData which is currently being placed, for each column.
|
|
* `null` if the user isn't currently placing a note.
|
|
* As the user moves down, we will update this note's sustain length, and finalize the note when they release.
|
|
*/
|
|
var currentLiveInputPlaceNoteData:Array<SongNoteData> = [];
|
|
|
|
// 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;
|
|
|
|
/**
|
|
* The current length of the hold note we are placing with the playhead, in steps.
|
|
* Play a sound when this value changes.
|
|
*/
|
|
var playheadDragLengthCurrent:Array<Float> = [];
|
|
|
|
/**
|
|
* 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(default, set):Array<SongNoteData> = [];
|
|
|
|
function set_currentNoteSelection(value:Array<SongNoteData>):Array<SongNoteData>
|
|
{
|
|
// This value is true if all elements of the current selection are also in the new selection.
|
|
var isSuperset:Bool = currentNoteSelection.isSubset(value);
|
|
var isEqual:Bool = currentNoteSelection.isEqualUnordered(value);
|
|
|
|
currentNoteSelection = value;
|
|
|
|
if (!isEqual)
|
|
{
|
|
if (currentNoteSelection.length > 0 && isSuperset)
|
|
{
|
|
notePreview.addSelectedNotes(currentNoteSelection, Std.int(songLengthInMs));
|
|
}
|
|
else
|
|
{
|
|
// The new selection removes elements from the old selection, so we have to redraw the note preview.
|
|
notePreviewDirty = true;
|
|
}
|
|
}
|
|
|
|
return currentNoteSelection;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* This happens when we scroll or add/remove notes, and need to update what notes are displayed and where.
|
|
*/
|
|
var noteDisplayDirty:Bool = true;
|
|
|
|
var noteTooltipsDirty:Bool = true;
|
|
|
|
/**
|
|
* Whether the selected charactesr have been modified and the health icons need to be updated.
|
|
*/
|
|
var healthIconsDirty:Bool = true;
|
|
|
|
/**
|
|
* Whether the note preview graphic needs to be FULLY rebuilt.
|
|
*/
|
|
var notePreviewDirty(default, set):Bool = true;
|
|
|
|
function set_notePreviewDirty(value:Bool):Bool
|
|
{
|
|
trace('Note preview dirtied!');
|
|
return notePreviewDirty = value;
|
|
}
|
|
|
|
var notePreviewViewportBoundsDirty:Bool = true;
|
|
|
|
/**
|
|
* Whether the chart has been modified since it was last saved.
|
|
* Used to determine whether to auto-save, etc.
|
|
*/
|
|
var saveDataDirty(default, set):Bool = false;
|
|
|
|
function set_saveDataDirty(value:Bool):Bool
|
|
{
|
|
if (value == saveDataDirty) return value;
|
|
|
|
if (value)
|
|
{
|
|
// Start the auto-save timer.
|
|
autoSaveTimer = new FlxTimer().start(Constants.AUTOSAVE_TIMER_DELAY_SEC, (_) -> autoSave());
|
|
}
|
|
else
|
|
{
|
|
if (autoSaveTimer != null)
|
|
{
|
|
// Stop the auto-save timer.
|
|
autoSaveTimer.cancel();
|
|
autoSaveTimer.destroy();
|
|
autoSaveTimer = null;
|
|
}
|
|
}
|
|
|
|
saveDataDirty = value;
|
|
applyWindowTitle();
|
|
return saveDataDirty;
|
|
}
|
|
|
|
var shouldShowBackupAvailableDialog(get, set):Bool;
|
|
|
|
function get_shouldShowBackupAvailableDialog():Bool
|
|
{
|
|
return Save.instance.chartEditorHasBackup;
|
|
}
|
|
|
|
function set_shouldShowBackupAvailableDialog(value:Bool):Bool
|
|
{
|
|
return Save.instance.chartEditorHasBackup = value;
|
|
}
|
|
|
|
/**
|
|
* A list of previous working file paths.
|
|
* Also known as the "recent files" list.
|
|
* The first element is [null] if the current working file has not been saved anywhere yet.
|
|
*/
|
|
public var previousWorkingFilePaths(default, set):Array<Null<String>> = [null];
|
|
|
|
function set_previousWorkingFilePaths(value:Array<Null<String>>):Array<Null<String>>
|
|
{
|
|
// Called only when the WHOLE LIST is overridden.
|
|
previousWorkingFilePaths = value;
|
|
applyWindowTitle();
|
|
populateOpenRecentMenu();
|
|
applyCanQuickSave();
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* The current file path which the chart editor is working with.
|
|
* If `null`, the current chart has not been saved yet.
|
|
*/
|
|
public var currentWorkingFilePath(get, set):Null<String>;
|
|
|
|
function get_currentWorkingFilePath():Null<String>
|
|
{
|
|
return previousWorkingFilePaths[0];
|
|
}
|
|
|
|
function set_currentWorkingFilePath(value:Null<String>):Null<String>
|
|
{
|
|
if (value == previousWorkingFilePaths[0]) return value;
|
|
|
|
if (previousWorkingFilePaths.contains(null))
|
|
{
|
|
// Filter all instances of `null` from the array.
|
|
previousWorkingFilePaths = previousWorkingFilePaths.filter(function(x:Null<String>):Bool {
|
|
return x != null;
|
|
});
|
|
}
|
|
|
|
if (previousWorkingFilePaths.contains(value))
|
|
{
|
|
// Move the path to the front of the list.
|
|
previousWorkingFilePaths.remove(value);
|
|
previousWorkingFilePaths.unshift(value);
|
|
}
|
|
else
|
|
{
|
|
// Add the path to the front of the list.
|
|
previousWorkingFilePaths.unshift(value);
|
|
}
|
|
|
|
while (previousWorkingFilePaths.length > Constants.MAX_PREVIOUS_WORKING_FILES)
|
|
{
|
|
// Remove the last path in the list.
|
|
previousWorkingFilePaths.pop();
|
|
}
|
|
|
|
populateOpenRecentMenu();
|
|
applyWindowTitle();
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Whether the difficulty tree view in the toolbox has been modified and needs to be updated.
|
|
* This happens when we add/remove difficulties.
|
|
*/
|
|
var difficultySelectDirty:Bool = true;
|
|
|
|
/**
|
|
* Whether the character select view in the toolbox has been modified and needs to be updated.
|
|
* This happens when we add/remove characters.
|
|
*/
|
|
var characterSelectDirty:Bool = true;
|
|
|
|
/**
|
|
* Whether the player preview toolbox have been modified and need to be updated.
|
|
* This happens when we switch characters.
|
|
*/
|
|
var playerPreviewDirty:Bool = true;
|
|
|
|
/**
|
|
* Whether the opponent preview toolbox have been modified and need to be updated.
|
|
* This happens when we switch characters.
|
|
*/
|
|
var opponentPreviewDirty:Bool = true;
|
|
|
|
/**
|
|
* Whether the undo/redo histories have changed since the last time the UI was updated.
|
|
*/
|
|
var commandHistoryDirty:Bool = true;
|
|
|
|
/**
|
|
* If true, we are currently in the process of quitting the chart editor.
|
|
* Skip any update functions as most of them will call a crash.
|
|
*/
|
|
var criticalFailure:Bool = false;
|
|
|
|
// Input
|
|
|
|
/**
|
|
* Handler used to track how long the user has been holding the undo keybind.
|
|
*/
|
|
var undoKeyHandler:TurboKeyHandler = TurboKeyHandler.build([FlxKey.CONTROL, FlxKey.Z]);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding the redo keybind.
|
|
*/
|
|
var redoKeyHandler:TurboKeyHandler = TurboKeyHandler.build([FlxKey.CONTROL, FlxKey.Y]);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding the up keybind.
|
|
*/
|
|
var upKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.UP);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding the down keybind.
|
|
*/
|
|
var downKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.DOWN);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding the W keybind.
|
|
*/
|
|
var wKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.W);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding the S keybind.
|
|
*/
|
|
var sKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.S);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding the page-up keybind.
|
|
*/
|
|
var pageUpKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEUP);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding the page-down keybind.
|
|
*/
|
|
var pageDownKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEDOWN);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding up on the dpad.
|
|
*/
|
|
var dpadUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_UP);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding down on the dpad.
|
|
*/
|
|
var dpadDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_DOWN);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding left on the dpad.
|
|
*/
|
|
var dpadLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_LEFT);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding right on the dpad.
|
|
*/
|
|
var dpadRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.DPAD_RIGHT);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding up on the left stick.
|
|
*/
|
|
var leftStickUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_UP);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding down on the left stick.
|
|
*/
|
|
var leftStickDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_DOWN);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding left on the left stick.
|
|
*/
|
|
var leftStickLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_LEFT);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding right on the left stick.
|
|
*/
|
|
var leftStickRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.LEFT_STICK_DIGITAL_RIGHT);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding up on the right stick.
|
|
*/
|
|
var rightStickUpGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_UP);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding down on the right stick.
|
|
*/
|
|
var rightStickDownGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_DOWN);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding left on the right stick.
|
|
*/
|
|
var rightStickLeftGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_LEFT);
|
|
|
|
/**
|
|
* Variable used to track how long the user has been holding right on the right stick.
|
|
*/
|
|
var rightStickRightGamepadHandler:TurboButtonHandler = TurboButtonHandler.build(FlxGamepadInputID.RIGHT_STICK_DIGITAL_RIGHT);
|
|
|
|
/**
|
|
* AUDIO AND SOUND DATA
|
|
*/
|
|
// ==============================
|
|
|
|
/**
|
|
* The chill audio track that plays in the chart editor.
|
|
* Plays when the main music is NOT being played.
|
|
*/
|
|
var welcomeMusic:FunkinSound = new FunkinSound();
|
|
|
|
/**
|
|
* The audio track for the instrumental.
|
|
* Replaced when switching instrumentals.
|
|
* `null` until an instrumental track is loaded.
|
|
*/
|
|
var audioInstTrack:Null<FunkinSound> = null;
|
|
|
|
/**
|
|
* The raw byte data for the instrumental audio tracks.
|
|
* Key is the instrumental name.
|
|
* `null` until an instrumental track is loaded.
|
|
*/
|
|
var audioInstTrackData:Map<String, Bytes> = [];
|
|
|
|
/**
|
|
* The audio track for the vocals.
|
|
* `null` until vocal track(s) are loaded.
|
|
* When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
|
|
*/
|
|
var audioVocalTrackGroup:VoicesGroup = new VoicesGroup();
|
|
|
|
/**
|
|
* The audio waveform visualization for the inst/vocals.
|
|
* `null` until vocal track(s) are loaded.
|
|
* When switching characters, the elements will be swapped to match the new character.
|
|
*/
|
|
var audioWaveforms:FlxTypedSpriteGroup<WaveformSprite> = new FlxTypedSpriteGroup<WaveformSprite>();
|
|
|
|
/**
|
|
* A map of the audio tracks for each character's vocals.
|
|
* - Keys are `characterId-variation` (with `characterId` being the default variation).
|
|
* - Values are the byte data for the audio track.
|
|
*/
|
|
var audioVocalTrackData:Map<String, Bytes> = [];
|
|
|
|
/**
|
|
* CHART DATA
|
|
*/
|
|
// ==============================
|
|
|
|
/**
|
|
* The song metadata.
|
|
* - Keys are the variation IDs. At least one (`default`) must exist.
|
|
* - Values are the relevant metadata, ready to be serialized to JSON.
|
|
*/
|
|
var songMetadata:Map<String, SongMetadata> = [];
|
|
|
|
/**
|
|
* Retrieves the list of variations for the current song.
|
|
*/
|
|
var availableVariations(get, never):Array<String>;
|
|
|
|
function get_availableVariations():Array<String>
|
|
{
|
|
var variations:Array<String> = [for (x in songMetadata.keys()) x];
|
|
variations.sort(SortUtil.defaultThenAlphabetically.bind('default'));
|
|
return variations;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the list of difficulties for the current variation of the current song.
|
|
* ONLY CONTAINS DIFFICULTIES FOR THE CURRENT VARIATION so if on the default variation, erect/nightmare won't be included.
|
|
*/
|
|
var availableDifficulties(get, never):Array<String>;
|
|
|
|
function get_availableDifficulties():Array<String>
|
|
{
|
|
var m:Null<SongMetadata> = songMetadata.get(selectedVariation);
|
|
return m?.playData?.difficulties ?? [Constants.DEFAULT_DIFFICULTY];
|
|
}
|
|
|
|
/**
|
|
* Retrieves the list of difficulties for ALL variations of the current song.
|
|
*/
|
|
var allDifficulties(get, never):Array<String>;
|
|
|
|
function get_allDifficulties():Array<String>
|
|
{
|
|
var result:Array<Array<String>> = [
|
|
for (x in availableVariations)
|
|
{
|
|
var m:Null<SongMetadata> = songMetadata.get(x);
|
|
m?.playData?.difficulties ?? [];
|
|
}
|
|
];
|
|
return result.flatten();
|
|
}
|
|
|
|
/**
|
|
* The song chart data.
|
|
* - Keys are the variation IDs. At least one (`default`) must exist.
|
|
* - Values are the relevant chart data, ready to be serialized to JSON.
|
|
*/
|
|
var songChartData:Map<String, SongChartData> = [];
|
|
|
|
/**
|
|
* Convenience property to get the chart data for the current variation.
|
|
*/
|
|
var currentSongMetadata(get, set):SongMetadata;
|
|
|
|
function get_currentSongMetadata():SongMetadata
|
|
{
|
|
var result:Null<SongMetadata> = songMetadata.get(selectedVariation);
|
|
if (result == null)
|
|
{
|
|
result = new SongMetadata('Default Song Name', Constants.DEFAULT_ARTIST, selectedVariation);
|
|
songMetadata.set(selectedVariation, result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function set_currentSongMetadata(value:SongMetadata):SongMetadata
|
|
{
|
|
songMetadata.set(selectedVariation, value);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Convenience property to get the chart data for the current variation.
|
|
*/
|
|
var currentSongChartData(get, set):SongChartData;
|
|
|
|
function get_currentSongChartData():SongChartData
|
|
{
|
|
var result:Null<SongChartData> = songChartData.get(selectedVariation);
|
|
if (result == null)
|
|
{
|
|
result = new SongChartData([Constants.DEFAULT_DIFFICULTY => 1.0], [], [Constants.DEFAULT_DIFFICULTY => []]);
|
|
songChartData.set(selectedVariation, result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function set_currentSongChartData(value:SongChartData):SongChartData
|
|
{
|
|
songChartData.set(selectedVariation, value);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Convenience property to get (and set) the scroll speed for the current difficulty.
|
|
*/
|
|
var currentSongChartScrollSpeed(get, set):Float;
|
|
|
|
function get_currentSongChartScrollSpeed():Float
|
|
{
|
|
var result:Null<Float> = currentSongChartData.scrollSpeed.get(selectedDifficulty);
|
|
if (result == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongChartData.scrollSpeed.set(selectedDifficulty, 1.0);
|
|
return 1.0;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function set_currentSongChartScrollSpeed(value:Float):Float
|
|
{
|
|
currentSongChartData.scrollSpeed.set(selectedDifficulty, value);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Convenience property to get the note data for the current difficulty.
|
|
*/
|
|
var currentSongChartNoteData(get, set):Array<SongNoteData>;
|
|
|
|
function get_currentSongChartNoteData():Array<SongNoteData>
|
|
{
|
|
var result:Null<Array<SongNoteData>> = currentSongChartData.notes.get(selectedDifficulty);
|
|
if (result == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
result = [];
|
|
trace('Initializing blank chart for difficulty ' + selectedDifficulty);
|
|
currentSongChartData.notes.set(selectedDifficulty, result);
|
|
currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty);
|
|
return result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function set_currentSongChartNoteData(value:Array<SongNoteData>):Array<SongNoteData>
|
|
{
|
|
currentSongChartData.notes.set(selectedDifficulty, value);
|
|
currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Convenience property to get the event data for the current difficulty.
|
|
*/
|
|
var currentSongChartEventData(get, set):Array<SongEventData>;
|
|
|
|
function get_currentSongChartEventData():Array<SongEventData>
|
|
{
|
|
if (currentSongChartData.events == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongChartData.events = [];
|
|
}
|
|
return currentSongChartData.events;
|
|
}
|
|
|
|
function set_currentSongChartEventData(value:Array<SongEventData>):Array<SongEventData>
|
|
{
|
|
return currentSongChartData.events = value;
|
|
}
|
|
|
|
/**
|
|
* Convenience property to get the rating for this difficulty in the Freeplay menu.
|
|
*/
|
|
var currentSongChartDifficultyRating(get, set):Int;
|
|
|
|
function get_currentSongChartDifficultyRating():Int
|
|
{
|
|
var result:Null<Int> = currentSongMetadata.playData.ratings.get(selectedDifficulty);
|
|
if (result == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongMetadata.playData.ratings.set(selectedDifficulty, 0);
|
|
return 0;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function set_currentSongChartDifficultyRating(value:Int):Int
|
|
{
|
|
currentSongMetadata.playData.ratings.set(selectedDifficulty, value);
|
|
return value;
|
|
}
|
|
|
|
var currentSongNoteStyle(get, set):String;
|
|
|
|
function get_currentSongNoteStyle():String
|
|
{
|
|
if (currentSongMetadata.playData.noteStyle == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE;
|
|
}
|
|
return currentSongMetadata.playData.noteStyle;
|
|
}
|
|
|
|
function set_currentSongNoteStyle(value:String):String
|
|
{
|
|
return currentSongMetadata.playData.noteStyle = value;
|
|
}
|
|
|
|
var currentSongFreeplayPreviewStart(get, set):Int;
|
|
|
|
function get_currentSongFreeplayPreviewStart():Int
|
|
{
|
|
return currentSongMetadata.playData.previewStart;
|
|
}
|
|
|
|
function set_currentSongFreeplayPreviewStart(value:Int):Int
|
|
{
|
|
return currentSongMetadata.playData.previewStart = value;
|
|
}
|
|
|
|
var currentSongFreeplayPreviewEnd(get, set):Int;
|
|
|
|
function get_currentSongFreeplayPreviewEnd():Int
|
|
{
|
|
return currentSongMetadata.playData.previewEnd;
|
|
}
|
|
|
|
function set_currentSongFreeplayPreviewEnd(value:Int):Int
|
|
{
|
|
return currentSongMetadata.playData.previewEnd = value;
|
|
}
|
|
|
|
var currentSongStage(get, set):String;
|
|
|
|
function get_currentSongStage():String
|
|
{
|
|
if (currentSongMetadata.playData.stage == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongMetadata.playData.stage = 'mainStage';
|
|
}
|
|
return currentSongMetadata.playData.stage;
|
|
}
|
|
|
|
function set_currentSongStage(value:String):String
|
|
{
|
|
return currentSongMetadata.playData.stage = value;
|
|
}
|
|
|
|
var currentSongName(get, set):String;
|
|
|
|
function get_currentSongName():String
|
|
{
|
|
if (currentSongMetadata.songName == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongMetadata.songName = 'New Song';
|
|
}
|
|
return currentSongMetadata.songName;
|
|
}
|
|
|
|
function set_currentSongName(value:String):String
|
|
{
|
|
return currentSongMetadata.songName = value;
|
|
}
|
|
|
|
var currentSongId(get, never):String;
|
|
|
|
function get_currentSongId():String
|
|
{
|
|
return currentSongName.toLowerKebabCase().replace(' ', '-').sanitize();
|
|
}
|
|
|
|
var currentSongArtist(get, set):String;
|
|
|
|
function get_currentSongArtist():String
|
|
{
|
|
if (currentSongMetadata.artist == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongMetadata.artist = 'Unknown';
|
|
}
|
|
return currentSongMetadata.artist;
|
|
}
|
|
|
|
function set_currentSongArtist(value:String):String
|
|
{
|
|
return currentSongMetadata.artist = value;
|
|
}
|
|
|
|
/**
|
|
* Convenience property to get the player charId for the current variation.
|
|
*/
|
|
var currentPlayerChar(get, set):String;
|
|
|
|
function get_currentPlayerChar():String
|
|
{
|
|
if (currentSongMetadata.playData.characters.player == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongMetadata.playData.characters.player = Constants.DEFAULT_CHARACTER;
|
|
}
|
|
return currentSongMetadata.playData.characters.player;
|
|
}
|
|
|
|
function set_currentPlayerChar(value:String):String
|
|
{
|
|
return currentSongMetadata.playData.characters.player = value;
|
|
}
|
|
|
|
/**
|
|
* Convenience property to get the opponent charId for the current variation.
|
|
*/
|
|
var currentOpponentChar(get, set):String;
|
|
|
|
function get_currentOpponentChar():String
|
|
{
|
|
if (currentSongMetadata.playData.characters.opponent == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongMetadata.playData.characters.opponent = Constants.DEFAULT_CHARACTER;
|
|
}
|
|
return currentSongMetadata.playData.characters.opponent;
|
|
}
|
|
|
|
function set_currentOpponentChar(value:String):String
|
|
{
|
|
return currentSongMetadata.playData.characters.opponent = value;
|
|
}
|
|
|
|
/**
|
|
* Convenience property to get the song offset data for the current variation.
|
|
*/
|
|
var currentSongOffsets(get, set):SongOffsets;
|
|
|
|
function get_currentSongOffsets():SongOffsets
|
|
{
|
|
if (currentSongMetadata.offsets == null)
|
|
{
|
|
// Initialize to the default value if not set.
|
|
currentSongMetadata.offsets = new SongOffsets();
|
|
}
|
|
return currentSongMetadata.offsets;
|
|
}
|
|
|
|
function set_currentSongOffsets(value:SongOffsets):SongOffsets
|
|
{
|
|
return currentSongMetadata.offsets = value;
|
|
}
|
|
|
|
var currentInstrumentalOffset(get, set):Float;
|
|
|
|
function get_currentInstrumentalOffset():Float
|
|
{
|
|
// TODO: Apply for alt instrumentals.
|
|
return currentSongOffsets.getInstrumentalOffset();
|
|
}
|
|
|
|
function set_currentInstrumentalOffset(value:Float):Float
|
|
{
|
|
// TODO: Apply for alt instrumentals.
|
|
currentSongOffsets.setInstrumentalOffset(value);
|
|
return value;
|
|
}
|
|
|
|
var currentVocalOffsetPlayer(get, set):Float;
|
|
|
|
function get_currentVocalOffsetPlayer():Float
|
|
{
|
|
return currentSongOffsets.getVocalOffset(currentPlayerChar);
|
|
}
|
|
|
|
function set_currentVocalOffsetPlayer(value:Float):Float
|
|
{
|
|
currentSongOffsets.setVocalOffset(currentPlayerChar, value);
|
|
return value;
|
|
}
|
|
|
|
var currentVocalOffsetOpponent(get, set):Float;
|
|
|
|
function get_currentVocalOffsetOpponent():Float
|
|
{
|
|
return currentSongOffsets.getVocalOffset(currentOpponentChar);
|
|
}
|
|
|
|
function set_currentVocalOffsetOpponent(value:Float):Float
|
|
{
|
|
currentSongOffsets.setVocalOffset(currentOpponentChar, value);
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* The variation ID for the difficulty which is currently being edited.
|
|
*/
|
|
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;
|
|
noteTooltipsDirty = 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
|
|
{
|
|
if (value == null) value = availableDifficulties[0] ?? Constants.DEFAULT_DIFFICULTY;
|
|
|
|
selectedDifficulty = value;
|
|
|
|
// Make sure view is updated when the difficulty changes.
|
|
noteDisplayDirty = true;
|
|
notePreviewDirty = true;
|
|
noteTooltipsDirty = 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;
|
|
}
|
|
|
|
/**
|
|
* HAXEUI COMPONENTS
|
|
*/
|
|
// ==============================
|
|
|
|
/**
|
|
* The layout containing the playbar.
|
|
* Constructed manually and added to the layout so we can control its position.
|
|
*/
|
|
var playbarHeadLayout:Null<ChartEditorPlaybarHead> = null;
|
|
|
|
// NOTE: All the components below are automatically assigned via HaxeUI macros.
|
|
|
|
/**
|
|
* The menubar at the top of the screen.
|
|
*/
|
|
var menubar:MenuBar;
|
|
|
|
/**
|
|
* The `File -> New Chart` menu item.
|
|
*/
|
|
var menubarItemNewChart:MenuItem;
|
|
|
|
/**
|
|
* The `File -> Open Chart` menu item.
|
|
*/
|
|
var menubarItemOpenChart:MenuItem;
|
|
|
|
/**
|
|
* The `File -> Open Recent` menu.
|
|
*/
|
|
var menubarOpenRecent:Menu;
|
|
|
|
/**
|
|
* The `File -> Save Chart` menu item.
|
|
*/
|
|
var menubarItemSaveChart:MenuItem;
|
|
|
|
/**
|
|
* The `File -> Save Chart As` menu item.
|
|
*/
|
|
var menubarItemSaveChartAs:MenuItem;
|
|
|
|
/**
|
|
* The `File -> Preferences` menu item.
|
|
*/
|
|
var menubarItemPreferences:MenuItem;
|
|
|
|
/**
|
|
* The `File -> Exit` menu item.
|
|
*/
|
|
var menubarItemExit:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Undo` menu item.
|
|
*/
|
|
var menubarItemUndo:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Redo` menu item.
|
|
*/
|
|
var menubarItemRedo:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Cut` menu item.
|
|
*/
|
|
var menubarItemCut:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Copy` menu item.
|
|
*/
|
|
var menubarItemCopy:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Paste` menu item.
|
|
*/
|
|
var menubarItemPaste:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Paste Unsnapped` menu item.
|
|
*/
|
|
var menubarItemPasteUnsnapped:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Delete` menu item.
|
|
*/
|
|
var menubarItemDelete:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Flip Notes` menu item.
|
|
*/
|
|
var menubarItemFlipNotes:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Select All` menu item.
|
|
*/
|
|
var menubarItemSelectAll:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Select Inverse` menu item.
|
|
*/
|
|
var menubarItemSelectInverse:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Select None` menu item.
|
|
*/
|
|
var menubarItemSelectNone:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Select Region` menu item.
|
|
*/
|
|
var menubarItemSelectRegion:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Select Before Cursor` menu item.
|
|
*/
|
|
var menubarItemSelectBeforeCursor:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Select After Cursor` menu item.
|
|
*/
|
|
var menubarItemSelectAfterCursor:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Decrease Note Snap Precision` menu item.
|
|
*/
|
|
var menuBarItemNoteSnapDecrease:MenuItem;
|
|
|
|
/**
|
|
* The `Edit -> Decrease Note Snap Precision` menu item.
|
|
*/
|
|
var menuBarItemNoteSnapIncrease:MenuItem;
|
|
|
|
/**
|
|
* The `View -> Downscroll` menu item.
|
|
*/
|
|
var menubarItemDownscroll:MenuCheckBox;
|
|
|
|
/**
|
|
* The `View -> Increase Difficulty` menu item.
|
|
*/
|
|
var menubarItemDifficultyUp:MenuItem;
|
|
|
|
/**
|
|
* The `View -> Decrease Difficulty` menu item.
|
|
*/
|
|
var menubarItemDifficultyDown:MenuItem;
|
|
|
|
/**
|
|
* The `Audio -> Play/Pause` menu item.
|
|
*/
|
|
var menubarItemPlayPause:MenuItem;
|
|
|
|
/**
|
|
* The `Audio -> Load Instrumental` menu item.
|
|
*/
|
|
var menubarItemLoadInstrumental:MenuItem;
|
|
|
|
/**
|
|
* The `Audio -> Load Vocals` menu item.
|
|
*/
|
|
var menubarItemLoadVocals:MenuItem;
|
|
|
|
/**
|
|
* The `Audio -> Metronome Volume` label.
|
|
*/
|
|
var menubarLabelVolumeMetronome:Label;
|
|
|
|
/**
|
|
* The `Audio -> Metronome Volume` slider.
|
|
*/
|
|
var menubarItemVolumeMetronome:Slider;
|
|
|
|
/**
|
|
* The `Audio -> Play Theme Music` menu checkbox.
|
|
*/
|
|
var menubarItemThemeMusic:MenuCheckBox;
|
|
|
|
/**
|
|
* The `Audio -> Player Hitsound Volume` label.
|
|
*/
|
|
var menubarLabelVolumeHitsoundPlayer:Label;
|
|
|
|
/**
|
|
* The `Audio -> Enemy Hitsound Volume` label.
|
|
*/
|
|
var menubarLabelVolumeHitsoundOpponent:Label;
|
|
|
|
/**
|
|
* The `Audio -> Player Hitsound Volume` slider.
|
|
*/
|
|
var menubarItemVolumeHitsoundPlayer:Slider;
|
|
|
|
/**
|
|
* The `Audio -> Enemy Hitsound Volume` slider.
|
|
*/
|
|
var menubarItemVolumeHitsoundOpponent:Slider;
|
|
|
|
/**
|
|
* The `Audio -> Instrumental Volume` label.
|
|
*/
|
|
var menubarLabelVolumeInstrumental:Label;
|
|
|
|
/**
|
|
* The `Audio -> Instrumental Volume` slider.
|
|
*/
|
|
var menubarItemVolumeInstrumental:Slider;
|
|
|
|
/**
|
|
* The `Audio -> Player Volume` label.
|
|
*/
|
|
var menubarLabelVolumeVocalsPlayer:Label;
|
|
|
|
/**
|
|
* The `Audio -> Enemy Volume` label.
|
|
*/
|
|
var menubarLabelVolumeVocalsOpponent:Label;
|
|
|
|
/**
|
|
* The `Audio -> Player Volume` slider.
|
|
*/
|
|
var menubarItemVolumeVocalsPlayer:Slider;
|
|
|
|
/**
|
|
* The `Audio -> Enemy Volume` slider.
|
|
*/
|
|
var menubarItemVolumeVocalsOpponent:Slider;
|
|
|
|
/**
|
|
* The `Audio -> Playback Speed` label.
|
|
*/
|
|
var menubarLabelPlaybackSpeed:Label;
|
|
|
|
/**
|
|
* The `Audio -> Playback Speed` slider.
|
|
*/
|
|
var menubarItemPlaybackSpeed:Slider;
|
|
|
|
/**
|
|
* The label by the playbar telling the song position.
|
|
*/
|
|
var playbarSongPos:Label;
|
|
|
|
/**
|
|
* The label by the playbar telling the song time remaining.
|
|
*/
|
|
var playbarSongRemaining:Label;
|
|
|
|
/**
|
|
* The label by the playbar telling the note snap.
|
|
*/
|
|
var playbarNoteSnap:Label;
|
|
|
|
/**
|
|
* The button by the playbar to jump to the start of the song.
|
|
*/
|
|
var playbarStart:Button;
|
|
|
|
/**
|
|
* The button by the playbar to jump backwards in the song.
|
|
*/
|
|
var playbarBack:Button;
|
|
|
|
/**
|
|
* The button by the playbar to play or pause the song.
|
|
*/
|
|
var playbarPlay:Button;
|
|
|
|
/**
|
|
* The button by the playbar to jump forwards in the song.
|
|
*/
|
|
var playbarForward:Button;
|
|
|
|
/**
|
|
* The button by the playbar to jump to the end of the song.
|
|
*/
|
|
var playbarEnd:Button;
|
|
|
|
/**
|
|
* The button above the grid that selects all notes on the opponent's side.
|
|
* Constructed manually and added to the layout so we can control its position.
|
|
*/
|
|
var buttonSelectOpponent:Button;
|
|
|
|
/**
|
|
* The button above the grid that selects all notes on the player's side.
|
|
* Constructed manually and added to the layout so we can control its position.
|
|
*/
|
|
var buttonSelectPlayer:Button;
|
|
|
|
/**
|
|
* The button above the grid that selects all song events.
|
|
* Constructed manually and added to the layout so we can control its position.
|
|
*/
|
|
var buttonSelectEvent:Button;
|
|
|
|
/**
|
|
* The slider above the grid that sets the volume of the player's sounds.
|
|
* Constructed manually and added to the layout so we can control its position.
|
|
*/
|
|
var sliderVolumePlayer:Slider;
|
|
|
|
/**
|
|
* The slider above the grid that sets the volume of the opponent's sounds.
|
|
* Constructed manually and added to the layout so we can control its position.
|
|
*/
|
|
var sliderVolumeOpponent:Slider;
|
|
|
|
/**
|
|
* RENDER OBJECTS
|
|
*/
|
|
// ==============================
|
|
|
|
/**
|
|
* The group containing the visulizers! */
|
|
var visulizerGrps:FlxTypedGroup<PolygonSpectogram> = null;
|
|
|
|
/**
|
|
* The IMAGE used for the grid. Updated by ChartEditorThemeHandler.
|
|
*/
|
|
var gridBitmap:Null<BitmapData> = null;
|
|
|
|
/**
|
|
* The IMAGE used for the selection squares. Updated by ChartEditorThemeHandler.
|
|
* Used two ways:
|
|
* 1. A sprite is given this bitmap and placed over selected notes.
|
|
* 2. The image is split and used for a 9-slice sprite for the selection box.
|
|
*/
|
|
var selectionSquareBitmap:Null<BitmapData> = null;
|
|
|
|
/**
|
|
* The IMAGE used for the note preview bitmap. Updated by ChartEditorThemeHandler.
|
|
* The image is split and used for a 9-slice sprite for the box over the note preview.
|
|
*/
|
|
var notePreviewViewportBitmap:Null<BitmapData> = null;
|
|
|
|
/**
|
|
* The IMAGE used for the measure ticks. Updated by ChartEditorThemeHandler.
|
|
*/
|
|
var measureTickBitmap:Null<BitmapData> = null;
|
|
|
|
/**
|
|
* The IMAGE used for the offset ticks. Updated by ChartEditorThemeHandler.
|
|
*/
|
|
var offsetTickBitmap:Null<BitmapData> = null;
|
|
|
|
/**
|
|
* The tiled sprite used to display the grid.
|
|
* The height is the length of the song, and scrolling is done by simply the sprite.
|
|
*/
|
|
var gridTiledSprite:Null<FlxSprite> = null;
|
|
|
|
/**
|
|
* The measure ticks area. Includes the numbers and the background sprite.
|
|
*/
|
|
var measureTicks:Null<ChartEditorMeasureTicks> = null;
|
|
|
|
/**
|
|
* The playhead representing the current position in the song.
|
|
* Can move around on the grid independently of the view.
|
|
*/
|
|
var gridPlayhead:FlxSpriteGroup = new FlxSpriteGroup();
|
|
|
|
/**
|
|
* A sprite used to indicate the note that will be placed on click.
|
|
*/
|
|
var gridGhostNote:Null<ChartEditorNoteSprite> = null;
|
|
|
|
/**
|
|
* A sprite used to indicate the hold note that will be placed on click.
|
|
*/
|
|
var gridGhostHoldNote:Null<ChartEditorHoldNoteSprite> = null;
|
|
|
|
/**
|
|
* A sprite used to indicate the hold note that will be placed on button release.
|
|
*/
|
|
var gridPlayheadGhostHoldNotes:Array<ChartEditorHoldNoteSprite> = [];
|
|
|
|
/**
|
|
* A sprite used to indicate the event that will be placed on click.
|
|
*/
|
|
var gridGhostEvent:Null<ChartEditorEventSprite> = null;
|
|
|
|
/**
|
|
* The sprite used to display the note preview area.
|
|
* We move this up and down to scroll the preview.
|
|
*/
|
|
var notePreview:Null<ChartEditorNotePreview> = null;
|
|
|
|
/**
|
|
* The rectangular sprite used for representing the current viewport on the note preview.
|
|
* We move this up and down and resize it to represent the visible area.
|
|
*/
|
|
var notePreviewViewport:Null<FlxSliceSprite> = null;
|
|
|
|
/**
|
|
* The thin sprite used for representing the playhead on the note preview.
|
|
* We move this up and down to represent the current position.
|
|
*/
|
|
var notePreviewPlayhead:Null<FlxSprite> = null;
|
|
|
|
/**
|
|
* The rectangular sprite used for rendering the selection box.
|
|
* Uses a 9-slice to stretch the selection box to the correct size without warping.
|
|
*/
|
|
var selectionBoxSprite:Null<FlxSliceSprite> = null;
|
|
|
|
/**
|
|
* The opponent's health icon.
|
|
*/
|
|
var healthIconDad:Null<HealthIcon> = null;
|
|
|
|
/**
|
|
* The player's health icon.
|
|
*/
|
|
var healthIconBF:Null<HealthIcon> = null;
|
|
|
|
/**
|
|
* The text that pop's up when copying something
|
|
*/
|
|
var txtCopyNotif:Null<FlxText> = null;
|
|
|
|
/**
|
|
* The purple background sprite.
|
|
*/
|
|
var menuBG:Null<FlxSprite> = null;
|
|
|
|
/**
|
|
* The sprite group containing the note graphics.
|
|
* Only displays a subset of the data from `currentSongChartNoteData`,
|
|
* and kills notes that are off-screen to be recycled later.
|
|
*/
|
|
var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite> = new FlxTypedSpriteGroup<ChartEditorNoteSprite>();
|
|
|
|
/**
|
|
* The sprite group containing the hold note graphics.
|
|
* Only displays a subset of the data from `currentSongChartNoteData`,
|
|
* and kills notes that are off-screen to be recycled later.
|
|
*/
|
|
var renderedHoldNotes:FlxTypedSpriteGroup<ChartEditorHoldNoteSprite> = new FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>();
|
|
|
|
/**
|
|
* The sprite group containing the song events.
|
|
* Only displays a subset of the data from `currentSongChartEventData`,
|
|
* and kills events that are off-screen to be recycled later.
|
|
*/
|
|
var renderedEvents:FlxTypedSpriteGroup<ChartEditorEventSprite> = new FlxTypedSpriteGroup<ChartEditorEventSprite>();
|
|
|
|
var renderedSelectionSquares:FlxTypedSpriteGroup<ChartEditorSelectionSquareSprite> = new FlxTypedSpriteGroup<ChartEditorSelectionSquareSprite>();
|
|
|
|
/**
|
|
* LIFE CYCLE FUNCTIONS
|
|
*/
|
|
// ==============================
|
|
|
|
/**
|
|
* The params which were passed in when the Chart Editor was initialized.
|
|
*/
|
|
var params:Null<ChartEditorParams>;
|
|
|
|
public function new(?params:ChartEditorParams)
|
|
{
|
|
super();
|
|
this.params = params;
|
|
}
|
|
|
|
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.
|
|
super.create();
|
|
// Set the z-index of the HaxeUI.
|
|
this.root.zIndex = 100;
|
|
|
|
// Get rid of any music from the previous state.
|
|
if (FlxG.sound.music != null) FlxG.sound.music.stop();
|
|
|
|
// Play the welcome music.
|
|
setupWelcomeMusic();
|
|
|
|
// Show the mouse cursor.
|
|
Cursor.show();
|
|
|
|
loadPreferences();
|
|
|
|
uiCamera = new FunkinCamera('chartEditorUI');
|
|
FlxG.cameras.reset(uiCamera);
|
|
|
|
buildDefaultSongData();
|
|
|
|
buildBackground();
|
|
|
|
this.updateTheme();
|
|
|
|
buildGrid();
|
|
buildMeasureTicks();
|
|
buildNotePreview();
|
|
|
|
buildAdditionalUI();
|
|
populateOpenRecentMenu();
|
|
this.applyPlatformShortcutText();
|
|
|
|
// Setup the onClick listeners for the UI after it's been created.
|
|
setupUIListeners();
|
|
setupContextMenu();
|
|
setupTurboKeyHandlers();
|
|
|
|
setupAutoSave();
|
|
|
|
refresh();
|
|
|
|
if (params != null && params.fnfcTargetPath != null)
|
|
{
|
|
// Chart editor was opened from the command line. Open the FNFC file now!
|
|
var result:Null<Array<String>> = this.loadFromFNFCPath(params.fnfcTargetPath);
|
|
if (result != null)
|
|
{
|
|
if (result.length == 0)
|
|
{
|
|
this.success('Loaded Chart', 'Loaded chart (${params.fnfcTargetPath})');
|
|
}
|
|
else
|
|
{
|
|
this.warning('Loaded Chart', 'Loaded chart with issues (${params.fnfcTargetPath})\n${result.join("\n")}');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.error('Failure', 'Failed to load chart (${params.fnfcTargetPath})');
|
|
|
|
// Song failed to load, open the Welcome dialog so we aren't in a broken state.
|
|
var welcomeDialog = this.openWelcomeDialog(false);
|
|
if (shouldShowBackupAvailableDialog)
|
|
{
|
|
this.openBackupAvailableDialog(welcomeDialog);
|
|
}
|
|
}
|
|
}
|
|
else if (params != null && params.targetSongId != null)
|
|
{
|
|
this.loadSongAsTemplate(params.targetSongId);
|
|
}
|
|
else
|
|
{
|
|
var welcomeDialog = this.openWelcomeDialog(false);
|
|
if (shouldShowBackupAvailableDialog)
|
|
{
|
|
this.openBackupAvailableDialog(welcomeDialog);
|
|
}
|
|
}
|
|
}
|
|
|
|
function setupWelcomeMusic()
|
|
{
|
|
this.welcomeMusic.loadEmbedded(Paths.music('chartEditorLoop/chartEditorLoop'));
|
|
FlxG.sound.list.add(this.welcomeMusic);
|
|
this.welcomeMusic.looped = true;
|
|
}
|
|
|
|
public function loadPreferences():Void
|
|
{
|
|
var save:Save = Save.instance;
|
|
|
|
if (previousWorkingFilePaths[0] == null)
|
|
{
|
|
previousWorkingFilePaths = [null].concat(save.chartEditorPreviousFiles);
|
|
}
|
|
else
|
|
{
|
|
previousWorkingFilePaths = [currentWorkingFilePath].concat(save.chartEditorPreviousFiles);
|
|
}
|
|
noteSnapQuantIndex = save.chartEditorNoteQuant;
|
|
currentLiveInputStyle = save.chartEditorLiveInputStyle;
|
|
isViewDownscroll = save.chartEditorDownscroll;
|
|
playtestStartTime = save.chartEditorPlaytestStartTime;
|
|
currentTheme = save.chartEditorTheme;
|
|
metronomeVolume = save.chartEditorMetronomeVolume;
|
|
hitsoundVolumePlayer = save.chartEditorHitsoundVolumePlayer;
|
|
hitsoundVolumePlayer = save.chartEditorHitsoundVolumeOpponent;
|
|
this.welcomeMusic.active = save.chartEditorThemeMusic;
|
|
|
|
// audioInstTrack.volume = save.chartEditorInstVolume;
|
|
// audioInstTrack.pitch = save.chartEditorPlaybackSpeed;
|
|
// audioVocalTrackGroup.volume = save.chartEditorVoicesVolume;
|
|
// audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed;
|
|
}
|
|
|
|
public function writePreferences(hasBackup:Bool):Void
|
|
{
|
|
var save:Save = Save.instance;
|
|
|
|
// Can't use filter() because of null safety checking!
|
|
var filteredWorkingFilePaths:Array<String> = [];
|
|
for (chartPath in previousWorkingFilePaths)
|
|
if (chartPath != null) filteredWorkingFilePaths.push(chartPath);
|
|
save.chartEditorPreviousFiles = filteredWorkingFilePaths;
|
|
|
|
if (hasBackup) trace('Queuing backup prompt for next time!');
|
|
save.chartEditorHasBackup = hasBackup;
|
|
|
|
save.chartEditorNoteQuant = noteSnapQuantIndex;
|
|
save.chartEditorLiveInputStyle = currentLiveInputStyle;
|
|
save.chartEditorDownscroll = isViewDownscroll;
|
|
save.chartEditorPlaytestStartTime = playtestStartTime;
|
|
save.chartEditorTheme = currentTheme;
|
|
save.chartEditorMetronomeVolume = metronomeVolume;
|
|
save.chartEditorHitsoundVolumePlayer = hitsoundVolumePlayer;
|
|
save.chartEditorHitsoundVolumeOpponent = hitsoundVolumeOpponent;
|
|
save.chartEditorThemeMusic = this.welcomeMusic.active;
|
|
|
|
// save.chartEditorInstVolume = audioInstTrack.volume;
|
|
// save.chartEditorVoicesVolume = audioVocalTrackGroup.volume;
|
|
// save.chartEditorPlaybackSpeed = audioInstTrack.pitch;
|
|
}
|
|
|
|
public function populateOpenRecentMenu():Void
|
|
{
|
|
if (menubarOpenRecent == null) return;
|
|
|
|
#if sys
|
|
menubarOpenRecent.removeAllComponents();
|
|
|
|
for (chartPath in previousWorkingFilePaths)
|
|
{
|
|
if (chartPath == null) continue;
|
|
|
|
var menuItemRecentChart:MenuItem = new MenuItem();
|
|
menuItemRecentChart.text = chartPath;
|
|
menuItemRecentChart.onClick = function(_event) {
|
|
// Load chart from file
|
|
var result:Null<Array<String>> = this.loadFromFNFCPath(chartPath);
|
|
if (result != null)
|
|
{
|
|
if (result.length == 0)
|
|
{
|
|
this.success('Loaded Chart', 'Loaded chart (${chartPath.toString()})');
|
|
}
|
|
else
|
|
{
|
|
this.warning('Loaded Chart', 'Loaded chart with issues (${chartPath.toString()})\n${result.join("\n")}');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this.error('Failure', 'Failed to load chart (${chartPath.toString()})');
|
|
}
|
|
}
|
|
|
|
if (!FileUtil.doesFileExist(chartPath))
|
|
{
|
|
trace('Previously loaded chart file (${chartPath.toString()}) does not exist, disabling link...');
|
|
menuItemRecentChart.disabled = true;
|
|
}
|
|
else
|
|
{
|
|
menuItemRecentChart.disabled = false;
|
|
}
|
|
|
|
menubarOpenRecent.addComponent(menuItemRecentChart);
|
|
}
|
|
#else
|
|
menubarOpenRecent.hide();
|
|
#end
|
|
}
|
|
|
|
var bgMusicTimer:FlxTimer;
|
|
|
|
function fadeInWelcomeMusic(?extraWait:Float = 0, ?fadeInTime:Float = 5):Void
|
|
{
|
|
if (!this.welcomeMusic.active)
|
|
{
|
|
stopWelcomeMusic();
|
|
return;
|
|
}
|
|
|
|
bgMusicTimer = new FlxTimer().start(extraWait, (_) -> {
|
|
this.welcomeMusic.volume = 0;
|
|
if (this.welcomeMusic.active)
|
|
{
|
|
this.welcomeMusic.play();
|
|
this.welcomeMusic.fadeIn(fadeInTime, 0, 1.0);
|
|
}
|
|
});
|
|
}
|
|
|
|
function stopWelcomeMusic():Void
|
|
{
|
|
if (bgMusicTimer != null) bgMusicTimer.cancel();
|
|
// this.welcomeMusic.fadeOut(4, 0);
|
|
this.welcomeMusic.pause();
|
|
}
|
|
|
|
function buildDefaultSongData():Void
|
|
{
|
|
selectedVariation = Constants.DEFAULT_VARIATION;
|
|
selectedDifficulty = Constants.DEFAULT_DIFFICULTY;
|
|
|
|
// Initialize the song metadata.
|
|
songMetadata = new Map<String, SongMetadata>();
|
|
|
|
// Initialize the song chart data.
|
|
songChartData = new Map<String, SongChartData>();
|
|
}
|
|
|
|
/**
|
|
* Builds and displays the background sprite.
|
|
*/
|
|
function buildBackground():Void
|
|
{
|
|
menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat'));
|
|
add(menuBG);
|
|
|
|
menuBG.setGraphicSize(Std.int(menuBG.width * 1.1));
|
|
menuBG.updateHitbox();
|
|
menuBG.screenCenter();
|
|
menuBG.scrollFactor.set(0, 0);
|
|
menuBG.zIndex = -100;
|
|
}
|
|
|
|
var oppSpectogram:PolygonSpectogram;
|
|
|
|
/**
|
|
* Builds and displays the chart editor grid, including the playhead and cursor.
|
|
*/
|
|
function buildGrid():Void
|
|
{
|
|
if (gridBitmap == null) throw 'ERROR: Tried to build grid, but gridBitmap is null! Check ChartEditorThemeHandler.updateTheme().';
|
|
|
|
gridTiledSprite = new FlxTiledSprite(gridBitmap, gridBitmap.width, 1000, false, true);
|
|
gridTiledSprite.x = GRID_X_POS; // Center the grid.
|
|
gridTiledSprite.y = GRID_INITIAL_Y_POS; // Push down to account for the menu bar.
|
|
add(gridTiledSprite);
|
|
gridTiledSprite.zIndex = 10;
|
|
|
|
gridGhostNote = new ChartEditorNoteSprite(this);
|
|
gridGhostNote.alpha = 0.6;
|
|
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "");
|
|
gridGhostNote.visible = false;
|
|
add(gridGhostNote);
|
|
gridGhostNote.zIndex = 11;
|
|
|
|
gridGhostHoldNote = new ChartEditorHoldNoteSprite(this);
|
|
gridGhostHoldNote.alpha = 0.6;
|
|
gridGhostHoldNote.noteData = null;
|
|
gridGhostHoldNote.visible = false;
|
|
add(gridGhostHoldNote);
|
|
gridGhostHoldNote.zIndex = 11;
|
|
|
|
gridGhostEvent = new ChartEditorEventSprite(this, true);
|
|
gridGhostEvent.alpha = 0.6;
|
|
gridGhostEvent.eventData = new SongEventData(-1, '', {});
|
|
gridGhostEvent.visible = false;
|
|
add(gridGhostEvent);
|
|
gridGhostEvent.zIndex = 12;
|
|
|
|
buildNoteGroup();
|
|
|
|
// The playhead that show the current position in the song.
|
|
add(gridPlayhead);
|
|
gridPlayhead.zIndex = 30;
|
|
|
|
var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2);
|
|
var playheadBaseYPos:Float = GRID_INITIAL_Y_POS;
|
|
gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos);
|
|
var playheadSprite:FunkinSprite = new FunkinSprite().makeSolidColor(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
|
|
playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH;
|
|
playheadSprite.y = 0;
|
|
gridPlayhead.add(playheadSprite);
|
|
|
|
var playheadBlock:FlxSprite = ChartEditorThemeHandler.buildPlayheadBlock();
|
|
playheadBlock.x = -PLAYHEAD_SCROLL_AREA_WIDTH;
|
|
playheadBlock.y = -PLAYHEAD_HEIGHT / 2;
|
|
gridPlayhead.add(playheadBlock);
|
|
|
|
// Character icons.
|
|
healthIconDad = new HealthIcon(currentSongMetadata.playData.characters.opponent);
|
|
healthIconDad.autoUpdate = false;
|
|
healthIconDad.size.set(0.5, 0.5);
|
|
add(healthIconDad);
|
|
healthIconDad.zIndex = 30;
|
|
|
|
healthIconBF = new HealthIcon(currentSongMetadata.playData.characters.player);
|
|
healthIconBF.autoUpdate = false;
|
|
healthIconBF.size.set(0.5, 0.5);
|
|
healthIconBF.flipX = true;
|
|
add(healthIconBF);
|
|
healthIconBF.zIndex = 30;
|
|
|
|
add(audioWaveforms);
|
|
}
|
|
|
|
function buildMeasureTicks():Void
|
|
{
|
|
measureTicks = new ChartEditorMeasureTicks(this);
|
|
var measureTicksWidth = (GRID_SIZE);
|
|
measureTicks.x = gridTiledSprite.x - measureTicksWidth;
|
|
measureTicks.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
|
|
measureTicks.zIndex = 20;
|
|
|
|
add(measureTicks);
|
|
}
|
|
|
|
function buildNotePreview():Void
|
|
{
|
|
var playbarHeightWithPad = PLAYBAR_HEIGHT + 10;
|
|
var notePreviewHeight:Int = FlxG.height - NOTE_PREVIEW_Y_POS - playbarHeightWithPad;
|
|
notePreview = new ChartEditorNotePreview(notePreviewHeight);
|
|
notePreview.x = NOTE_PREVIEW_X_POS;
|
|
notePreview.y = NOTE_PREVIEW_Y_POS;
|
|
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;
|
|
|
|
notePreviewPlayhead = new FlxSprite().makeGraphic(2, 2, 0xFFFF0000);
|
|
notePreviewPlayhead.scrollFactor.set(0, 0);
|
|
notePreviewPlayhead.scale.set(notePreview.width / 2, 0.5); // Setting width does nothing.
|
|
notePreviewPlayhead.updateHitbox();
|
|
notePreviewPlayhead.x = notePreview.x;
|
|
notePreviewPlayhead.y = notePreview.y;
|
|
add(notePreviewPlayhead);
|
|
notePreviewPlayhead.zIndex = 31;
|
|
|
|
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
|
|
}
|
|
|
|
function setSelectionBoxBounds(bounds:FlxRect = null):Void
|
|
{
|
|
if (selectionBoxSprite == null)
|
|
throw 'ERROR: Tried to set selection box bounds, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().';
|
|
|
|
if (bounds == null)
|
|
{
|
|
selectionBoxSprite.visible = false;
|
|
selectionBoxSprite.x = -9999;
|
|
selectionBoxSprite.y = -9999;
|
|
}
|
|
else
|
|
{
|
|
selectionBoxSprite.visible = true;
|
|
selectionBoxSprite.x = bounds.x;
|
|
selectionBoxSprite.y = bounds.y;
|
|
selectionBoxSprite.width = bounds.width;
|
|
selectionBoxSprite.height = bounds.height;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Automatically goes through and calls render on everything you added.
|
|
*/
|
|
override public function draw():Void
|
|
{
|
|
super.draw();
|
|
}
|
|
|
|
function calculateNotePreviewViewportBounds():FlxRect
|
|
{
|
|
var bounds:FlxRect = new FlxRect();
|
|
|
|
// Return 0, 0, 0, 0 if the note preview doesn't exist for some reason.
|
|
if (notePreview == null) return bounds;
|
|
|
|
// Horizontal position and width are constant.
|
|
bounds.x = notePreview.x;
|
|
bounds.width = notePreview.width;
|
|
|
|
// Vertical position depends on scroll position.
|
|
bounds.y = notePreview.y + (notePreview.height * (scrollPositionInPixels / songLengthInPixels));
|
|
|
|
// Height depends on the viewport size.
|
|
bounds.height = notePreview.height * (FlxG.height / songLengthInPixels);
|
|
|
|
// Make sure the viewport doesn't go off the top or bottom of the note preview.
|
|
if (bounds.y < notePreview.y)
|
|
{
|
|
bounds.height -= notePreview.y - bounds.y;
|
|
bounds.y = notePreview.y;
|
|
}
|
|
else if (bounds.y + bounds.height > notePreview.y + notePreview.height)
|
|
{
|
|
bounds.height -= (bounds.y + bounds.height) - (notePreview.y + notePreview.height);
|
|
}
|
|
|
|
var MIN_HEIGHT:Int = 8;
|
|
if (bounds.height < MIN_HEIGHT)
|
|
{
|
|
bounds.y -= MIN_HEIGHT - bounds.height;
|
|
bounds.height = MIN_HEIGHT;
|
|
}
|
|
|
|
// trace('Note preview viewport bounds: ' + bounds.toString());
|
|
|
|
return bounds;
|
|
}
|
|
|
|
function setNotePreviewViewportBounds(bounds:FlxRect = null):Void
|
|
{
|
|
if (notePreviewViewport == null)
|
|
{
|
|
trace('[WARN] Tried to set note preview viewport bounds, but notePreviewViewport is null!');
|
|
return;
|
|
}
|
|
|
|
if (bounds == null)
|
|
{
|
|
notePreviewViewport.visible = false;
|
|
notePreviewViewport.x = -9999;
|
|
notePreviewViewport.y = -9999;
|
|
}
|
|
else
|
|
{
|
|
notePreviewViewport.visible = true;
|
|
notePreviewViewport.x = bounds.x;
|
|
notePreviewViewport.y = bounds.y;
|
|
notePreviewViewport.width = bounds.width;
|
|
notePreviewViewport.height = bounds.height;
|
|
}
|
|
}
|
|
|
|
function refreshNotePreviewPlayheadPosition():Void
|
|
{
|
|
if (notePreviewPlayhead == null) return;
|
|
|
|
notePreviewPlayhead.y = notePreview.y + (notePreview.height * ((scrollPositionInPixels + playheadPositionInPixels) / songLengthInPixels));
|
|
}
|
|
|
|
/**
|
|
* Builds the group that will hold all the notes.
|
|
*/
|
|
function buildNoteGroup():Void
|
|
{
|
|
if (gridTiledSprite == null) throw 'ERROR: Tried to build note groups, but gridTiledSprite is null! Check ChartEditorState.buildGrid().';
|
|
|
|
renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
|
|
add(renderedHoldNotes);
|
|
renderedHoldNotes.zIndex = 24;
|
|
|
|
renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
|
|
add(renderedNotes);
|
|
renderedNotes.zIndex = 25;
|
|
|
|
renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y);
|
|
add(renderedEvents);
|
|
renderedEvents.zIndex = 25;
|
|
|
|
renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y);
|
|
add(renderedSelectionSquares);
|
|
renderedSelectionSquares.zIndex = 26;
|
|
}
|
|
|
|
function buildAdditionalUI():Void
|
|
{
|
|
playbarHeadLayout = new ChartEditorPlaybarHead();
|
|
|
|
playbarHeadLayout.zIndex = 110;
|
|
playbarHeadLayout.width = FlxG.width - 8;
|
|
playbarHeadLayout.height = 10;
|
|
playbarHeadLayout.x = 4;
|
|
playbarHeadLayout.y = FlxG.height - 48 - 8;
|
|
|
|
playbarHeadLayout.playbarHead.allowFocus = false;
|
|
playbarHeadLayout.playbarHead.width = FlxG.width;
|
|
playbarHeadLayout.playbarHead.height = 10;
|
|
playbarHeadLayout.playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;';
|
|
|
|
playbarHeadLayout.playbarHead.onDragStart = function(_:DragEvent) {
|
|
playbarHeadDragging = true;
|
|
|
|
// If we were dragging the playhead while the song was playing, resume playing.
|
|
if (audioInstTrack != null && audioInstTrack.isPlaying)
|
|
{
|
|
playbarHeadDraggingWasPlaying = true;
|
|
stopAudioPlayback();
|
|
}
|
|
else
|
|
{
|
|
playbarHeadDraggingWasPlaying = false;
|
|
}
|
|
}
|
|
|
|
playbarHeadLayout.playbarHead.onDrag = function(_:DragEvent) {
|
|
if (playbarHeadDragging)
|
|
{
|
|
// Set the song position to where the playhead was moved to.
|
|
scrollPositionInPixels = (songLengthInPixels) * playbarHeadLayout.playbarHead.value / 100;
|
|
// Update the conductor and audio tracks to match.
|
|
moveSongToScrollPosition();
|
|
}
|
|
}
|
|
|
|
playbarHeadLayout.playbarHead.onDragEnd = function(_:DragEvent) {
|
|
playbarHeadDragging = false;
|
|
|
|
// If we were dragging the playhead while the song was playing, resume playing.
|
|
if (playbarHeadDraggingWasPlaying)
|
|
{
|
|
playbarHeadDraggingWasPlaying = false;
|
|
// Disabled code to resume song playback on drag.
|
|
// startAudioPlayback();
|
|
}
|
|
}
|
|
|
|
add(playbarHeadLayout);
|
|
|
|
// Little text that shows up when you copy something.
|
|
txtCopyNotif = new FlxText(0, 0, 0, '', 24);
|
|
txtCopyNotif.setBorderStyle(OUTLINE, 0xFF074809, 1);
|
|
txtCopyNotif.color = 0xFF52FF77;
|
|
txtCopyNotif.zIndex = 120;
|
|
add(txtCopyNotif);
|
|
|
|
if (!Preferences.debugDisplay) menubar.paddingLeft = null;
|
|
|
|
this.setupNotifications();
|
|
|
|
// Setup character dropdowns.
|
|
FlxMouseEvent.add(healthIconDad, function(_) {
|
|
if (!isCursorOverHaxeUI)
|
|
{
|
|
this.openCharacterDropdown(CharacterType.DAD, true);
|
|
}
|
|
});
|
|
|
|
FlxMouseEvent.add(healthIconBF, function(_) {
|
|
if (!isCursorOverHaxeUI)
|
|
{
|
|
this.openCharacterDropdown(CharacterType.BF, true);
|
|
}
|
|
});
|
|
|
|
buttonSelectOpponent = new Button();
|
|
buttonSelectOpponent.allowFocus = false;
|
|
buttonSelectOpponent.text = "Opponent"; // Default text.
|
|
buttonSelectOpponent.x = GRID_X_POS;
|
|
buttonSelectOpponent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 8;
|
|
buttonSelectOpponent.width = GRID_SIZE * 4;
|
|
buttonSelectOpponent.height = NOTE_SELECT_BUTTON_HEIGHT;
|
|
buttonSelectOpponent.tooltip = "Click to set selection to all notes on this side.\nShift-click to add all notes on this side to selection.";
|
|
buttonSelectOpponent.zIndex = 110;
|
|
add(buttonSelectOpponent);
|
|
|
|
buttonSelectOpponent.onClick = (_) -> {
|
|
var notesToSelect:Array<SongNoteData> = currentSongChartNoteData;
|
|
notesToSelect = SongDataUtils.getNotesInDataRange(notesToSelect, STRUMLINE_SIZE, STRUMLINE_SIZE * 2 - 1);
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
performCommand(new SelectItemsCommand(notesToSelect, []));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SetItemSelectionCommand(notesToSelect, []));
|
|
}
|
|
}
|
|
|
|
buttonSelectPlayer = new Button();
|
|
buttonSelectPlayer.allowFocus = false;
|
|
buttonSelectPlayer.text = "Player"; // Default text.
|
|
buttonSelectPlayer.x = buttonSelectOpponent.x + buttonSelectOpponent.width;
|
|
buttonSelectPlayer.y = buttonSelectOpponent.y;
|
|
buttonSelectPlayer.width = GRID_SIZE * 4;
|
|
buttonSelectPlayer.height = NOTE_SELECT_BUTTON_HEIGHT;
|
|
buttonSelectPlayer.tooltip = "Click to set selection to all notes on this side.\nShift-click to add all notes on this side to selection.";
|
|
buttonSelectPlayer.zIndex = 110;
|
|
add(buttonSelectPlayer);
|
|
|
|
buttonSelectPlayer.onClick = (_) -> {
|
|
var notesToSelect:Array<SongNoteData> = currentSongChartNoteData;
|
|
notesToSelect = SongDataUtils.getNotesInDataRange(notesToSelect, 0, STRUMLINE_SIZE - 1);
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
performCommand(new SelectItemsCommand(notesToSelect, []));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SetItemSelectionCommand(notesToSelect, []));
|
|
}
|
|
}
|
|
|
|
buttonSelectEvent = new Button();
|
|
buttonSelectEvent.allowFocus = false;
|
|
buttonSelectEvent.icon = Paths.image('ui/chart-editor/events/Default');
|
|
buttonSelectEvent.iconPosition = "top";
|
|
buttonSelectEvent.x = buttonSelectPlayer.x + buttonSelectPlayer.width;
|
|
buttonSelectEvent.y = buttonSelectPlayer.y;
|
|
buttonSelectEvent.width = GRID_SIZE;
|
|
buttonSelectEvent.height = NOTE_SELECT_BUTTON_HEIGHT;
|
|
buttonSelectEvent.tooltip = "Click to set selection to all events.\nShift-click to add all events to selection.";
|
|
buttonSelectEvent.zIndex = 110;
|
|
add(buttonSelectEvent);
|
|
|
|
buttonSelectEvent.onClick = (_) -> {
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
performCommand(new SelectItemsCommand([], currentSongChartEventData));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SetItemSelectionCommand([], currentSongChartEventData));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up the onClick listeners for the UI.
|
|
*/
|
|
function setupUIListeners():Void
|
|
{
|
|
// Add functionality to the playbar.
|
|
|
|
playbarStart.onClick = _ -> playbarButtonPressed = 'playbarStart';
|
|
playbarBack.onClick = _ -> playbarButtonPressed = 'playbarBack';
|
|
playbarPlay.onClick = _ -> toggleAudioPlayback();
|
|
playbarForward.onClick = _ -> playbarButtonPressed = 'playbarForward';
|
|
playbarEnd.onClick = _ -> playbarButtonPressed = 'playbarEnd';
|
|
|
|
// Cycle note snap quant.
|
|
playbarNoteSnap.onRightClick = _ -> {
|
|
noteSnapQuantIndex--;
|
|
if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
|
|
};
|
|
playbarNoteSnap.onClick = _ -> {
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
noteSnapQuantIndex = BASE_QUANT_INDEX;
|
|
}
|
|
else
|
|
{
|
|
noteSnapQuantIndex++;
|
|
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
|
|
}
|
|
};
|
|
|
|
playbarBPM.onClick = _ -> {
|
|
if (FlxG.keys.pressed.CONTROL)
|
|
{
|
|
this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, true);
|
|
}
|
|
else
|
|
{
|
|
Conductor.instance.currentTimeChange.bpm += 1;
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
|
}
|
|
}
|
|
|
|
playbarBPM.onRightClick = _ -> {
|
|
Conductor.instance.currentTimeChange.bpm -= 1;
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
|
}
|
|
|
|
playbarDifficulty.onClick = _ -> {
|
|
if (FlxG.keys.pressed.CONTROL)
|
|
{
|
|
this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, true);
|
|
}
|
|
else
|
|
{
|
|
incrementDifficulty(-1);
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
|
}
|
|
}
|
|
|
|
playbarDifficulty.onRightClick = _ -> {
|
|
incrementDifficulty(1);
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
|
}
|
|
|
|
// Add functionality to the menu items.
|
|
|
|
// File
|
|
menubarItemNewChart.onClick = _ -> this.openWelcomeDialog(true);
|
|
menubarItemOpenChart.onClick = _ -> this.openBrowseFNFC(true);
|
|
menubarItemSaveChart.onClick = _ -> {
|
|
if (currentWorkingFilePath != null)
|
|
{
|
|
this.exportAllSongData(true, currentWorkingFilePath);
|
|
}
|
|
else
|
|
{
|
|
this.exportAllSongData(false, null);
|
|
}
|
|
};
|
|
menubarItemSaveChartAs.onClick = _ -> this.exportAllSongData(false, null);
|
|
menubarItemExit.onClick = _ -> quitChartEditor();
|
|
|
|
// Edit
|
|
menubarItemUndo.onClick = _ -> undoLastCommand();
|
|
menubarItemRedo.onClick = _ -> redoLastCommand();
|
|
menubarItemCopy.onClick = function(_) {
|
|
copySelection();
|
|
};
|
|
menubarItemCut.onClick = _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection));
|
|
|
|
menubarItemPaste.onClick = _ -> {
|
|
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
|
|
var targetStep:Float = Conductor.instance.getTimeInSteps(targetMs);
|
|
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
|
|
var targetSnappedMs:Float = Conductor.instance.getStepTimeInMs(targetSnappedStep);
|
|
performCommand(new PasteItemsCommand(targetSnappedMs));
|
|
};
|
|
|
|
menubarItemPasteUnsnapped.onClick = _ -> {
|
|
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
|
|
performCommand(new PasteItemsCommand(targetMs));
|
|
};
|
|
|
|
menubarItemDelete.onClick = _ -> {
|
|
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
|
|
{
|
|
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
|
|
}
|
|
else if (currentNoteSelection.length > 0)
|
|
{
|
|
performCommand(new RemoveNotesCommand(currentNoteSelection));
|
|
}
|
|
else if (currentEventSelection.length > 0)
|
|
{
|
|
performCommand(new RemoveEventsCommand(currentEventSelection));
|
|
}
|
|
else
|
|
{
|
|
// Do nothing???
|
|
}
|
|
};
|
|
|
|
menubarItemFlipNotes.onClick = _ -> performCommand(new FlipNotesCommand(currentNoteSelection));
|
|
|
|
menubarItemSelectAllNotes.onClick = _ -> performCommand(new SelectAllItemsCommand(true, false));
|
|
|
|
menubarItemSelectAllEvents.onClick = _ -> performCommand(new SelectAllItemsCommand(false, true));
|
|
|
|
menubarItemSelectInverse.onClick = _ -> performCommand(new InvertSelectedItemsCommand());
|
|
|
|
menubarItemSelectNone.onClick = _ -> performCommand(new DeselectAllItemsCommand());
|
|
|
|
menubarItemPlaytestFull.onClick = _ -> testSongInPlayState(false);
|
|
menubarItemPlaytestMinimal.onClick = _ -> testSongInPlayState(true);
|
|
|
|
menuBarItemNoteSnapDecrease.onClick = _ -> {
|
|
noteSnapQuantIndex--;
|
|
if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
|
|
};
|
|
menuBarItemNoteSnapIncrease.onClick = _ -> {
|
|
noteSnapQuantIndex++;
|
|
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
|
|
};
|
|
|
|
menuBarItemInputStyleNone.onClick = function(event:UIEvent) {
|
|
currentLiveInputStyle = None;
|
|
};
|
|
menuBarItemInputStyleNone.selected = currentLiveInputStyle == None;
|
|
menuBarItemInputStyleNumberKeys.onClick = function(event:UIEvent) {
|
|
currentLiveInputStyle = NumberKeys;
|
|
};
|
|
menuBarItemInputStyleNumberKeys.selected = currentLiveInputStyle == NumberKeys;
|
|
menuBarItemInputStyleWASD.onClick = function(event:UIEvent) {
|
|
currentLiveInputStyle = WASDKeys;
|
|
};
|
|
menuBarItemInputStyleWASD.selected = currentLiveInputStyle == WASDKeys;
|
|
|
|
menubarItemAbout.onClick = _ -> this.openAboutDialog();
|
|
menubarItemWelcomeDialog.onClick = _ -> this.openWelcomeDialog(true);
|
|
|
|
#if sys
|
|
menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder();
|
|
#else
|
|
// Disable the menu item if we're not on a desktop platform.
|
|
menubarItemGoToBackupsFolder.disabled = true;
|
|
#end
|
|
|
|
menubarItemUserGuide.onClick = _ -> this.openUserGuideDialog();
|
|
|
|
menubarItemDownscroll.onClick = event -> isViewDownscroll = event.value;
|
|
menubarItemDownscroll.selected = isViewDownscroll;
|
|
|
|
menubarItemDifficultyUp.onClick = _ -> incrementDifficulty(1);
|
|
menubarItemDifficultyDown.onClick = _ -> incrementDifficulty(-1);
|
|
|
|
menuBarItemThemeLight.onChange = function(event:UIEvent) {
|
|
if (event.target.value) currentTheme = ChartEditorTheme.Light;
|
|
};
|
|
menuBarItemThemeLight.selected = currentTheme == ChartEditorTheme.Light;
|
|
|
|
menuBarItemThemeDark.onChange = function(event:UIEvent) {
|
|
if (event.target.value) currentTheme = ChartEditorTheme.Dark;
|
|
};
|
|
menuBarItemThemeDark.selected = currentTheme == ChartEditorTheme.Dark;
|
|
|
|
menubarItemPlayPause.onClick = _ -> toggleAudioPlayback();
|
|
|
|
menubarItemLoadInstrumental.onClick = _ -> {
|
|
var dialog = this.openUploadInstDialog(true);
|
|
// Ensure instrumental and vocals are reloaded properly.
|
|
dialog.onDialogClosed = function(_) {
|
|
this.isHaxeUIDialogOpen = false;
|
|
this.switchToCurrentInstrumental();
|
|
this.postLoadInstrumental();
|
|
}
|
|
};
|
|
|
|
menubarItemLoadVocals.onClick = _ -> {
|
|
var dialog = this.openUploadVocalsDialog(true);
|
|
// Ensure instrumental and vocals are reloaded properly.
|
|
dialog.onDialogClosed = function(_) {
|
|
this.isHaxeUIDialogOpen = false;
|
|
this.switchToCurrentInstrumental();
|
|
this.postLoadInstrumental();
|
|
}
|
|
};
|
|
|
|
menubarItemVolumeMetronome.onChange = event -> {
|
|
var volume:Float = event.value.toFloat() / 100.0;
|
|
metronomeVolume = volume;
|
|
menubarLabelVolumeMetronome.text = 'Metronome - ${Std.int(event.value)}%';
|
|
};
|
|
menubarItemVolumeMetronome.value = Std.int(metronomeVolume * 100);
|
|
|
|
menubarItemThemeMusic.onChange = event -> {
|
|
this.welcomeMusic.active = event.value;
|
|
fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION);
|
|
};
|
|
menubarItemThemeMusic.selected = this.welcomeMusic.active;
|
|
|
|
menubarItemVolumeHitsoundPlayer.onChange = event -> {
|
|
var volume:Float = event.value.toFloat() / 100.0;
|
|
hitsoundVolumePlayer = volume;
|
|
menubarLabelVolumeHitsoundPlayer.text = 'Player - ${Std.int(event.value)}%';
|
|
};
|
|
menubarItemVolumeHitsoundPlayer.value = Std.int(hitsoundVolumePlayer * 100);
|
|
|
|
menubarItemVolumeHitsoundOpponent.onChange = event -> {
|
|
var volume:Float = event.value.toFloat() / 100.0;
|
|
hitsoundVolumeOpponent = volume;
|
|
menubarLabelVolumeHitsoundOpponent.text = 'Enemy - ${Std.int(event.value)}%';
|
|
};
|
|
menubarItemVolumeHitsoundOpponent.value = Std.int(hitsoundVolumeOpponent * 100);
|
|
|
|
menubarItemVolumeInstrumental.onChange = event -> {
|
|
var volume:Float = event.value.toFloat() / 100.0;
|
|
if (audioInstTrack != null) audioInstTrack.volume = volume;
|
|
menubarLabelVolumeInstrumental.text = 'Instrumental - ${Std.int(event.value)}%';
|
|
};
|
|
|
|
menubarItemVolumeVocalsPlayer.onChange = event -> {
|
|
var volume:Float = event.value.toFloat() / 100.0;
|
|
audioVocalTrackGroup.playerVolume = volume;
|
|
menubarLabelVolumeVocalsPlayer.text = 'Player - ${Std.int(event.value)}%';
|
|
};
|
|
|
|
menubarItemVolumeVocalsOpponent.onChange = event -> {
|
|
var volume:Float = event.value.toFloat() / 100.0;
|
|
audioVocalTrackGroup.opponentVolume = volume;
|
|
menubarLabelVolumeVocalsOpponent.text = 'Enemy - ${Std.int(event.value)}%';
|
|
};
|
|
|
|
menubarItemPlaybackSpeed.onChange = event -> {
|
|
var pitch:Float = (event.value.toFloat() * 2.0) / 100.0;
|
|
pitch = Math.floor(pitch / 0.05) * 0.05; // Round to nearest 5%
|
|
pitch = Math.max(0.05, Math.min(2.0, pitch)); // Clamp to 5% to 200%
|
|
#if FLX_PITCH
|
|
if (audioInstTrack != null) audioInstTrack.pitch = pitch;
|
|
audioVocalTrackGroup.pitch = pitch;
|
|
#end
|
|
var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places.
|
|
menubarLabelPlaybackSpeed.text = 'Playback Speed - ${pitchDisplay}x';
|
|
}
|
|
|
|
menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
|
|
menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
|
|
menubarItemToggleToolboxOffsets.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT, event.value);
|
|
menubarItemToggleToolboxNoteData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTE_DATA_LAYOUT, event.value);
|
|
menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value);
|
|
menubarItemToggleToolboxFreeplay.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT, event.value);
|
|
menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value);
|
|
menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
|
|
menubarItemToggleToolboxOpponentPreview.onChange = 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'));
|
|
}
|
|
|
|
function setupContextMenu():Void
|
|
{
|
|
Screen.instance.registerEvent(MouseEvent.RIGHT_MOUSE_UP, function(e:MouseEvent) {
|
|
var xPos = e.screenX;
|
|
var yPos = e.screenY;
|
|
onContextMenu(xPos, yPos);
|
|
});
|
|
}
|
|
|
|
function onContextMenu(xPos:Float, yPos:Float)
|
|
{
|
|
trace('User right clicked to open menu at (${xPos}, ${yPos})');
|
|
// this.openDefaultContextMenu(xPos, yPos);
|
|
}
|
|
|
|
function copySelection():Void
|
|
{
|
|
// Doesn't use a command because it's not undoable.
|
|
|
|
// Calculate a single time offset for all the notes and events.
|
|
var timeOffset:Null<Int> = currentNoteSelection.length > 0 ? Std.int(currentNoteSelection[0].time) : null;
|
|
if (currentEventSelection.length > 0)
|
|
{
|
|
if (timeOffset == null || currentEventSelection[0].time < timeOffset)
|
|
{
|
|
timeOffset = Std.int(currentEventSelection[0].time);
|
|
}
|
|
}
|
|
|
|
SongDataUtils.writeItemsToClipboard(
|
|
{
|
|
notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset),
|
|
events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize TurboKeyHandlers and add them to the state (so `update()` is called)
|
|
* We can then probe `keyHandler.activated` to see if the key combo's action should be taken.
|
|
*/
|
|
function setupTurboKeyHandlers():Void
|
|
{
|
|
// Keyboard shortcuts
|
|
add(undoKeyHandler);
|
|
add(redoKeyHandler);
|
|
add(upKeyHandler);
|
|
add(downKeyHandler);
|
|
add(wKeyHandler);
|
|
add(sKeyHandler);
|
|
add(pageUpKeyHandler);
|
|
add(pageDownKeyHandler);
|
|
|
|
// Gamepad inputs
|
|
add(dpadUpGamepadHandler);
|
|
add(dpadDownGamepadHandler);
|
|
add(dpadLeftGamepadHandler);
|
|
add(dpadRightGamepadHandler);
|
|
add(leftStickUpGamepadHandler);
|
|
add(leftStickDownGamepadHandler);
|
|
add(leftStickLeftGamepadHandler);
|
|
add(leftStickRightGamepadHandler);
|
|
add(rightStickUpGamepadHandler);
|
|
add(rightStickDownGamepadHandler);
|
|
add(rightStickLeftGamepadHandler);
|
|
add(rightStickRightGamepadHandler);
|
|
}
|
|
|
|
/**
|
|
* Setup timers and listerners to handle auto-save.
|
|
*/
|
|
function setupAutoSave():Void
|
|
{
|
|
// Called when clicking the X button on the window.
|
|
WindowUtil.windowExit.add(onWindowClose);
|
|
|
|
// Called when the game crashes.
|
|
CrashHandler.errorSignal.add(onWindowCrash);
|
|
CrashHandler.criticalErrorSignal.add(onWindowCrash);
|
|
|
|
saveDataDirty = false;
|
|
}
|
|
|
|
var displayAutosavePopup:Bool = false;
|
|
|
|
/**
|
|
* UPDATE FUNCTIONS
|
|
*/
|
|
function autoSave(?beforePlaytest:Bool = false):Void
|
|
{
|
|
var needsAutoSave:Bool = saveDataDirty;
|
|
|
|
saveDataDirty = false;
|
|
|
|
// Auto-save preferences.
|
|
writePreferences(needsAutoSave);
|
|
|
|
// Auto-save the chart.
|
|
#if html5
|
|
// Auto-save to local storage.
|
|
// TODO: Implement this.
|
|
#else
|
|
// Auto-save to temp file.
|
|
if (needsAutoSave)
|
|
{
|
|
this.exportAllSongData(true, null);
|
|
if (beforePlaytest)
|
|
{
|
|
displayAutosavePopup = true;
|
|
}
|
|
else
|
|
{
|
|
displayAutosavePopup = false;
|
|
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
|
|
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [
|
|
{
|
|
text: "Take Me There",
|
|
callback: openBackupsFolder,
|
|
}
|
|
]);
|
|
}
|
|
}
|
|
#end
|
|
}
|
|
|
|
/**
|
|
* Open the backups folder in the file explorer.
|
|
* Don't call this on HTML5.
|
|
*/
|
|
function openBackupsFolder(?_):Bool
|
|
{
|
|
#if sys
|
|
// TODO: Is there a way to open a folder and highlight a file in it?
|
|
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
|
|
WindowUtil.openFolder(absoluteBackupsPath);
|
|
return true;
|
|
#else
|
|
trace('No file system access, cannot open backups folder.');
|
|
return false;
|
|
#end
|
|
}
|
|
|
|
/**
|
|
* Called when the window was closed, to save a backup of the chart.
|
|
* @param exitCode The exit code of the window. We use `-1` when calling the function due to a game crash.
|
|
*/
|
|
function onWindowClose(exitCode:Int):Void
|
|
{
|
|
trace('Window exited with exit code: $exitCode');
|
|
trace('Should save chart? $saveDataDirty');
|
|
|
|
var needsAutoSave:Bool = saveDataDirty;
|
|
|
|
writePreferences(needsAutoSave);
|
|
|
|
if (needsAutoSave)
|
|
{
|
|
this.exportAllSongData(true, null);
|
|
}
|
|
}
|
|
|
|
function onWindowCrash(message:String):Void
|
|
{
|
|
trace('Chart editor intercepted crash:');
|
|
trace('${message}');
|
|
|
|
trace('Should save chart? $saveDataDirty');
|
|
|
|
var needsAutoSave:Bool = saveDataDirty;
|
|
|
|
writePreferences(needsAutoSave);
|
|
|
|
if (needsAutoSave)
|
|
{
|
|
this.exportAllSongData(true, null);
|
|
}
|
|
}
|
|
|
|
function cleanupAutoSave():Void
|
|
{
|
|
WindowUtil.windowExit.remove(onWindowClose);
|
|
CrashHandler.errorSignal.remove(onWindowCrash);
|
|
CrashHandler.criticalErrorSignal.remove(onWindowCrash);
|
|
}
|
|
|
|
public override function update(elapsed:Float):Void
|
|
{
|
|
// Override F4 behavior to include the autosave.
|
|
if (FlxG.keys.justPressed.F4 && !criticalFailure)
|
|
{
|
|
quitChartEditor();
|
|
return;
|
|
}
|
|
|
|
// dispatchEvent gets called here.
|
|
super.update(elapsed);
|
|
|
|
if (criticalFailure) return;
|
|
|
|
// These ones happen even if the modal dialog is open.
|
|
handleMusicPlayback(elapsed);
|
|
handleNoteDisplay();
|
|
|
|
// These ones only happen if the modal dialog is not open.
|
|
handleScrollKeybinds();
|
|
handleSnap();
|
|
handleCursor();
|
|
|
|
handleMenubar();
|
|
handleToolboxes();
|
|
handlePlaybar();
|
|
handlePlayhead();
|
|
handleNotePreview();
|
|
handleHealthIcons();
|
|
|
|
handleFileKeybinds();
|
|
handleEditKeybinds();
|
|
handleViewKeybinds();
|
|
handleTestKeybinds();
|
|
handleHelpKeybinds();
|
|
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
handleQuickWatch();
|
|
#end
|
|
|
|
handlePostUpdate();
|
|
}
|
|
|
|
/**
|
|
* Beat hit while the song is playing.
|
|
*/
|
|
override function beatHit():Bool
|
|
{
|
|
// dispatchEvent gets called here.
|
|
if (!super.beatHit()) return false;
|
|
|
|
if (metronomeVolume > 0.0 && this.subState == null && (audioInstTrack != null && audioInstTrack.isPlaying))
|
|
{
|
|
playMetronomeTick(Conductor.instance.currentBeat % Conductor.instance.beatsPerMeasure == 0);
|
|
}
|
|
|
|
// Show the mouse cursor.
|
|
// Just throwing this somewhere convenient and infrequently called because sometimes Flixel's debug thing hides the cursor.
|
|
Cursor.show();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Step hit while the song is playing.
|
|
*/
|
|
override function stepHit():Bool
|
|
{
|
|
// dispatchEvent gets called here.
|
|
if (!super.stepHit()) return false;
|
|
|
|
if (audioInstTrack != null && audioInstTrack.isPlaying)
|
|
{
|
|
if (healthIconDad != null) healthIconDad.onStepHit(Conductor.instance.currentStep);
|
|
if (healthIconBF != null) healthIconBF.onStepHit(Conductor.instance.currentStep);
|
|
}
|
|
|
|
// Updating these every step keeps it more accurate.
|
|
// playerPreviewDirty = true;
|
|
// opponentPreviewDirty = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* UPDATE HANDLERS
|
|
*/
|
|
// ====================
|
|
|
|
/**
|
|
* Handle syncronizing the conductor with the music playback.
|
|
*/
|
|
function handleMusicPlayback(elapsed:Float):Void
|
|
{
|
|
if (audioInstTrack != null)
|
|
{
|
|
// This normally gets called by FlxG.sound.update()
|
|
// but we handle instrumental updates manually to prevent FlxG.sound.music.update()
|
|
// from being called twice when we move to the PlayState.
|
|
audioInstTrack.update(elapsed);
|
|
|
|
// If the song starts 50ms in, make sure we start the song there.
|
|
if (Conductor.instance.instrumentalOffset < 0)
|
|
{
|
|
if (audioInstTrack.time < -Conductor.instance.instrumentalOffset)
|
|
{
|
|
trace('Resetting instrumental time to ${- Conductor.instance.instrumentalOffset}ms');
|
|
audioInstTrack.time = -Conductor.instance.instrumentalOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (audioInstTrack != null && audioInstTrack.isPlaying)
|
|
{
|
|
if (FlxG.keys.pressed.ALT)
|
|
{
|
|
// If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat!
|
|
|
|
var oldStepTime:Float = Conductor.instance.currentStepTime;
|
|
var oldSongPosition:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
|
|
Conductor.instance.update(audioInstTrack.time);
|
|
handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
|
|
// Resync vocals.
|
|
if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
|
|
{
|
|
audioVocalTrackGroup.time = audioInstTrack.time;
|
|
}
|
|
var diffStepTime:Float = Conductor.instance.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.instance.songPosition + Conductor.instance.instrumentalOffset;
|
|
Conductor.instance.update(audioInstTrack.time);
|
|
handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
|
|
// Resync vocals.
|
|
if (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.instance.currentStepTime + Conductor.instance.instrumentalOffsetSteps) * 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 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.noteData == currentPlaceNoteData)
|
|
{
|
|
// This hold note is for the note we are currently dragging.
|
|
// It will be displayed by gridGhostHoldNoteSprite instead.
|
|
holdNoteSprite.kill();
|
|
}
|
|
else if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
|
|
{
|
|
// This hold note is off-screen.
|
|
// Kill the hold note sprite and recycle it.
|
|
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 height and position.
|
|
// var holdNoteHeight = holdNoteSprite.noteData.getStepLength() * GRID_SIZE;
|
|
// holdNoteSprite.setHeightDirectly(holdNoteHeight);
|
|
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 - PLAYBAR_HEIGHT, MENU_BAR_HEIGHT)
|
|
&& 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);
|
|
// Update the sprite's graphic. TODO: Is this inefficient?
|
|
eventSprite.playAnimation(eventSprite.eventData.eventKind);
|
|
}
|
|
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.instance.measureLengthMs * 2); // Is 2 measures enough?
|
|
var viewAreaBottomMs:Float = scrollPositionInMs + (Conductor.instance.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
|
|
&& noteSprite.noteData != currentPlaceNoteData)
|
|
{
|
|
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);
|
|
|
|
trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
eventSprite.updateTooltipPosition();
|
|
}
|
|
|
|
// 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 note the one we are dragging? If so, ghostHoldNoteSprite will handle it.
|
|
if (noteData == currentPlaceNoteData) 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;
|
|
|
|
var stepLength = noteSprite.noteData.getStepLength();
|
|
selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * 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;
|
|
}
|
|
|
|
// Additional cleanup on notes.
|
|
if (noteTooltipsDirty) eventSprite.updateTooltipText();
|
|
}
|
|
|
|
noteTooltipsDirty = false;
|
|
|
|
// 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 user is interacting with the UI, unless a playbar button (the << >> ones) is pressed.
|
|
if ((isHaxeUIFocused || isCursorOverHaxeUI) && playbarButtonPressed == null) return;
|
|
|
|
var scrollAmount:Float = 0; // Amount to scroll the grid.
|
|
var playheadAmount:Float = 0; // Amount to scroll the playhead relative to the grid.
|
|
var shouldPause:Bool = false; // Whether to pause the song when scrolling.
|
|
var shouldEase:Bool = false; // Whether to ease the scroll.
|
|
|
|
// Handle scroll anchor
|
|
if (scrollAnchorScreenPos != null)
|
|
{
|
|
var currentScreenPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
|
var distance = currentScreenPos - scrollAnchorScreenPos;
|
|
|
|
var verticalDistance = distance.y;
|
|
|
|
// How much scrolling should be done based on the distance of the cursor from the anchor.
|
|
final ANCHOR_SCROLL_SPEED = 0.2;
|
|
|
|
scrollAmount = ANCHOR_SCROLL_SPEED * verticalDistance;
|
|
shouldPause = true;
|
|
}
|
|
|
|
// Mouse Wheel = Scroll
|
|
if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL)
|
|
{
|
|
scrollAmount = -50 * FlxG.mouse.wheel;
|
|
shouldPause = true;
|
|
}
|
|
|
|
// Up Arrow = Scroll Up
|
|
if (upKeyHandler.activated && currentLiveInputStyle == None)
|
|
{
|
|
scrollAmount = -GRID_SIZE * 4;
|
|
shouldPause = true;
|
|
}
|
|
// Down Arrow = Scroll Down
|
|
if (downKeyHandler.activated && currentLiveInputStyle == None)
|
|
{
|
|
scrollAmount = GRID_SIZE * 4;
|
|
shouldPause = true;
|
|
}
|
|
|
|
// W = Scroll Up (doesn't work with Ctrl+Scroll)
|
|
if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
|
|
{
|
|
scrollAmount = -GRID_SIZE * 4;
|
|
shouldPause = true;
|
|
}
|
|
// S = Scroll Down (doesn't work with Ctrl+Scroll)
|
|
if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
|
|
{
|
|
scrollAmount = GRID_SIZE * 4;
|
|
shouldPause = true;
|
|
}
|
|
|
|
// GAMEPAD LEFT STICK UP = Scroll Up by 1 note snap
|
|
if (leftStickUpGamepadHandler.activated)
|
|
{
|
|
scrollAmount = -GRID_SIZE * noteSnapRatio;
|
|
shouldPause = true;
|
|
}
|
|
// GAMEPAD LEFT STICK DOWN = Scroll Down by 1 note snap
|
|
if (leftStickDownGamepadHandler.activated)
|
|
{
|
|
scrollAmount = GRID_SIZE * noteSnapRatio;
|
|
shouldPause = true;
|
|
}
|
|
|
|
// GAMEPAD RIGHT STICK UP = Scroll Up by 1 note snap (playhead only)
|
|
if (rightStickUpGamepadHandler.activated)
|
|
{
|
|
playheadAmount = -GRID_SIZE * noteSnapRatio;
|
|
shouldPause = true;
|
|
}
|
|
// GAMEPAD RIGHT STICK DOWN = Scroll Down by 1 note snap (playhead only)
|
|
if (rightStickDownGamepadHandler.activated)
|
|
{
|
|
playheadAmount = GRID_SIZE * noteSnapRatio;
|
|
shouldPause = true;
|
|
}
|
|
|
|
var funcJumpUp = (playheadOnly:Bool) -> {
|
|
var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
|
|
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
|
|
var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight;
|
|
// If we would move less than one grid, instead move to the top of the previous measure.
|
|
var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos);
|
|
if (targetScrollAmount < GRID_SIZE)
|
|
{
|
|
targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure;
|
|
}
|
|
|
|
if (playheadOnly)
|
|
{
|
|
playheadAmount = targetScrollPosition - playheadPos;
|
|
}
|
|
else
|
|
{
|
|
scrollAmount = targetScrollPosition - playheadPos;
|
|
}
|
|
}
|
|
|
|
// PAGE UP = Jump up to nearest measure
|
|
// GAMEPAD LEFT STICK LEFT = Jump up to nearest measure
|
|
if (pageUpKeyHandler.activated || leftStickLeftGamepadHandler.activated)
|
|
{
|
|
funcJumpUp(false);
|
|
shouldPause = true;
|
|
}
|
|
if (rightStickLeftGamepadHandler.activated)
|
|
{
|
|
funcJumpUp(true);
|
|
shouldPause = true;
|
|
}
|
|
if (playbarButtonPressed == 'playbarBack')
|
|
{
|
|
playbarButtonPressed = '';
|
|
funcJumpUp(false);
|
|
shouldPause = true;
|
|
}
|
|
|
|
var funcJumpDown = (playheadOnly:Bool) -> {
|
|
var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure;
|
|
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
|
|
var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight;
|
|
// If we would move less than one grid, instead move to the top of the next measure.
|
|
var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos);
|
|
if (targetScrollAmount < GRID_SIZE)
|
|
{
|
|
targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure;
|
|
}
|
|
|
|
if (playheadOnly)
|
|
{
|
|
playheadAmount = targetScrollPosition - playheadPos;
|
|
}
|
|
else
|
|
{
|
|
scrollAmount = targetScrollPosition - playheadPos;
|
|
}
|
|
}
|
|
|
|
// PAGE DOWN = Jump down to nearest measure
|
|
// GAMEPAD LEFT STICK RIGHT = Jump down to nearest measure
|
|
if (pageDownKeyHandler.activated || leftStickRightGamepadHandler.activated)
|
|
{
|
|
funcJumpDown(false);
|
|
shouldPause = true;
|
|
}
|
|
if (rightStickRightGamepadHandler.activated)
|
|
{
|
|
funcJumpDown(true);
|
|
shouldPause = true;
|
|
}
|
|
if (playbarButtonPressed == 'playbarForward')
|
|
{
|
|
playbarButtonPressed = '';
|
|
funcJumpDown(false);
|
|
shouldPause = true;
|
|
}
|
|
|
|
// SHIFT + Scroll = Scroll Fast
|
|
// GAMEPAD LEFT STICK CLICK + Scroll = Scroll Fast
|
|
if (FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_STICK_CLICK ?? false))
|
|
{
|
|
scrollAmount *= 2;
|
|
}
|
|
// CONTROL + Scroll = Scroll Precise
|
|
if (FlxG.keys.pressed.CONTROL)
|
|
{
|
|
scrollAmount /= 4;
|
|
}
|
|
|
|
// Alt + Drag = Scroll but move the playhead the same amount.
|
|
if (FlxG.keys.pressed.ALT)
|
|
{
|
|
playheadAmount = scrollAmount;
|
|
scrollAmount = 0;
|
|
shouldPause = false;
|
|
}
|
|
|
|
// HOME = Scroll to Top
|
|
if (FlxG.keys.justPressed.HOME)
|
|
{
|
|
// Scroll amount is the difference between the current position and the top.
|
|
scrollAmount = 0 - this.scrollPositionInPixels;
|
|
playheadAmount = 0 - this.playheadPositionInPixels;
|
|
shouldPause = true;
|
|
}
|
|
if (playbarButtonPressed == 'playbarStart')
|
|
{
|
|
playbarButtonPressed = '';
|
|
scrollAmount = 0 - this.scrollPositionInPixels;
|
|
playheadAmount = 0 - this.playheadPositionInPixels;
|
|
shouldPause = true;
|
|
}
|
|
|
|
// END = Scroll to Bottom
|
|
if (FlxG.keys.justPressed.END)
|
|
{
|
|
// Scroll amount is the difference between the current position and the bottom.
|
|
scrollAmount = this.songLengthInPixels - this.scrollPositionInPixels;
|
|
shouldPause = true;
|
|
}
|
|
if (playbarButtonPressed == 'playbarEnd')
|
|
{
|
|
playbarButtonPressed = '';
|
|
scrollAmount = this.songLengthInPixels - this.scrollPositionInPixels;
|
|
shouldPause = true;
|
|
}
|
|
|
|
if (Math.abs(scrollAmount) > GRID_SIZE * 8)
|
|
{
|
|
shouldEase = true;
|
|
}
|
|
|
|
// Resync the conductor and audio tracks.
|
|
if (scrollAmount != 0 || playheadAmount != 0)
|
|
{
|
|
this.playheadPositionInPixels += playheadAmount;
|
|
if (shouldEase)
|
|
{
|
|
easeSongToScrollPosition(this.scrollPositionInPixels + scrollAmount);
|
|
}
|
|
else
|
|
{
|
|
// Apply the scroll amount.
|
|
this.scrollPositionInPixels += scrollAmount;
|
|
moveSongToScrollPosition();
|
|
}
|
|
}
|
|
if (shouldPause) stopAudioPlayback();
|
|
}
|
|
|
|
/**
|
|
* Handle changing the note snapping level.
|
|
*/
|
|
function handleSnap():Void
|
|
{
|
|
if (currentLiveInputStyle == None)
|
|
{
|
|
if (FlxG.keys.justPressed.LEFT && !FlxG.keys.pressed.CONTROL)
|
|
{
|
|
noteSnapQuantIndex--;
|
|
if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
|
|
}
|
|
|
|
if (FlxG.keys.justPressed.RIGHT && !FlxG.keys.pressed.CONTROL)
|
|
{
|
|
noteSnapQuantIndex++;
|
|
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle display of the mouse cursor.
|
|
*/
|
|
function handleCursor():Void
|
|
{
|
|
// Mouse sounds
|
|
if (FlxG.mouse.justPressed) FunkinSound.playOnce(Paths.sound("chartingSounds/ClickDown"));
|
|
if (FlxG.mouse.justReleased) FunkinSound.playOnce(Paths.sound("chartingSounds/ClickUp"));
|
|
|
|
// Note: If a menu is open in HaxeUI, don't handle cursor behavior.
|
|
var shouldHandleCursor:Bool = !(isHaxeUIFocused || playbarHeadDragging || isHaxeUIDialogOpen)
|
|
|| (selectionBoxStartPos != null)
|
|
|| (dragTargetNote != null || dragTargetEvent != null);
|
|
|
|
var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1;
|
|
|
|
// trace('shouldHandleCursor: $shouldHandleCursor');
|
|
|
|
// TODO: TBH some of this should be using FlxMouseEventManager...
|
|
|
|
if (shouldHandleCursor)
|
|
{
|
|
// Over the course of this big conditional block,
|
|
// we determine what the cursor should look like,
|
|
// and fall back to the default cursor if none of the conditions are met.
|
|
var targetCursorMode:Null<CursorMode> = null;
|
|
|
|
if (gridTiledSprite == null) throw "ERROR: Tried to handle cursor, but gridTiledSprite is null! Check ChartEditorState.buildGrid()";
|
|
|
|
var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite);
|
|
|
|
var overlapsRenderedNotes:Bool = FlxG.mouse.overlaps(renderedNotes);
|
|
var overlapsRenderedHoldNotes:Bool = FlxG.mouse.overlaps(renderedHoldNotes);
|
|
var overlapsRenderedEvents:Bool = FlxG.mouse.overlaps(renderedEvents);
|
|
|
|
// Cursor position relative to the grid.
|
|
var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x;
|
|
var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y;
|
|
|
|
var overlapsSelectionBorder:Bool = overlapsGrid
|
|
&& ((cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2)
|
|
|| (cursorX % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2))
|
|
|| (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)));
|
|
|
|
var overlapsSelection:Bool = FlxG.mouse.overlaps(renderedSelectionSquares);
|
|
|
|
var overlapsHealthIcons:Bool = FlxG.mouse.overlaps(healthIconBF) || FlxG.mouse.overlaps(healthIconDad);
|
|
|
|
if (FlxG.mouse.justPressedMiddle)
|
|
{
|
|
if (scrollAnchorScreenPos == null)
|
|
{
|
|
scrollAnchorScreenPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
|
selectionBoxStartPos = null;
|
|
}
|
|
else
|
|
{
|
|
scrollAnchorScreenPos = null;
|
|
}
|
|
}
|
|
|
|
if (FlxG.mouse.justPressed)
|
|
{
|
|
if (scrollAnchorScreenPos != null)
|
|
{
|
|
scrollAnchorScreenPos = null;
|
|
}
|
|
else if (measureTicks != null && FlxG.mouse.overlaps(measureTicks) && !isCursorOverHaxeUI)
|
|
{
|
|
gridPlayheadScrollAreaPressed = true;
|
|
}
|
|
else if (notePreview != null && FlxG.mouse.overlaps(notePreview) && !isCursorOverHaxeUI)
|
|
{
|
|
// Clicked note preview
|
|
notePreviewScrollAreaStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
|
}
|
|
else if (!isCursorOverHaxeUI && (!overlapsGrid || overlapsSelectionBorder))
|
|
{
|
|
selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
|
// Drawing selection box.
|
|
targetCursorMode = Crosshair;
|
|
}
|
|
else if (overlapsSelection)
|
|
{
|
|
// Do nothing
|
|
trace('Clicked on a selected note!');
|
|
}
|
|
}
|
|
|
|
if (gridPlayheadScrollAreaPressed && FlxG.mouse.released)
|
|
{
|
|
gridPlayheadScrollAreaPressed = false;
|
|
}
|
|
|
|
if (notePreviewScrollAreaStartPos != null && FlxG.mouse.released)
|
|
{
|
|
notePreviewScrollAreaStartPos = null;
|
|
}
|
|
|
|
if (gridPlayheadScrollAreaPressed)
|
|
{
|
|
// Clicked on the playhead scroll area.
|
|
// Move the playhead to the cursor position.
|
|
this.playheadPositionInPixels = FlxG.mouse.screenY - (GRID_INITIAL_Y_POS);
|
|
moveSongToScrollPosition();
|
|
|
|
// Cursor should be a grabby hand.
|
|
if (targetCursorMode == null) targetCursorMode = Grabbing;
|
|
}
|
|
|
|
// The song position of the cursor, in steps.
|
|
var cursorFractionalStep:Float = cursorY / GRID_SIZE;
|
|
var cursorMs:Float = Conductor.instance.getStepTimeInMs(cursorFractionalStep);
|
|
// Round the cursor step to the nearest snap quant.
|
|
var cursorSnappedStep:Float = Math.floor(cursorFractionalStep / noteSnapRatio) * noteSnapRatio;
|
|
var cursorSnappedMs:Float = Conductor.instance.getStepTimeInMs(cursorSnappedStep);
|
|
|
|
// The direction value for the column at the cursor.
|
|
var cursorGridPos:Int = Math.floor(cursorX / GRID_SIZE);
|
|
var cursorColumn:Int = gridColumnToNoteData(cursorGridPos);
|
|
|
|
if (selectionBoxStartPos != null)
|
|
{
|
|
var cursorXStart:Float = selectionBoxStartPos.x - gridTiledSprite.x;
|
|
var cursorYStart:Float = selectionBoxStartPos.y - gridTiledSprite.y;
|
|
|
|
var hasDraggedMouse:Bool = Math.abs(cursorX - cursorXStart) > DRAG_THRESHOLD || Math.abs(cursorY - cursorYStart) > DRAG_THRESHOLD;
|
|
|
|
// Determine if we dragged the mouse at all.
|
|
if (hasDraggedMouse)
|
|
{
|
|
// Handle releasing the selection box.
|
|
if (FlxG.mouse.justReleased)
|
|
{
|
|
// We released the mouse. Select the notes in the box.
|
|
var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE;
|
|
var cursorStepStart:Int = Math.floor(cursorFractionalStepStart);
|
|
var cursorMsStart:Float = Conductor.instance.getStepTimeInMs(cursorStepStart);
|
|
var cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE);
|
|
var cursorColumnBaseStart:Int = Math.floor(cursorXStart / GRID_SIZE);
|
|
|
|
// Since this selects based on noteData directly,
|
|
// we don't need to specifically exclude sustain pieces.
|
|
|
|
// This logic is gross because the columns go 4567-0123-8.
|
|
// We build a list of columns to select.
|
|
var columnStart:Int = Std.int(Math.min(cursorColumnBase, cursorColumnBaseStart));
|
|
var columnEnd:Int = Std.int(Math.max(cursorColumnBase, cursorColumnBaseStart));
|
|
var columns:Array<Int> = [for (i in columnStart...(columnEnd + 1)) i].map(function(i:Int):Int {
|
|
if (i >= eventColumn)
|
|
{
|
|
// Don't invert the event column.
|
|
return eventColumn;
|
|
}
|
|
else if (i >= STRUMLINE_SIZE)
|
|
{
|
|
// Invert the player columns.
|
|
return i - STRUMLINE_SIZE;
|
|
}
|
|
else if (i >= 0)
|
|
{
|
|
// Invert the opponent columns.
|
|
return i + STRUMLINE_SIZE;
|
|
}
|
|
else
|
|
{
|
|
// Minimum of 0.
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
if (columns.length > 0)
|
|
{
|
|
var notesToSelect:Array<SongNoteData> = currentSongChartNoteData;
|
|
notesToSelect = SongDataUtils.getNotesInTimeRange(notesToSelect, Math.min(cursorMsStart, cursorMs), Math.max(cursorMsStart, cursorMs));
|
|
notesToSelect = SongDataUtils.getNotesWithData(notesToSelect, columns);
|
|
|
|
var eventsToSelect:Array<SongEventData> = [];
|
|
|
|
if (columns.indexOf(eventColumn) != -1)
|
|
{
|
|
// The drag selection included the event column.
|
|
eventsToSelect = currentSongChartEventData;
|
|
eventsToSelect = SongDataUtils.getEventsInTimeRange(eventsToSelect, Math.min(cursorMsStart, cursorMs), Math.max(cursorMsStart, cursorMs));
|
|
}
|
|
|
|
if (notesToSelect.length > 0 || eventsToSelect.length > 0)
|
|
{
|
|
if (FlxG.keys.pressed.CONTROL)
|
|
{
|
|
// Add to the selection.
|
|
performCommand(new SelectItemsCommand(notesToSelect, eventsToSelect));
|
|
}
|
|
else
|
|
{
|
|
// Set the selection.
|
|
performCommand(new SetItemSelectionCommand(notesToSelect, eventsToSelect));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We made a selection box, but it didn't select anything.
|
|
|
|
if (!FlxG.keys.pressed.CONTROL)
|
|
{
|
|
// Deselect all items.
|
|
var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0);
|
|
if (shouldDeselect)
|
|
{
|
|
performCommand(new DeselectAllItemsCommand());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We made a selection box, but it didn't select any columns.
|
|
}
|
|
|
|
// Clear the selection box.
|
|
selectionBoxStartPos = null;
|
|
setSelectionBoxBounds();
|
|
}
|
|
else
|
|
{
|
|
// Clicking and dragging.
|
|
|
|
// Scroll the screen if the mouse is above or below the grid.
|
|
if (FlxG.mouse.screenY < MENU_BAR_HEIGHT)
|
|
{
|
|
// Scroll up.
|
|
var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY;
|
|
scrollPositionInPixels -= diff * 0.5; // Too fast!
|
|
moveSongToScrollPosition();
|
|
}
|
|
else if (FlxG.mouse.screenY > (playbarHeadLayout?.y ?? 0.0))
|
|
{
|
|
// Scroll down.
|
|
var diff:Float = FlxG.mouse.screenY - (playbarHeadLayout?.y ?? 0.0);
|
|
scrollPositionInPixels += diff * 0.5; // Too fast!
|
|
moveSongToScrollPosition();
|
|
}
|
|
|
|
// Render the selection box.
|
|
var selectionRect:FlxRect = new FlxRect();
|
|
selectionRect.x = Math.min(FlxG.mouse.screenX, selectionBoxStartPos.x);
|
|
selectionRect.y = Math.min(FlxG.mouse.screenY, selectionBoxStartPos.y);
|
|
selectionRect.width = Math.abs(FlxG.mouse.screenX - selectionBoxStartPos.x);
|
|
selectionRect.height = Math.abs(FlxG.mouse.screenY - selectionBoxStartPos.y);
|
|
setSelectionBoxBounds(selectionRect);
|
|
|
|
targetCursorMode = Crosshair;
|
|
}
|
|
}
|
|
else if (FlxG.mouse.justReleased)
|
|
{
|
|
// Clear the selection box.
|
|
selectionBoxStartPos = null;
|
|
setSelectionBoxBounds();
|
|
|
|
if (overlapsGrid)
|
|
{
|
|
// We clicked on the grid without moving the mouse.
|
|
|
|
// Find the first note that is at the cursor position.
|
|
var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
|
|
// If note.alive is false, the note is dead and awaiting recycling.
|
|
return note.alive && FlxG.mouse.overlaps(note);
|
|
});
|
|
var highlightedEvent:Null<ChartEditorEventSprite> = null;
|
|
if (highlightedNote == null)
|
|
{
|
|
highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool {
|
|
return event.alive && FlxG.mouse.overlaps(event);
|
|
});
|
|
}
|
|
var highlightedHoldNote:Null<ChartEditorHoldNoteSprite> = null;
|
|
if (highlightedNote == null && highlightedEvent == null)
|
|
{
|
|
highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool {
|
|
return holdNote.alive && FlxG.mouse.overlaps(holdNote);
|
|
});
|
|
}
|
|
|
|
if (FlxG.keys.pressed.CONTROL)
|
|
{
|
|
if (highlightedNote != null && highlightedNote.noteData != null)
|
|
{
|
|
// Control click to select/deselect an individual note.
|
|
if (isNoteSelected(highlightedNote.noteData))
|
|
{
|
|
performCommand(new DeselectItemsCommand([highlightedNote.noteData], []));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SelectItemsCommand([highlightedNote.noteData], []));
|
|
}
|
|
}
|
|
else if (highlightedEvent != null && highlightedEvent.eventData != null)
|
|
{
|
|
// Control click to select/deselect an individual note.
|
|
if (isEventSelected(highlightedEvent.eventData))
|
|
{
|
|
performCommand(new DeselectItemsCommand([], [highlightedEvent.eventData]));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SelectItemsCommand([], [highlightedEvent.eventData]));
|
|
}
|
|
}
|
|
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
|
|
{
|
|
// Control click to select/deselect an individual note.
|
|
if (isNoteSelected(highlightedNote.noteData))
|
|
{
|
|
performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], []));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SelectItemsCommand([highlightedHoldNote.noteData], []));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Do nothing if you control-clicked on an empty space.
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (highlightedNote != null && highlightedNote.noteData != null)
|
|
{
|
|
// Click a note to select it.
|
|
performCommand(new SetItemSelectionCommand([highlightedNote.noteData], []));
|
|
}
|
|
else if (highlightedEvent != null && highlightedEvent.eventData != null)
|
|
{
|
|
// Click an event to select it.
|
|
performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData]));
|
|
}
|
|
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
|
|
{
|
|
// Click a hold note to select it.
|
|
performCommand(new SetItemSelectionCommand([highlightedHoldNote.noteData], []));
|
|
}
|
|
else
|
|
{
|
|
// Click on an empty space to deselect everything.
|
|
var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0);
|
|
if (shouldDeselect)
|
|
{
|
|
performCommand(new DeselectAllItemsCommand());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we clicked and released outside the grid.
|
|
|
|
if (!FlxG.keys.pressed.CONTROL)
|
|
{
|
|
// Deselect all items.
|
|
var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0);
|
|
if (shouldDeselect)
|
|
{
|
|
performCommand(new DeselectAllItemsCommand());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (notePreviewScrollAreaStartPos != null)
|
|
{
|
|
// Player is clicking and holding on note preview to scrub around.
|
|
targetCursorMode = Grabbing;
|
|
|
|
var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.screenY, (notePreview?.y ?? 0.0),
|
|
(notePreview?.y ?? 0.0) + (notePreview?.height ?? 0.0), 0, songLengthInPixels);
|
|
|
|
scrollPositionInPixels = clickedPosInPixels;
|
|
moveSongToScrollPosition();
|
|
}
|
|
else if (scrollAnchorScreenPos != null)
|
|
{
|
|
// Cursor should be a scroll anchor.
|
|
targetCursorMode = Scroll;
|
|
}
|
|
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.instance.getStepTimeInMs(dragTargetNote.noteData.getStepTime() + dragDistanceSteps) - dragTargetNote.noteData.time;
|
|
}
|
|
else if (dragTargetEvent != null && dragTargetEvent.eventData != null)
|
|
{
|
|
dragDistanceMs = Conductor.instance.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.
|
|
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.
|
|
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.instance.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.
|
|
|
|
var stepTime:Float = inline currentPlaceNoteData.getStepTime();
|
|
var dragLengthSteps:Float = Conductor.instance.getTimeInSteps(cursorSnappedMs) - stepTime;
|
|
var dragLengthMs:Float = dragLengthSteps * Conductor.instance.stepLengthMs;
|
|
var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE;
|
|
|
|
if (gridGhostHoldNote != null)
|
|
{
|
|
if (dragLengthSteps > 0)
|
|
{
|
|
if (dragLengthCurrent != dragLengthSteps)
|
|
{
|
|
stretchySounds = !stretchySounds;
|
|
this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
|
|
|
|
dragLengthCurrent = dragLengthSteps;
|
|
}
|
|
|
|
gridGhostHoldNote.visible = true;
|
|
gridGhostHoldNote.noteData = currentPlaceNoteData;
|
|
gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
|
|
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
|
|
|
|
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
|
|
}
|
|
else
|
|
{
|
|
gridGhostHoldNote.visible = false;
|
|
gridGhostHoldNote.setHeightDirectly(0);
|
|
}
|
|
}
|
|
|
|
if (FlxG.mouse.justReleased)
|
|
{
|
|
if (dragLengthSteps > 0)
|
|
{
|
|
this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
|
|
// Apply the new length.
|
|
performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs));
|
|
}
|
|
else
|
|
{
|
|
// Apply the new (zero) length if we are changing the length.
|
|
if (currentPlaceNoteData.length > 0)
|
|
{
|
|
this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
|
|
performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, 0));
|
|
}
|
|
}
|
|
|
|
// Finished dragging. Release the note.
|
|
currentPlaceNoteData = null;
|
|
}
|
|
else
|
|
{
|
|
// Cursor should be a grabby hand.
|
|
if (targetCursorMode == null) targetCursorMode = Grabbing;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (FlxG.mouse.justPressed)
|
|
{
|
|
// Just clicked to place a note.
|
|
if (!isCursorOverHaxeUI && overlapsGrid && !overlapsSelectionBorder)
|
|
{
|
|
// We clicked on the grid without moving the mouse.
|
|
|
|
// Find the first note that is at the cursor position.
|
|
var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
|
|
// If note.alive is false, the note is dead and awaiting recycling.
|
|
return note.alive && FlxG.mouse.overlaps(note);
|
|
});
|
|
var highlightedEvent:Null<ChartEditorEventSprite> = null;
|
|
if (highlightedNote == null)
|
|
{
|
|
highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool {
|
|
// If event.alive is false, the event is dead and awaiting recycling.
|
|
return event.alive && FlxG.mouse.overlaps(event);
|
|
});
|
|
}
|
|
var highlightedHoldNote:Null<ChartEditorHoldNoteSprite> = null;
|
|
if (highlightedNote == null && highlightedEvent == null)
|
|
{
|
|
highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool {
|
|
// If holdNote.alive is false, the holdNote is dead and awaiting recycling.
|
|
return holdNote.alive && FlxG.mouse.overlaps(holdNote);
|
|
});
|
|
}
|
|
|
|
if (FlxG.keys.pressed.CONTROL)
|
|
{
|
|
// Control click to select/deselect an individual note.
|
|
if (highlightedNote != null && highlightedNote.noteData != null)
|
|
{
|
|
if (isNoteSelected(highlightedNote.noteData))
|
|
{
|
|
performCommand(new DeselectItemsCommand([highlightedNote.noteData], []));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SelectItemsCommand([highlightedNote.noteData], []));
|
|
}
|
|
}
|
|
else if (highlightedEvent != null && highlightedEvent.eventData != null)
|
|
{
|
|
if (isEventSelected(highlightedEvent.eventData))
|
|
{
|
|
performCommand(new DeselectItemsCommand([], [highlightedEvent.eventData]));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SelectItemsCommand([], [highlightedEvent.eventData]));
|
|
}
|
|
}
|
|
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
|
|
{
|
|
if (isNoteSelected(highlightedNote.noteData))
|
|
{
|
|
performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], []));
|
|
}
|
|
else
|
|
{
|
|
performCommand(new SelectItemsCommand([highlightedHoldNote.noteData], []));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Do nothing when control clicking nothing.
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (highlightedNote != null && highlightedNote.noteData != null)
|
|
{
|
|
if (isNoteSelected(highlightedNote.noteData))
|
|
{
|
|
// Clicked a selected event, start dragging.
|
|
dragTargetNote = highlightedNote;
|
|
}
|
|
else
|
|
{
|
|
// If you click an unselected note, and aren't holding Control, deselect everything else.
|
|
performCommand(new SetItemSelectionCommand([highlightedNote.noteData], []));
|
|
}
|
|
}
|
|
else if (highlightedEvent != null && highlightedEvent.eventData != null)
|
|
{
|
|
if (isEventSelected(highlightedEvent.eventData))
|
|
{
|
|
// Clicked a selected event, start dragging.
|
|
dragTargetEvent = highlightedEvent;
|
|
}
|
|
else
|
|
{
|
|
// If you click an unselected event, and aren't holding Control, deselect everything else.
|
|
performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData]));
|
|
}
|
|
}
|
|
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
|
|
{
|
|
// Clicked a hold note, start dragging TO EXTEND NOTE LENGTH.
|
|
currentPlaceNoteData = highlightedHoldNote.noteData;
|
|
}
|
|
else
|
|
{
|
|
// Click a blank space to place a note and select it.
|
|
|
|
if (cursorGridPos == eventColumn)
|
|
{
|
|
// Create an event and place it in the chart.
|
|
// TODO: Figure out configuring event data.
|
|
var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.copy());
|
|
|
|
performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
|
|
}
|
|
else
|
|
{
|
|
// Create a note and place it in the chart.
|
|
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace);
|
|
|
|
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
|
|
|
|
currentPlaceNoteData = newNoteData;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we clicked and released outside the grid (or on HaxeUI), do nothing.
|
|
}
|
|
}
|
|
|
|
var rightMouseUpdated:Bool = (FlxG.mouse.justPressedRight)
|
|
|| (FlxG.mouse.pressedRight && (FlxG.mouse.deltaX > 0 || FlxG.mouse.deltaY > 0));
|
|
if (rightMouseUpdated && overlapsGrid)
|
|
{
|
|
// We right clicked on the grid.
|
|
|
|
// Find the first note that is at the cursor position.
|
|
var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool {
|
|
// If note.alive is false, the note is dead and awaiting recycling.
|
|
return note.alive && FlxG.mouse.overlaps(note);
|
|
});
|
|
var highlightedEvent:Null<ChartEditorEventSprite> = null;
|
|
if (highlightedNote == null)
|
|
{
|
|
highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool {
|
|
// If event.alive is false, the event is dead and awaiting recycling.
|
|
return event.alive && FlxG.mouse.overlaps(event);
|
|
});
|
|
}
|
|
var highlightedHoldNote:Null<ChartEditorHoldNoteSprite> = null;
|
|
if (highlightedNote == null && highlightedEvent == null)
|
|
{
|
|
highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool {
|
|
// If holdNote.alive is false, the holdNote is dead and awaiting recycling.
|
|
return holdNote.alive && FlxG.mouse.overlaps(holdNote);
|
|
});
|
|
}
|
|
|
|
if (highlightedNote != null && highlightedNote.noteData != null)
|
|
{
|
|
// TODO: Handle the case of clicking on a sustain piece.
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
// Shift + Right click opens the context menu.
|
|
// If we are clicking a large selection, open the Selection context menu, otherwise open the Note context menu.
|
|
var isHighlightedNoteSelected:Bool = isNoteSelected(highlightedNote.noteData);
|
|
var useSingleNoteContextMenu:Bool = (!isHighlightedNoteSelected)
|
|
|| (isHighlightedNoteSelected && currentNoteSelection.length == 1);
|
|
// Show the context menu connected to the note.
|
|
if (useSingleNoteContextMenu)
|
|
{
|
|
this.openNoteContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedNote.noteData);
|
|
}
|
|
else
|
|
{
|
|
this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Right click removes the note.
|
|
performCommand(new RemoveNotesCommand([highlightedNote.noteData]));
|
|
}
|
|
}
|
|
else if (highlightedEvent != null && highlightedEvent.eventData != null)
|
|
{
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
// Shift + Right click opens the context menu.
|
|
// If we are clicking a large selection, open the Selection context menu, otherwise open the Event context menu.
|
|
var isHighlightedEventSelected:Bool = isEventSelected(highlightedEvent.eventData);
|
|
var useSingleEventContextMenu:Bool = (!isHighlightedEventSelected)
|
|
|| (isHighlightedEventSelected && currentEventSelection.length == 1);
|
|
if (useSingleEventContextMenu)
|
|
{
|
|
this.openEventContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedEvent.eventData);
|
|
}
|
|
else
|
|
{
|
|
this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Right click removes the event.
|
|
performCommand(new RemoveEventsCommand([highlightedEvent.eventData]));
|
|
}
|
|
}
|
|
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
|
|
{
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
// Shift + Right click opens the context menu.
|
|
// If we are clicking a large selection, open the Selection context menu, otherwise open the Note context menu.
|
|
var isHighlightedNoteSelected:Bool = isNoteSelected(highlightedHoldNote.noteData);
|
|
var useSingleNoteContextMenu:Bool = (!isHighlightedNoteSelected)
|
|
|| (isHighlightedNoteSelected && currentNoteSelection.length == 1);
|
|
// Show the context menu connected to the note.
|
|
if (useSingleNoteContextMenu)
|
|
{
|
|
this.openHoldNoteContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedHoldNote.noteData);
|
|
}
|
|
else
|
|
{
|
|
this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Right click removes hold from the note.
|
|
this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
|
|
performCommand(new ExtendNoteLengthCommand(highlightedHoldNote.noteData, 0));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Right clicked on nothing.
|
|
}
|
|
}
|
|
|
|
var isOrWillSelect = overlapsSelection || dragTargetNote != null || dragTargetEvent != null || overlapsRenderedNotes || overlapsRenderedHoldNotes
|
|
|| overlapsRenderedEvents;
|
|
// Handle grid cursor.
|
|
if (!isCursorOverHaxeUI && overlapsGrid && !isOrWillSelect && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed)
|
|
{
|
|
// Indicate that we can place a note here.
|
|
|
|
if (cursorGridPos == eventColumn)
|
|
{
|
|
if (gridGhostNote != null) gridGhostNote.visible = false;
|
|
if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
|
|
|
|
if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()";
|
|
|
|
var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, eventKindToPlace, null);
|
|
|
|
if (eventKindToPlace != eventData.eventKind)
|
|
{
|
|
eventData.eventKind = eventKindToPlace;
|
|
}
|
|
eventData.time = cursorSnappedMs;
|
|
|
|
gridGhostEvent.visible = true;
|
|
gridGhostEvent.eventData = eventData;
|
|
gridGhostEvent.updateEventPosition(renderedEvents);
|
|
|
|
targetCursorMode = Cell;
|
|
}
|
|
else
|
|
{
|
|
if (gridGhostEvent != null) gridGhostEvent.visible = false;
|
|
|
|
if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
|
|
|
|
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace);
|
|
|
|
if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind)
|
|
{
|
|
noteData.kind = noteKindToPlace;
|
|
noteData.data = cursorColumn;
|
|
gridGhostNote.playNoteAnimation();
|
|
}
|
|
noteData.time = cursorSnappedMs;
|
|
|
|
gridGhostNote.visible = true;
|
|
gridGhostNote.noteData = noteData;
|
|
gridGhostNote.updateNotePosition(renderedNotes);
|
|
|
|
targetCursorMode = Cell;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (gridGhostNote != null) gridGhostNote.visible = false;
|
|
if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
|
|
if (gridGhostEvent != null) gridGhostEvent.visible = false;
|
|
}
|
|
}
|
|
|
|
if (targetCursorMode == null)
|
|
{
|
|
if (FlxG.mouse.pressed)
|
|
{
|
|
if (overlapsSelection)
|
|
{
|
|
targetCursorMode = Grabbing;
|
|
}
|
|
if (overlapsSelectionBorder)
|
|
{
|
|
targetCursorMode = Crosshair;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!isCursorOverHaxeUI)
|
|
{
|
|
if (notePreview != null && FlxG.mouse.overlaps(notePreview))
|
|
{
|
|
targetCursorMode = Pointer;
|
|
}
|
|
else if (measureTicks != null && FlxG.mouse.overlaps(measureTicks))
|
|
{
|
|
targetCursorMode = Pointer;
|
|
}
|
|
else if (overlapsSelection)
|
|
{
|
|
targetCursorMode = Pointer;
|
|
}
|
|
else if (overlapsSelectionBorder)
|
|
{
|
|
targetCursorMode = Crosshair;
|
|
}
|
|
else if (overlapsRenderedNotes)
|
|
{
|
|
targetCursorMode = Pointer;
|
|
}
|
|
else if (overlapsRenderedHoldNotes)
|
|
{
|
|
targetCursorMode = Pointer;
|
|
}
|
|
else if (overlapsRenderedEvents)
|
|
{
|
|
targetCursorMode = Pointer;
|
|
}
|
|
else if (overlapsGrid)
|
|
{
|
|
targetCursorMode = Cell;
|
|
}
|
|
else if (overlapsHealthIcons)
|
|
{
|
|
targetCursorMode = Pointer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Actually set the cursor mode to the one we specified earlier.
|
|
Cursor.cursorMode = targetCursorMode ?? Default;
|
|
}
|
|
else
|
|
{
|
|
if (gridGhostNote != null) gridGhostNote.visible = false;
|
|
if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
|
|
if (gridGhostEvent != null) gridGhostEvent.visible = false;
|
|
|
|
// Do not set Cursor.cursorMode here, because it will be set by the HaxeUI.
|
|
}
|
|
}
|
|
|
|
function handleToolboxes():Void
|
|
{
|
|
handleDifficultyToolbox();
|
|
handlePlayerPreviewToolbox();
|
|
handleOpponentPreviewToolbox();
|
|
}
|
|
|
|
function handleDifficultyToolbox():Void
|
|
{
|
|
if (difficultySelectDirty)
|
|
{
|
|
difficultySelectDirty = false;
|
|
|
|
var difficultyToolbox:ChartEditorDifficultyToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
|
if (difficultyToolbox == null) return;
|
|
|
|
difficultyToolbox.updateTree();
|
|
}
|
|
}
|
|
|
|
function handlePlayerPreviewToolbox():Void
|
|
{
|
|
// Manage the Select Difficulty tree view.
|
|
var charPreviewToolbox:Null<CollapsibleDialog> = this.getToolbox_OLD(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_OLD(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleSelectionButtons():Void
|
|
{
|
|
// Make sure buttons are never nudged out of the correct spot.
|
|
// TODO: Why do these even move in the first place? The camera never moves, LOL.
|
|
buttonSelectOpponent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2;
|
|
buttonSelectPlayer.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2;
|
|
buttonSelectEvent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2;
|
|
}
|
|
|
|
/**
|
|
* Handles display elements for the playbar at the bottom.
|
|
*/
|
|
function handlePlaybar():Void
|
|
{
|
|
if (playbarHeadLayout == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!";
|
|
|
|
// Make sure the playbar is never nudged out of the correct spot.
|
|
playbarHeadLayout.x = 4;
|
|
playbarHeadLayout.y = FlxG.height - 48 - 8;
|
|
|
|
// Move the playhead to match the song position, if we aren't dragging it.
|
|
if (!playbarHeadDragging)
|
|
{
|
|
var songPosPercent = scrollPositionInPixels / (songLengthInPixels) * 100;
|
|
|
|
if (playbarHeadLayout.playbarHead.value != songPosPercent) playbarHeadLayout.playbarHead.value = songPosPercent;
|
|
}
|
|
|
|
var songPos:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
|
|
var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2);
|
|
var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2);
|
|
if (songPos < 0) songPosMinutes = '-' + songPosMinutes;
|
|
var songPosString:String = '${songPosMinutes}:${songPosSeconds}';
|
|
|
|
if (playbarSongPos.value != songPosString) playbarSongPos.value = songPosString;
|
|
|
|
var songRemaining:Float = Math.max(songLengthInMs - songPos, 0.0);
|
|
var songRemainingSeconds:String = Std.string(Math.floor((songRemaining / 1000) % 60)).lpad('0', 2);
|
|
var songRemainingMinutes:String = Std.string(Math.floor((songRemaining / 1000) / 60)).lpad('0', 2);
|
|
var songRemainingString:String = '-${songRemainingMinutes}:${songRemainingSeconds}';
|
|
|
|
if (playbarSongRemaining.value != songRemainingString) playbarSongRemaining.value = songRemainingString;
|
|
|
|
playbarNoteSnap.text = '1/${noteSnapQuant}';
|
|
playbarDifficulty.text = '${selectedDifficulty.toTitleCase()}';
|
|
playbarBPM.text = 'BPM: ${(Conductor.instance.bpm ?? 0.0)}';
|
|
}
|
|
|
|
function handlePlayhead():Void
|
|
{
|
|
// Place notes at the playhead with the keyboard.
|
|
switch (currentLiveInputStyle)
|
|
{
|
|
case ChartEditorLiveInputStyle.WASDKeys:
|
|
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
|
|
if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
|
|
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
|
|
if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
|
|
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
|
|
if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
|
|
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
|
|
if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
|
|
|
|
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
|
|
if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
|
|
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
|
|
if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
|
|
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
|
|
if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
|
|
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
|
|
if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(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.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
|
|
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
|
|
if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
|
|
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
|
|
if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
|
|
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
|
|
if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
|
|
|
|
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
|
|
if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
|
|
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
|
|
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
|
|
if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
|
|
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
|
|
if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
|
|
case ChartEditorLiveInputStyle.None:
|
|
// Do nothing.
|
|
}
|
|
|
|
// Place events at playhead.
|
|
if (FlxG.keys.justPressed.COMMA) placeEventAtPlayhead(true);
|
|
if (FlxG.keys.justPressed.PERIOD) placeEventAtPlayhead(false);
|
|
|
|
updatePlayheadGhostHoldNotes();
|
|
}
|
|
|
|
function placeNoteAtPlayhead(column:Int):Void
|
|
{
|
|
// SHIFT + press or LEFT_SHOULDER + press to remove notes instead of placing them.
|
|
var removeNoteInstead:Bool = FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_SHOULDER ?? false);
|
|
|
|
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.instance.stepLengthMs * noteSnapRatio;
|
|
|
|
// Look for notes within 1 step of the playhead.
|
|
var notesAtPos:Array<SongNoteData> = SongDataUtils.getNotesInTimeRange(currentSongChartNoteData, playheadPosSnappedMs,
|
|
playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio);
|
|
notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]);
|
|
|
|
if (notesAtPos.length == 0 && !removeNoteInstead)
|
|
{
|
|
trace('Placing note. ${column}');
|
|
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
|
|
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
|
|
currentLiveInputPlaceNoteData[column] = newNoteData;
|
|
}
|
|
else if (removeNoteInstead)
|
|
{
|
|
trace('Removing existing note at position. ${column}');
|
|
performCommand(new RemoveNotesCommand(notesAtPos));
|
|
}
|
|
else
|
|
{
|
|
trace('Already a note there. ${column}');
|
|
}
|
|
}
|
|
|
|
function placeEventAtPlayhead(isOpponent:Bool):Void
|
|
{
|
|
// SHIFT + press or LEFT_SHOULDER + press to remove events instead of placing them.
|
|
var removeEventInstead:Bool = FlxG.keys.pressed.SHIFT || (FlxG.gamepads.firstActive?.pressed?.LEFT_SHOULDER ?? false);
|
|
|
|
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.instance.stepLengthMs * noteSnapRatio;
|
|
|
|
// Look for events within 1 step of the playhead.
|
|
var eventsAtPos:Array<SongEventData> = SongDataUtils.getEventsInTimeRange(currentSongChartEventData, playheadPosSnappedMs,
|
|
playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio);
|
|
eventsAtPos = SongDataUtils.getEventsWithKind(eventsAtPos, ['FocusCamera']);
|
|
|
|
if (eventsAtPos.length == 0 && !removeEventInstead)
|
|
{
|
|
trace('Placing event ${isOpponent}');
|
|
var newEventData:SongEventData = new SongEventData(playheadPosSnappedMs, 'FocusCamera',
|
|
{
|
|
char: isOpponent ? 1 : 0,
|
|
});
|
|
performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
|
|
}
|
|
else if (removeEventInstead)
|
|
{
|
|
trace('Removing existing event at position.');
|
|
performCommand(new RemoveEventsCommand(eventsAtPos));
|
|
}
|
|
else
|
|
{
|
|
trace('Already an event there.');
|
|
}
|
|
}
|
|
|
|
function updatePlayheadGhostHoldNotes():Void
|
|
{
|
|
// Ensure all the ghost hold notes exist.
|
|
while (gridPlayheadGhostHoldNotes.length < (STRUMLINE_SIZE * 2))
|
|
{
|
|
var ghost = new ChartEditorHoldNoteSprite(this);
|
|
ghost.alpha = 0.6;
|
|
ghost.noteData = null;
|
|
ghost.visible = false;
|
|
ghost.zIndex = 11;
|
|
add(ghost); // Don't add to `renderedHoldNotes` because then it will get killed every frame.
|
|
|
|
gridPlayheadGhostHoldNotes.push(ghost);
|
|
refresh();
|
|
}
|
|
|
|
// Update playhead ghost hold notes.
|
|
for (column in 0...gridPlayheadGhostHoldNotes.length)
|
|
{
|
|
var targetNoteData = currentLiveInputPlaceNoteData[column];
|
|
var ghostHold = gridPlayheadGhostHoldNotes[column];
|
|
|
|
if (targetNoteData == null && ghostHold.noteData != null)
|
|
{
|
|
// Remove the ghost hold note.
|
|
ghostHold.noteData = null;
|
|
}
|
|
|
|
if (targetNoteData != null && ghostHold.noteData == null)
|
|
{
|
|
// Readd the new ghost hold note.
|
|
ghostHold.noteData = targetNoteData.clone();
|
|
ghostHold.noteDirection = ghostHold.noteData.getDirection();
|
|
ghostHold.visible = true;
|
|
ghostHold.alpha = 0.6;
|
|
ghostHold.setHeightDirectly(0);
|
|
ghostHold.updateHoldNotePosition(renderedHoldNotes);
|
|
}
|
|
|
|
if (ghostHold.noteData == null)
|
|
{
|
|
ghostHold.visible = false;
|
|
ghostHold.setHeightDirectly(0);
|
|
playheadDragLengthCurrent[column] = 0;
|
|
continue;
|
|
}
|
|
|
|
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.instance.stepLengthMs * noteSnapRatio;
|
|
|
|
var newNoteLength:Float = playheadPosSnappedMs - ghostHold.noteData.time;
|
|
trace('newNoteLength: ${newNoteLength}');
|
|
|
|
if (newNoteLength > 0)
|
|
{
|
|
ghostHold.noteData.length = newNoteLength;
|
|
var targetNoteLengthSteps:Float = ghostHold.noteData.getStepLength(true);
|
|
var targetNoteLengthStepsInt:Int = Std.int(Math.floor(targetNoteLengthSteps));
|
|
var targetNoteLengthPixels:Float = targetNoteLengthSteps * GRID_SIZE;
|
|
|
|
if (playheadDragLengthCurrent[column] != targetNoteLengthStepsInt)
|
|
{
|
|
stretchySounds = !stretchySounds;
|
|
this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
|
|
playheadDragLengthCurrent[column] = targetNoteLengthStepsInt;
|
|
}
|
|
ghostHold.visible = true;
|
|
ghostHold.alpha = 0.6;
|
|
ghostHold.setHeightDirectly(targetNoteLengthPixels, true);
|
|
ghostHold.updateHoldNotePosition(renderedHoldNotes);
|
|
trace('lerpLength: ${ghostHold.fullSustainLength}');
|
|
trace('position: ${ghostHold.x}, ${ghostHold.y}');
|
|
}
|
|
else
|
|
{
|
|
ghostHold.visible = false;
|
|
ghostHold.setHeightDirectly(0);
|
|
playheadDragLengthCurrent[column] = 0;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
function finishPlaceNoteAtPlayhead(column:Int):Void
|
|
{
|
|
if (currentLiveInputPlaceNoteData[column] == null) return;
|
|
|
|
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.instance.stepLengthMs * noteSnapRatio;
|
|
|
|
var newNoteLength:Float = playheadPosSnappedMs - currentLiveInputPlaceNoteData[column].time;
|
|
trace('finishPlace newNoteLength: ${newNoteLength}');
|
|
|
|
if (newNoteLength < Conductor.instance.stepLengthMs)
|
|
{
|
|
// Don't extend the note if it's too short.
|
|
trace('Not extending note. ${column}');
|
|
currentLiveInputPlaceNoteData[column] = null;
|
|
gridPlayheadGhostHoldNotes[column].noteData = null;
|
|
}
|
|
else
|
|
{
|
|
// Extend the note to the playhead position.
|
|
trace('Extending note. ${column}');
|
|
this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
|
|
performCommand(new ExtendNoteLengthCommand(currentLiveInputPlaceNoteData[column], newNoteLength));
|
|
currentLiveInputPlaceNoteData[column] = null;
|
|
gridPlayheadGhostHoldNotes[column].noteData = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 (buttonSelectPlayer != null)
|
|
{
|
|
buttonSelectPlayer.text = charDataBF?.name ?? 'Player';
|
|
}
|
|
if (healthIconDad != null)
|
|
{
|
|
healthIconDad.configure(charDataDad?.healthIcon);
|
|
healthIconDad.size *= 0.5; // Make the icon smaller in Chart Editor.
|
|
}
|
|
if (buttonSelectOpponent != null)
|
|
{
|
|
buttonSelectOpponent.text = charDataDad?.name ?? 'Opponent';
|
|
}
|
|
healthIconsDirty = false;
|
|
}
|
|
|
|
// Right align, and visibly center, the BF health icon.
|
|
if (healthIconBF != null)
|
|
{
|
|
// Base X position to the right of the grid.
|
|
var xOffset = 45 - (healthIconBF.width / 2);
|
|
healthIconBF.x = (gridTiledSprite == null) ? (0) : (GRID_X_POS + gridTiledSprite.width + xOffset);
|
|
var yOffset = 30 - (healthIconBF.height / 2);
|
|
healthIconBF.y = (gridTiledSprite == null) ? (0) : (GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT) + yOffset;
|
|
}
|
|
|
|
// Visibly center the Dad health icon.
|
|
if (healthIconDad != null)
|
|
{
|
|
var xOffset = 75 + (healthIconDad.width / 2);
|
|
healthIconDad.x = (gridTiledSprite == null) ? (0) : (GRID_X_POS - xOffset);
|
|
var yOffset = 30 - (healthIconDad.height / 2);
|
|
healthIconDad.y = (gridTiledSprite == null) ? (0) : (GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT) + yOffset;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle keybinds for File menu items.
|
|
*/
|
|
function handleFileKeybinds():Void
|
|
{
|
|
// CTRL + N = New Chart
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.N && !isHaxeUIDialogOpen)
|
|
{
|
|
this.openWelcomeDialog(true);
|
|
}
|
|
|
|
// CTRL + O = Open Chart
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.O && !isHaxeUIDialogOpen)
|
|
{
|
|
this.openBrowseFNFC(true);
|
|
}
|
|
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.S && !isHaxeUIDialogOpen)
|
|
{
|
|
if (currentWorkingFilePath == null || FlxG.keys.pressed.SHIFT)
|
|
{
|
|
// CTRL + SHIFT + S = Save As
|
|
this.exportAllSongData(false, null, function(path:String) {
|
|
// CTRL + SHIFT + S Successful
|
|
this.success('Saved Chart', 'Chart saved successfully to ${path}.');
|
|
}, function() {
|
|
// CTRL + SHIFT + S Cancelled
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// CTRL + S = Save Chart
|
|
this.exportAllSongData(true, currentWorkingFilePath);
|
|
this.success('Saved Chart', 'Chart saved successfully to ${currentWorkingFilePath}.');
|
|
}
|
|
}
|
|
|
|
// CTRL + Q = Quit to Menu
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q)
|
|
{
|
|
quitChartEditor();
|
|
}
|
|
}
|
|
|
|
@:nullSafety(Off)
|
|
function quitChartEditor():Void
|
|
{
|
|
autoSave();
|
|
stopWelcomeMusic();
|
|
// TODO: PR Flixel to make onComplete nullable.
|
|
if (audioInstTrack != null) audioInstTrack.onComplete = null;
|
|
FlxG.switchState(() -> new MainMenuState());
|
|
|
|
resetWindowTitle();
|
|
|
|
criticalFailure = true;
|
|
}
|
|
|
|
/**
|
|
* Handle keybinds for edit menu items.
|
|
*/
|
|
function handleEditKeybinds():Void
|
|
{
|
|
// CTRL + Z = Undo
|
|
if (undoKeyHandler.activated)
|
|
{
|
|
undoLastCommand();
|
|
}
|
|
|
|
// CTRL + Y = Redo
|
|
if (redoKeyHandler.activated)
|
|
{
|
|
redoLastCommand();
|
|
}
|
|
|
|
// CTRL + C = Copy
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.C)
|
|
{
|
|
performCommand(new CopyItemsCommand(currentNoteSelection, currentEventSelection));
|
|
}
|
|
|
|
// CTRL + X = Cut
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.X)
|
|
{
|
|
// Cut selected notes.
|
|
performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection));
|
|
}
|
|
|
|
// CTRL + V = Paste
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.V)
|
|
{
|
|
// CTRL + SHIFT + V = Paste Unsnapped.
|
|
var targetMs:Float = if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
scrollPositionInMs + playheadPositionInMs;
|
|
}
|
|
else
|
|
{
|
|
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
|
|
var targetStep:Float = Conductor.instance.getTimeInSteps(targetMs);
|
|
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
|
|
var targetSnappedMs:Float = Conductor.instance.getStepTimeInMs(targetSnappedStep);
|
|
targetSnappedMs;
|
|
}
|
|
performCommand(new PasteItemsCommand(targetMs));
|
|
}
|
|
|
|
// DELETE = Delete
|
|
var delete:Bool = FlxG.keys.justPressed.DELETE;
|
|
|
|
// on macbooks, Delete == backspace
|
|
#if mac
|
|
delete = delete || FlxG.keys.justPressed.BACKSPACE;
|
|
#end
|
|
|
|
if (delete)
|
|
{
|
|
// Delete selected items.
|
|
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
|
|
{
|
|
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
|
|
}
|
|
else if (currentNoteSelection.length > 0)
|
|
{
|
|
performCommand(new RemoveNotesCommand(currentNoteSelection));
|
|
}
|
|
else if (currentEventSelection.length > 0)
|
|
{
|
|
performCommand(new RemoveEventsCommand(currentEventSelection));
|
|
}
|
|
}
|
|
|
|
// CTRL + F = Flip Notes
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.F)
|
|
{
|
|
// Flip selected notes.
|
|
performCommand(new FlipNotesCommand(currentNoteSelection));
|
|
}
|
|
|
|
// CTRL + A = Select All Notes
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.A)
|
|
{
|
|
// Select all items.
|
|
if (FlxG.keys.pressed.ALT)
|
|
{
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
// CTRL + ALT + SHIFT + A = Append All Events to Selection
|
|
performCommand(new SelectItemsCommand([], currentSongChartEventData));
|
|
}
|
|
else
|
|
{
|
|
// CTRL + ALT + A = Set Selection to All Events
|
|
performCommand(new SelectAllItemsCommand(false, true));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (FlxG.keys.pressed.SHIFT)
|
|
{
|
|
// CTRL + SHIFT + A = Append All Notes to Selection
|
|
performCommand(new SelectItemsCommand(currentSongChartNoteData, []));
|
|
}
|
|
else
|
|
{
|
|
// CTRL + A = Set Selection to All Notes
|
|
performCommand(new SelectAllItemsCommand(true, false));
|
|
}
|
|
}
|
|
}
|
|
|
|
// CTRL + I = Select Inverse
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.I)
|
|
{
|
|
// Select unselected items and deselect selected items.
|
|
performCommand(new InvertSelectedItemsCommand());
|
|
}
|
|
|
|
// CTRL + D = Select None
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.D)
|
|
{
|
|
// Deselect all items.
|
|
performCommand(new DeselectAllItemsCommand());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle keybinds for View menu items.
|
|
*/
|
|
function handleViewKeybinds():Void
|
|
{
|
|
if (currentLiveInputStyle == None)
|
|
{
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.LEFT)
|
|
{
|
|
incrementDifficulty(-1);
|
|
}
|
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.RIGHT)
|
|
{
|
|
incrementDifficulty(1);
|
|
}
|
|
// Would bind Ctrl+A and Ctrl+D here, but they are already bound to Select All and Select None.
|
|
}
|
|
else
|
|
{
|
|
trace('Ignoring keybinds for View menu items because we are in live input mode (${currentLiveInputStyle}).');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle keybinds for the Test menu items.
|
|
*/
|
|
function handleTestKeybinds():Void
|
|
{
|
|
if (!isHaxeUIDialogOpen && !isHaxeUIFocused && 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('musicTime', audioInstTrack?.time ?? 0.0);
|
|
|
|
FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace);
|
|
FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace);
|
|
|
|
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);
|
|
}
|
|
|
|
function handlePostUpdate():Void
|
|
{
|
|
wasCursorOverHaxeUI = isCursorOverHaxeUI;
|
|
}
|
|
|
|
/**
|
|
* PLAYTEST FUNCTIONS
|
|
*/
|
|
// ====================
|
|
|
|
/**
|
|
* Transitions to the Play State to test the song
|
|
*/
|
|
function testSongInPlayState(minimal:Bool = false):Void
|
|
{
|
|
autoSave(true);
|
|
|
|
stopWelcomeMusic();
|
|
stopAudioPlayback();
|
|
|
|
var startTimestamp:Float = 0;
|
|
if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
|
|
|
|
var playbackRate:Float = ((menubarItemPlaybackSpeed.value ?? 1.0) * 2.0) / 100.0;
|
|
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
|
|
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
|
|
|
|
var targetSong:Song;
|
|
try
|
|
{
|
|
targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, playtestSongScripts, false);
|
|
}
|
|
catch (e)
|
|
{
|
|
this.error('Could Not Playtest', 'Got an error trying to playtest the song.\n${e}');
|
|
return;
|
|
}
|
|
|
|
// TODO: Rework asset system so we can remove this jank.
|
|
switch (currentSongStage)
|
|
{
|
|
case 'mainStage':
|
|
PlayStatePlaylist.campaignId = 'week1';
|
|
case 'spookyMansion':
|
|
PlayStatePlaylist.campaignId = 'week2';
|
|
case 'phillyTrain':
|
|
PlayStatePlaylist.campaignId = 'week3';
|
|
case 'limoRide':
|
|
PlayStatePlaylist.campaignId = 'week4';
|
|
case 'mallXmas' | 'mallEvil':
|
|
PlayStatePlaylist.campaignId = 'week5';
|
|
case 'school' | 'schoolEvil':
|
|
PlayStatePlaylist.campaignId = 'week6';
|
|
case 'tankmanBattlefield':
|
|
PlayStatePlaylist.campaignId = 'week7';
|
|
case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2':
|
|
PlayStatePlaylist.campaignId = 'weekend1';
|
|
}
|
|
Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
|
|
|
|
subStateClosed.add(reviveUICamera);
|
|
subStateClosed.add(resetConductorAfterTest);
|
|
|
|
FlxTransitionableState.skipNextTransIn = false;
|
|
FlxTransitionableState.skipNextTransOut = false;
|
|
|
|
var targetStateParams =
|
|
{
|
|
targetSong: targetSong,
|
|
targetDifficulty: selectedDifficulty,
|
|
targetVariation: selectedVariation,
|
|
practiceMode: playtestPracticeMode,
|
|
botPlayMode: playtestBotPlayMode,
|
|
minimalMode: minimal,
|
|
startTimestamp: startTimestamp,
|
|
playbackRate: playbackRate,
|
|
overrideMusic: true,
|
|
};
|
|
|
|
// Override music.
|
|
if (audioInstTrack != null)
|
|
{
|
|
FlxG.sound.music = audioInstTrack;
|
|
}
|
|
|
|
// Kill and replace the UI camera so it doesn't get destroyed during the state transition.
|
|
uiCamera.kill();
|
|
FlxG.cameras.remove(uiCamera, false);
|
|
FlxG.cameras.reset(new FunkinCamera('chartEditorUI2'));
|
|
|
|
this.persistentUpdate = false;
|
|
this.persistentDraw = false;
|
|
stopWelcomeMusic();
|
|
|
|
LoadingState.loadPlayState(targetStateParams, false, true, function(targetState) {
|
|
targetState.vocals = audioVocalTrackGroup;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 after performing the command.
|
|
*/
|
|
function performCommand(command:ChartEditorCommand, purgeRedoStack:Bool = true):Void
|
|
{
|
|
command.execute(this);
|
|
if (command.shouldAddToHistory(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);
|
|
// Note, if we are undoing a command, it should already be in the history,
|
|
// therefore we don't need to check `shouldAddToHistory(state)`
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 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(this);
|
|
result.loadGraphic(selectionSquareBitmap);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Revive the UI camera and re-establish it as the main camera so UI elements depending on it don't explode.
|
|
*/
|
|
function reviveUICamera(_:FlxSubState = null):Void
|
|
{
|
|
uiCamera.revive();
|
|
FlxG.cameras.reset(uiCamera);
|
|
|
|
add(this.root);
|
|
}
|
|
|
|
/**
|
|
* AUDIO FUNCTIONS
|
|
*/
|
|
// ====================
|
|
|
|
function startAudioPlayback():Void
|
|
{
|
|
if (audioInstTrack != null)
|
|
{
|
|
audioInstTrack.play(false, audioInstTrack.time);
|
|
audioVocalTrackGroup.play(false, audioInstTrack.time);
|
|
}
|
|
|
|
playbarPlay.text = '||'; // Pause
|
|
}
|
|
|
|
/**
|
|
* 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'}'), metronomeVolume);
|
|
}
|
|
|
|
function switchToCurrentInstrumental():Void
|
|
{
|
|
// ChartEditorAudioHandler
|
|
this.switchToInstrumental(currentInstrumentalId, currentSongMetadata.playData.characters.player, currentSongMetadata.playData.characters.opponent);
|
|
}
|
|
|
|
public function updateGridHeight():Void
|
|
{
|
|
// Make sure playhead doesn't go outside the song after we update the grid height.
|
|
if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs;
|
|
|
|
if (gridTiledSprite != null)
|
|
{
|
|
gridTiledSprite.height = songLengthInPixels;
|
|
}
|
|
if (measureTicks != null)
|
|
{
|
|
measureTicks.setHeight(songLengthInPixels);
|
|
}
|
|
|
|
// Remove any notes past the end of the song.
|
|
var songCutoffPointSteps:Float = songLengthInSteps - 0.1;
|
|
var songCutoffPointMs:Float = Conductor.instance.getStepTimeInMs(songCutoffPointSteps);
|
|
currentSongChartNoteData = SongDataUtils.clampSongNoteData(currentSongChartNoteData, 0.0, songCutoffPointMs);
|
|
currentSongChartEventData = SongDataUtils.clampSongEventData(currentSongChartEventData, 0.0, songCutoffPointMs);
|
|
|
|
scrollPositionInPixels = 0;
|
|
playheadPositionInPixels = 0;
|
|
notePreviewDirty = true;
|
|
notePreviewViewportBoundsDirty = true;
|
|
noteDisplayDirty = 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 isEventSelected(event:Null<SongEventData>):Bool
|
|
{
|
|
return event != null && currentEventSelection.indexOf(event) != -1;
|
|
}
|
|
|
|
function createDifficulty(variation:String, difficulty:String, scrollSpeed:Float = 1.0):Void
|
|
{
|
|
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 removeDifficulty(variation:String, difficulty:String):Void
|
|
{
|
|
var variationMetadata:Null<SongMetadata> = songMetadata.get(variation);
|
|
if (variationMetadata == null) return;
|
|
|
|
variationMetadata.playData.difficulties.remove(difficulty);
|
|
|
|
var resultChartData = songChartData.get(variation);
|
|
if (resultChartData != null)
|
|
{
|
|
resultChartData.scrollSpeed.remove(difficulty);
|
|
resultChartData.notes.remove(difficulty);
|
|
}
|
|
|
|
if (songMetadata.size() > 1)
|
|
{
|
|
if (variationMetadata.playData.difficulties.length == 0)
|
|
{
|
|
songMetadata.remove(variation);
|
|
songChartData.remove(variation);
|
|
}
|
|
|
|
if (variation == selectedVariation)
|
|
{
|
|
var firstVariation = songMetadata.keyValues()[0];
|
|
if (firstVariation != null) selectedVariation = firstVariation;
|
|
variationMetadata = songMetadata.get(selectedVariation);
|
|
}
|
|
}
|
|
|
|
if (selectedDifficulty == difficulty
|
|
|| !variationMetadata.playData.difficulties.contains(selectedDifficulty)) selectedDifficulty = variationMetadata.playData.difficulties[0];
|
|
|
|
difficultySelectDirty = true; // Force the Difficulty toolbox to update.
|
|
}
|
|
|
|
function incrementDifficulty(change:Int):Void
|
|
{
|
|
var currentDifficultyIndex:Int = availableDifficulties.indexOf(selectedDifficulty);
|
|
var currentAllDifficultyIndex:Int = allDifficulties.indexOf(selectedDifficulty);
|
|
|
|
if (currentDifficultyIndex == -1 || currentAllDifficultyIndex == -1)
|
|
{
|
|
trace('ERROR determining difficulty index!');
|
|
}
|
|
|
|
var isFirstDiff:Bool = currentAllDifficultyIndex == 0;
|
|
var isLastDiff:Bool = (currentAllDifficultyIndex == allDifficulties.length - 1);
|
|
|
|
var isFirstDiffInVariation:Bool = currentDifficultyIndex == 0;
|
|
var isLastDiffInVariation:Bool = (currentDifficultyIndex == availableDifficulties.length - 1);
|
|
|
|
trace(allDifficulties);
|
|
|
|
if (change < 0 && isFirstDiff)
|
|
{
|
|
trace('At lowest difficulty! Do nothing.');
|
|
return;
|
|
}
|
|
|
|
if (change > 0 && isLastDiff)
|
|
{
|
|
trace('At highest difficulty! Do nothing.');
|
|
return;
|
|
}
|
|
|
|
if (change < 0)
|
|
{
|
|
trace('Decrement difficulty.');
|
|
|
|
// If we reached this point, we are not at the lowest difficulty.
|
|
if (isFirstDiffInVariation)
|
|
{
|
|
// Go to the previous variation, then last difficulty in that variation.
|
|
var currentVariationIndex:Int = availableVariations.indexOf(selectedVariation);
|
|
var prevVariation = availableVariations[currentVariationIndex - 1];
|
|
selectedVariation = prevVariation;
|
|
|
|
var prevDifficulty = availableDifficulties[availableDifficulties.length - 1];
|
|
selectedDifficulty = prevDifficulty;
|
|
|
|
Conductor.instance.mapTimeChanges(this.currentSongMetadata.timeChanges);
|
|
updateTimeSignature();
|
|
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
|
}
|
|
else
|
|
{
|
|
// Go to previous difficulty in this variation.
|
|
var prevDifficulty = availableDifficulties[currentDifficultyIndex - 1];
|
|
selectedDifficulty = prevDifficulty;
|
|
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
trace('Increment difficulty.');
|
|
|
|
// If we reached this point, we are not at the highest difficulty.
|
|
if (isLastDiffInVariation)
|
|
{
|
|
// Go to next variation, then first difficulty in that variation.
|
|
var currentVariationIndex:Int = availableVariations.indexOf(selectedVariation);
|
|
var nextVariation = availableVariations[currentVariationIndex + 1];
|
|
selectedVariation = nextVariation;
|
|
|
|
var nextDifficulty = availableDifficulties[0];
|
|
selectedDifficulty = nextDifficulty;
|
|
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
|
}
|
|
else
|
|
{
|
|
// Go to next difficulty in this variation.
|
|
var nextDifficulty = availableDifficulties[currentDifficultyIndex + 1];
|
|
selectedDifficulty = nextDifficulty;
|
|
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
|
this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
|
}
|
|
}
|
|
|
|
// Removed this notification because you can see your difficulty in the playbar now.
|
|
// this.success('Switch Difficulty', 'Switched difficulty to ${selectedDifficulty.toTitleCase()}');
|
|
}
|
|
|
|
/**
|
|
* SCROLLING FUNCTIONS
|
|
*/
|
|
// ====================
|
|
|
|
/**
|
|
* 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 - Conductor.instance.instrumentalOffset;
|
|
// Update the songPosition in the Conductor.
|
|
Conductor.instance.update(audioInstTrack.time);
|
|
audioVocalTrackGroup.time = audioInstTrack.time;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fix the current scroll position after exiting the PlayState used when testing.
|
|
*/
|
|
@:nullSafety(Off)
|
|
function resetConductorAfterTest(_:FlxSubState = null):Void
|
|
{
|
|
this.persistentUpdate = true;
|
|
this.persistentDraw = true;
|
|
|
|
if (displayAutosavePopup)
|
|
{
|
|
displayAutosavePopup = false;
|
|
#if sys
|
|
Toolkit.callLater(() -> {
|
|
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
|
|
this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [
|
|
{
|
|
text: "Take Me There",
|
|
callback: openBackupsFolder,
|
|
}
|
|
]);
|
|
});
|
|
#else
|
|
// TODO: No auto-save on HTML5?
|
|
#end
|
|
}
|
|
|
|
moveSongToScrollPosition();
|
|
|
|
fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION);
|
|
|
|
// Reapply the volume.
|
|
var instTargetVolume:Float = menubarItemVolumeInstrumental.value ?? 1.0;
|
|
var vocalPlayerTargetVolume:Float = menubarItemVolumeVocalsPlayer.value ?? 1.0;
|
|
var vocalOpponentTargetVolume:Float = menubarItemVolumeVocalsOpponent.value ?? 1.0;
|
|
|
|
if (audioInstTrack != null)
|
|
{
|
|
audioInstTrack.volume = instTargetVolume;
|
|
audioInstTrack.onComplete = null;
|
|
}
|
|
if (audioVocalTrackGroup != null)
|
|
{
|
|
audioVocalTrackGroup.playerVolume = vocalPlayerTargetVolume;
|
|
audioVocalTrackGroup.opponentVolume = vocalOpponentTargetVolume;
|
|
}
|
|
}
|
|
|
|
function updateTimeSignature():Void
|
|
{
|
|
// Redo the grid bitmap to be 4/4.
|
|
this.updateTheme();
|
|
gridTiledSprite.loadGraphic(gridBitmap);
|
|
measureTicks.reloadTickBitmap();
|
|
}
|
|
|
|
/**
|
|
* HAXEUI FUNCTIONS
|
|
*/
|
|
// ==================
|
|
|
|
/**
|
|
* STATIC FUNCTIONS
|
|
*/
|
|
// ==================
|
|
|
|
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.addSelectedNotes(currentNoteSelection, Std.int(songLengthInMs));
|
|
notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs));
|
|
}
|
|
|
|
if (notePreviewViewportBoundsDirty)
|
|
{
|
|
setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
|
|
notePreviewViewportBoundsDirty = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
if (undoHistory.length == 0)
|
|
{
|
|
// Disable the Undo button.
|
|
menubarItemUndo.disabled = true;
|
|
menubarItemUndo.text = 'Undo';
|
|
}
|
|
else
|
|
{
|
|
// Change the label to the last command.
|
|
menubarItemUndo.disabled = false;
|
|
menubarItemUndo.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}';
|
|
}
|
|
|
|
if (redoHistory.length == 0)
|
|
{
|
|
// Disable the Redo button.
|
|
menubarItemRedo.disabled = true;
|
|
menubarItemRedo.text = 'Redo';
|
|
}
|
|
else
|
|
{
|
|
// Change the label to the last command.
|
|
menubarItemRedo.disabled = false;
|
|
menubarItemRedo.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', 0);
|
|
dispatchEvent(event);
|
|
|
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
|
if (event.eventCanceled) continue;
|
|
|
|
// Hitsounds.
|
|
switch (noteData.getStrumlineIndex())
|
|
{
|
|
case 0: // Player
|
|
if (hitsoundVolumePlayer > 0) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolumePlayer);
|
|
case 1: // Opponent
|
|
if (hitsoundVolumeOpponent > 0) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolumeOpponent);
|
|
}
|
|
}
|
|
}
|
|
|
|
function stopAudioPlayback():Void
|
|
{
|
|
if (audioInstTrack != null) audioInstTrack.pause();
|
|
audioVocalTrackGroup.pause();
|
|
|
|
playbarPlay.text = '>';
|
|
}
|
|
|
|
function toggleAudioPlayback():Void
|
|
{
|
|
if (audioInstTrack == null) return;
|
|
|
|
if (audioInstTrack.isPlaying)
|
|
{
|
|
// Pause
|
|
stopAudioPlayback();
|
|
fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION);
|
|
}
|
|
else
|
|
{
|
|
// Play
|
|
startAudioPlayback();
|
|
stopWelcomeMusic();
|
|
}
|
|
}
|
|
|
|
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();
|
|
// Keep the track at the end.
|
|
audioInstTrack.time = audioInstTrack.length;
|
|
}
|
|
audioVocalTrackGroup.pause();
|
|
};
|
|
}
|
|
else
|
|
{
|
|
trace('ERROR: Instrumental track is null!');
|
|
}
|
|
|
|
this.songLengthInMs = (audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset;
|
|
|
|
// Many things get reset when song length changes.
|
|
healthIconsDirty = true;
|
|
}
|
|
|
|
function hardRefreshOffsetsToolbox():Void
|
|
{
|
|
var offsetsToolbox:ChartEditorOffsetsToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT);
|
|
if (offsetsToolbox != null)
|
|
{
|
|
offsetsToolbox.refreshAudioPreview();
|
|
offsetsToolbox.refresh();
|
|
}
|
|
}
|
|
|
|
function hardRefreshFreeplayToolbox():Void
|
|
{
|
|
var freeplayToolbox:ChartEditorFreeplayToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT);
|
|
if (freeplayToolbox != null)
|
|
{
|
|
freeplayToolbox.refreshAudioPreview();
|
|
freeplayToolbox.refresh();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the voices group.
|
|
*/
|
|
public function clearVocals():Void
|
|
{
|
|
audioVocalTrackGroup.clear();
|
|
}
|
|
|
|
function isNoteSelected(note:Null<SongNoteData>):Bool
|
|
{
|
|
return note != null && currentNoteSelection.indexOf(note) != -1;
|
|
}
|
|
|
|
override function destroy():Void
|
|
{
|
|
super.destroy();
|
|
|
|
cleanupAutoSave();
|
|
|
|
this.closeAllMenus();
|
|
|
|
// Hide the mouse cursor on other states.
|
|
Cursor.hide();
|
|
|
|
@:privateAccess
|
|
ChartEditorNoteSprite.noteFrameCollection = null;
|
|
|
|
// Stop the music.
|
|
if (welcomeMusic != null) welcomeMusic.destroy();
|
|
if (audioInstTrack != null) audioInstTrack.destroy();
|
|
if (audioVocalTrackGroup != null) audioVocalTrackGroup.destroy();
|
|
}
|
|
|
|
function applyCanQuickSave():Void
|
|
{
|
|
if (menubarItemSaveChart == null) return;
|
|
|
|
if (currentWorkingFilePath == null)
|
|
{
|
|
menubarItemSaveChart.disabled = true;
|
|
}
|
|
else
|
|
{
|
|
menubarItemSaveChart.disabled = false;
|
|
}
|
|
}
|
|
|
|
function applyWindowTitle():Void
|
|
{
|
|
var inner:String = 'New Chart';
|
|
var cwfp:Null<String> = currentWorkingFilePath;
|
|
if (cwfp != null)
|
|
{
|
|
inner = cwfp;
|
|
}
|
|
if (currentWorkingFilePath == null || saveDataDirty)
|
|
{
|
|
inner += '*';
|
|
}
|
|
WindowUtil.setWindowTitle('Friday Night Funkin\' Chart Editor - ${inner}');
|
|
}
|
|
|
|
function resetWindowTitle():Void
|
|
{
|
|
WindowUtil.setWindowTitle('Friday Night Funkin\'');
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Available input modes for the chart editor state. Numbers/arrows/WASD available for other keybinds.
|
|
*/
|
|
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.
|
|
*/
|
|
WASDKeys;
|
|
}
|
|
|
|
typedef ChartEditorParams =
|
|
{
|
|
/**
|
|
* If non-null, load this song immediately instead of the welcome screen.
|
|
*/
|
|
var ?fnfcTargetPath:String;
|
|
|
|
/**
|
|
* If non-null, load this song immediately instead of the welcome screen.
|
|
*/
|
|
var ?targetSongId:String;
|
|
};
|
|
|
|
/**
|
|
* Available themes for the chart editor state.
|
|
*/
|
|
enum ChartEditorTheme
|
|
{
|
|
/**
|
|
* The default theme for the chart editor.
|
|
*/
|
|
Light;
|
|
|
|
/**
|
|
* A theme which introduces darker colors.
|
|
*/
|
|
Dark;
|
|
}
|