2022-09-07 19:07:08 -04:00
package funkin . ui . debug . charting ;
2023-02-28 13:17:28 -05:00
import haxe . ui . notifications . NotificationType ;
import haxe . ui . notifications . NotificationManager ;
2023-01-22 19:55:30 -05:00
import haxe . DynamicAccess ;
2023-01-02 17:40:53 -05:00
import haxe . io . Path ;
2022-10-26 01:14:29 -04:00
import flixel . addons . display . FlxSliceSprite ;
2022-10-07 00:37:21 -04:00
import flixel . addons . display . FlxTiledSprite ;
2022-12-17 15:19:42 -05:00
import flixel . FlxSprite ;
2022-09-07 19:07:08 -04:00
import flixel . group . FlxSpriteGroup ;
2022-10-26 01:14:29 -04:00
import flixel . math . FlxPoint ;
import flixel . math . FlxRect ;
2022-10-07 00:37:21 -04:00
import flixel . system . FlxSound ;
2022-09-07 19:07:08 -04:00
import flixel . util . FlxColor ;
2022-10-07 00:37:21 -04:00
import flixel . util . FlxSort ;
2022-10-13 21:32:19 -04:00
import flixel . util . FlxTimer ;
2022-10-07 00:37:21 -04:00
import funkin . audio . visualize . PolygonSpectogram ;
2022-12-17 15:19:42 -05:00
import funkin . audio . VocalGroup ;
import funkin . input . Cursor ;
import funkin . modding . events . ScriptEvent ;
import funkin . modding . events . ScriptEventDispatcher ;
2022-10-07 00:37:21 -04:00
import funkin . play . HealthIcon ;
2022-12-17 15:19:42 -05:00
import funkin . play . song . Song ;
2022-10-07 00:37:21 -04:00
import funkin . play . song . SongData . SongChartData ;
2022-12-17 15:19:42 -05:00
import funkin . play . song . SongData . SongDataParser ;
2022-10-07 00:37:21 -04:00
import funkin . play . song . SongData . SongEventData ;
import funkin . play . song . SongData . SongMetadata ;
import funkin . play . song . SongData . SongNoteData ;
2022-10-13 21:32:19 -04:00
import funkin . play . song . SongDataUtils ;
2022-10-07 00:37:21 -04:00
import funkin . play . song . SongSerializer ;
import funkin . ui . debug . charting . ChartEditorCommand ;
2022-10-26 01:14:29 -04:00
import funkin . ui . debug . charting . ChartEditorThemeHandler . ChartEditorTheme ;
import funkin . ui . debug . charting . ChartEditorToolboxHandler . ChartEditorToolMode ;
2022-12-17 15:19:42 -05:00
import funkin . ui . haxeui . components . CharacterPlayer ;
2022-09-07 19:07:08 -04:00
import funkin . ui . haxeui . HaxeUIState ;
2022-10-26 01:14:29 -04:00
import funkin . util . Constants ;
2022-12-17 15:19:42 -05:00
import funkin . util . FileUtil ;
2023-01-02 17:40:53 -05:00
import funkin . util . DateUtil ;
2022-12-17 15:19:42 -05:00
import funkin . util . SerializerUtil ;
2022-10-13 21:32:19 -04:00
import haxe . ui . components . Button ;
2022-10-11 03:14:57 -04:00
import haxe . ui . components . CheckBox ;
2022-10-26 01:14:29 -04:00
import haxe . ui . components . Label ;
2022-10-13 21:32:19 -04:00
import haxe . ui . components . Slider ;
2022-09-07 19:07:08 -04:00
import haxe . ui . containers . dialogs . Dialog ;
2022-10-13 21:32:19 -04:00
import haxe . ui . containers . dialogs . MessageBox ;
2022-10-07 00:37:21 -04:00
import haxe . ui . containers . menus . MenuCheckBox ;
2022-09-07 19:07:08 -04:00
import haxe . ui . containers . menus . MenuItem ;
2022-12-17 15:19:42 -05:00
import haxe . ui . containers . SideBar ;
import haxe . ui . containers . TreeView ;
import haxe . ui . containers . TreeViewNode ;
2022-09-07 19:07:08 -04:00
import haxe . ui . core . Component ;
2022-10-14 23:01:41 -04:00
import haxe . ui . core . Screen ;
2022-10-13 21:32:19 -04:00
import haxe . ui . events . DragEvent ;
2022-09-07 19:07:08 -04:00
import haxe . ui . events . MouseEvent ;
2022-10-07 00:37:21 -04:00
import haxe . ui . events . UIEvent ;
2022-12-17 15:19:42 -05:00
import lime . media . AudioBuffer ;
2023-01-02 17:40:53 -05:00
import funkin . util . WindowUtil ;
2022-09-07 19:07:08 -04:00
import openfl . display . BitmapData ;
2022-10-07 00:37:21 -04:00
import openfl . geom . Rectangle ;
2022-09-07 19:07:08 -04:00
2022-10-11 03:14:57 -04:00
using Lambda ;
2022-10-26 01:14:29 -04:00
/ * *
* A state dedicated to allowing the user to create and edit song charts .
* Built with HaxeUI for u s e b y b o t h d e v e l o p e r s a n d m o d d e r s .
*
* Some functionality is moved to other classes to help maintain my sanity .
*
* @ author MasterEric
* /
// Give other classes access to private instance fields
2022-10-07 00:37:21 -04:00
@ : allow ( funkin . ui . debug . charting . ChartEditorCommand )
2022-10-26 01:14:29 -04:00
@ : allow ( funkin . ui . debug . charting . ChartEditorDialogHandler )
@ : allow ( funkin . ui . debug . charting . ChartEditorThemeHandler )
@ : allow ( funkin . ui . debug . charting . ChartEditorToolboxHandler )
2022-09-07 19:07:08 -04:00
class ChartEditorState extends HaxeUIState
{
2023-01-22 19:55:30 -05:00
/ * *
* CONSTANTS
* /
// ==============================
// XML Layouts
static final CHART_EDITOR_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / m a i n - v i e w ' ) ;
static final CHART_EDITOR_NOTIFBAR_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / c o m p o n e n t s / n o t i f b a r ' ) ;
static final CHART_EDITOR_PLAYBARHEAD_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / c o m p o n e n t s / p l a y b a r - h e a d ' ) ;
static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / t o o l b o x / t o o l s ' ) ;
static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / t o o l b o x / n o t e d a t a ' ) ;
static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / t o o l b o x / e v e n t d a t a ' ) ;
static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / t o o l b o x / m e t a d a t a ' ) ;
static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / t o o l b o x / d i f f i c u l t y ' ) ;
static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / t o o l b o x / c h a r a c t e r s ' ) ;
static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / t o o l b o x / p l a y e r - p r e v i e w ' ) ;
static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT = Paths . ui ( ' c h a r t - e d i t o r / t o o l b o x / o p p o n e n t - p r e v i e w ' ) ;
2023-02-28 13:17:28 -05:00
// Validation
static final SUPPORTED_MUSIC_FORMATS : Array < String > = [ ' o g g ' ] ;
2023-01-22 19:55:30 -05:00
/ * *
* The base grid size for t h e c h a r t e d i t o r .
* /
public static final GRID_SIZE : Int = 40 ;
public static final PLAYHEAD_SCROLL_AREA_WIDTH : Int = 12 ;
public static final PLAYHEAD_HEIGHT : Int = Std . int ( GRID_SIZE / 8 ) ;
public static final GRID_SELECTION_BORDER_WIDTH : Int = 6 ;
/ * *
* Number of notes in each player ' s s t r u m l i n e .
* /
public static final STRUMLINE_SIZE = 4 ;
/ * *
* The height of the menu bar in the layout .
* /
static final MENU_BAR_HEIGHT = 32 ;
/ * *
* Duration to wait before autosaving the chart .
* /
static final AUTOSAVE_TIMER_DELAY : Float = 60.0 * 5.0 ;
/ * *
* The amount of padding between the menu bar and the chart grid when fully scrolled up .
* /
static final GRID_TOP_PAD : Int = 8 ;
/ * *
2023-02-28 13:17:28 -05:00
* Duration , i n milliseconds , until toast notifications are automatically hidden .
2023-01-22 19:55:30 -05:00
* /
2023-02-28 13:17:28 -05:00
static final NOTIFICATION_DISMISS_TIME : Int = 5000 ;
2023-01-22 19:55:30 -05:00
// Start performing rapid undo after this many seconds.
static final RAPID_UNDO_DELAY : Float = 0.4 ;
// Perform a rapid undo every this many seconds.
static final RAPID_UNDO_INTERVAL : Float = 0.1 ;
// UI Element Colors
// Background color tint.
static final CURSOR_COLOR : FlxColor = 0xE0FFFFFF ;
static final PREVIEW_BG_COLOR : FlxColor = 0xFF303030 ;
static final PLAYHEAD_SCROLL_AREA_COLOR : FlxColor = 0xFF682B2F ;
static final SPECTROGRAM_COLOR : FlxColor = 0xFFFF0000 ;
static final PLAYHEAD_COLOR : FlxColor = 0xC0BD0231 ;
/ * *
* How many pixels far the user needs to move the mouse before the cursor is considered to be dragged rather than clicked .
* /
static final DRAG_THRESHOLD : Float = 16.0 ;
/ * *
* Types of notes you can snap to .
* /
static final SNAP_QUANTS : Array < Int > = [ 4 , 8 , 12 , 16 , 20 , 24 , 32 , 48 , 64 , 96 , 192 ] ;
/ * *
* INSTANCE DATA
* /
// ==============================
public var currentZoomLevel: Float = 1.0 ;
var noteSnapQuantIndex: Int = 3 ;
public var noteSnapQuant( get , never ) : Int ;
function get_noteSnapQuant ( ) : Int
{
return SNAP_QUANTS [ noteSnapQuantIndex ] ;
}
/ * *
* scrollPosition is the current position in the song , i n pixels .
* One pixel is 1 / 40 of 1 step , and 1 / 160 of 1 beat .
* /
var scrollPositionInPixels( default , set ) : Float = - 1.0 ;
/ * *
* scrollPosition , converted to steps .
* TODO : Handle BPM changes .
* /
var scrollPositionInSteps( get , null ) : Float ;
function get_scrollPositionInSteps ( ) : Float
{
return scrollPositionInPixels / GRID_SIZE ;
}
/ * *
* scrollPosition , converted to milliseconds .
* TODO : Handle BPM changes .
* /
var scrollPositionInMs( get , set ) : Float ;
function get_scrollPositionInMs ( ) : Float
{
return scrollPositionInSteps * Conductor . stepCrochet ;
}
function set_scrollPositionInMs ( value : Float ) : Float
{
scrollPositionInPixels = value / Conductor . stepCrochet ;
return value ;
}
/ * *
* The position of the playhead , i n pixels , relative to the scrollPosition .
* 0 means playhead is at the top of the grid .
* 40 means the playhead is 1 grid length below the base position .
* - 40 means the playhead is 1 grid length above the base position .
* /
var playheadPositionInPixels( default , set ) : Float ;
var playheadPositionInSteps( get , null ) : Float ;
/ * *
* playheadPosition , converted to steps .
* /
function get_playheadPositionInSteps ( ) : Float
{
return playheadPositionInPixels / GRID_SIZE ;
}
/ * *
* playheadPosition , converted to milliseconds .
* /
var playheadPositionInMs( get , null ) : Float ;
function get_playheadPositionInMs ( ) : Float
{
return playheadPositionInSteps * Conductor . stepCrochet ;
}
/ * *
* This is the song ' s l e n g t h i n P I X E L S , s a m e f o r m a t a s s c r o l l P o s i t i o n .
* /
var songLengthInPixels( get , default ) : Int ;
function get_songLengthInPixels ( ) : Int
{
2023-01-22 22:25:45 -05:00
if ( songLengthInPixels <= 0 ) return 1000 ;
2023-01-22 19:55:30 -05:00
return songLengthInPixels ;
}
/ * *
* songLength , converted to steps .
* TODO : Handle BPM changes .
* /
var songLengthInSteps( get , set ) : Float ;
function get_songLengthInSteps ( ) : Float
{
return songLengthInPixels / GRID_SIZE ;
}
function set_songLengthInSteps ( value : Float ) : Float
{
songLengthInPixels = Std . int ( value * GRID_SIZE ) ;
return value ;
}
/ * *
* songLength , converted to milliseconds .
* TODO : Handle BPM changes .
* /
var songLengthInMs( get , set ) : Float ;
function get_songLengthInMs ( ) : Float
{
return songLengthInSteps * Conductor . stepCrochet ;
}
function set_songLengthInMs ( value : Float ) : Float
{
songLengthInSteps = Conductor . getTimeInSteps ( audioInstTrack . length ) ;
return value ;
}
var currentTheme( default , set ) : ChartEditorTheme = null ;
function set_currentTheme ( value : ChartEditorTheme ) : ChartEditorTheme
{
2023-01-22 22:25:45 -05:00
if ( value == null || value == currentTheme ) return currentTheme ;
2023-01-22 19:55:30 -05:00
currentTheme = value ;
ChartEditorThemeHandler . updateTheme ( this ) ;
return value ;
}
/ * *
* Whether a skip button has been pressed on the playbar , and which one .
* This will be used to update the scrollPosition ( i n t h e s a m e f u n c t i o n t h a t h a n d l e s t h e s c r o l l w h e e l ) , then cleared .
* /
var playbarButtonPressed: 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 ;
/ * *
* The note kind to use for n o t e s b e i n g p l a c e d i n t h e c h a r t . D e f a u l t s t o ` ' ' ` .
* /
var selectedNoteKind: String = ' ' ;
/ * *
* The note kind to use for n o t e s b e i n g p l a c e d i n t h e c h a r t . D e f a u l t s t o ` ' ' ` .
* /
var selectedEventKind: String = ' F o c u s C a m e r a ' ;
/ * *
* The note data as a struct .
* /
var selectedEventData: DynamicAccess < Dynamic > = { } ;
/ * *
* Whether to play a metronome sound while t h e p l a y h e a d i s m o v i n g .
* /
var shouldPlayMetronome: Bool = true ;
/ * *
* Use the tool window to affect how the user interacts with the program .
* /
var currentToolMode: ChartEditorToolMode = ChartEditorToolMode . Select ;
/ * *
* The character sprite in the Player Preview window .
* /
var currentPlayerCharacterPlayer: CharacterPlayer = null ;
/ * *
* The character sprite in the Opponent Preview window .
* /
var currentOpponentCharacterPlayer: CharacterPlayer = null ;
/ * *
* 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 ;
this . scrollPositionInPixels = this . scrollPositionInPixels ;
return isViewDownscroll ;
}
/ * *
* Whether hitsounds are enabled for a t l e a s t o n e c h a r a c t e r .
* /
var hitsoundsEnabled( get , null ) : Bool ;
function get_hitsoundsEnabled ( ) : Bool
{
return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent ;
}
/ * *
* Whether hitsounds are enabled for t h e p l a y e r .
* /
var hitsoundsEnabledPlayer: Bool = true ;
/ * *
* Whether hitsounds are enabled for t h e o p p o n e n t .
* /
var hitsoundsEnabledOpponent: Bool = true ;
/ * *
* Whether the user ' s m o u s e c u r s o r i s h o v e r i n g o v e r a S O L I D c o m p o n e n t o f t h e H a x e U I .
* If so , ignore mouse events underneath .
* /
var isCursorOverHaxeUI( get , null ) : Bool ;
function get_isCursorOverHaxeUI ( ) : Bool
{
return Screen . instance . hasSolidComponentUnderPoint ( FlxG . mouse . screenX , FlxG . mouse . screenY ) ;
}
var isCursorOverHaxeUIButton( get , null ) : Bool ;
function get_isCursorOverHaxeUIButton ( ) : Bool
{
return Screen . instance . hasSolidComponentUnderPoint ( FlxG . mouse . screenX , FlxG . mouse . screenY , haxe . ui . components . Button )
| | Screen . instance . hasSolidComponentUnderPoint ( FlxG . mouse . screenX , FlxG . mouse . screenY , haxe . ui . components . Link ) ;
}
/ * *
* Set by ChartEditorDialogHandler , used to prevent background interaction while t h e d i a l o g i s o p e n .
* /
public var isHaxeUIDialogOpen: Bool = false ;
/ * *
* The variation ID for t h e d i f f i c u l t y w h i c h i s c u r r e n t l y b e i n g e d i t e d .
* /
var selectedVariation( default , set ) : String = Constants . DEFAULT_VARIATION ;
function set_selectedVariation ( value : String ) : String
{
selectedVariation = value ;
// Make sure view is updated when the variation changes.
noteDisplayDirty = true ;
notePreviewDirty = true ;
return selectedVariation ;
}
/ * *
* The difficulty ID for t h e d i f f i c u l t y w h i c h i s c u r r e n t l y b e i n g e d i t e d .
* /
var selectedDifficulty( default , set ) : String = Constants . DEFAULT_DIFFICULTY ;
function set_selectedDifficulty ( value : String ) : String
{
selectedDifficulty = value ;
// Make sure view is updated when the difficulty changes.
noteDisplayDirty = true ;
notePreviewDirty = true ;
return selectedDifficulty ;
}
/ * *
* Whether the user is currently in Pattern Mode .
* This overrides the chart editor ' s n o r m a l b e h a v i o r .
* /
var isInPatternMode( default , set ) : Bool = false ;
function set_isInPatternMode ( value : Bool ) : Bool
{
isInPatternMode = value ;
// Make sure view is updated when we change modes.
noteDisplayDirty = true ;
notePreviewDirty = true ;
this . scrollPositionInPixels = 0 ;
return isInPatternMode ;
}
var currentPattern: String = ' ' ;
/ * *
* 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 ;
/ * *
* Whether the note preview graphic needs to be FULLY rebuilt .
* The Bitmap can be modified by individual commands without using this .
* /
var notePreviewDirty: 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
{
2023-01-22 22:25:45 -05:00
if ( value == saveDataDirty ) return value ;
2023-01-22 19:55:30 -05:00
if ( value )
{
// Start the auto-save timer.
autoSaveTimer = new FlxTimer ( ) . start ( AUTOSAVE_TIMER_DELAY , ( _ ) - > autoSave ( ) ) ;
}
e lse
{
// Stop the auto-save timer.
autoSaveTimer . cancel ( ) ;
autoSaveTimer . destroy ( ) ;
autoSaveTimer = null ;
}
return saveDataDirty = value ;
}
/ * *
* A timer used to auto - save the chart after a period of inactivity .
* /
var autoSaveTimer: FlxTimer ;
/ * *
* 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 ;
var isInPlaytestMode: Bool = false ;
/ * *
* The list of command previously performed . Used for u n d o i n g p r e v i o u s a c t i o n s .
* /
var undoHistory: Array < ChartEditorCommand > = [ ] ;
/ * *
* The list of commands that have been undone . Used for r e d o i n g p r e v i o u s a c t i o n s .
* /
var redoHistory: Array < ChartEditorCommand > = [ ] ;
/ * *
* Variable used to track how long the user has been holding the undo keybind .
* /
var undoHeldTime: Float = 0.0 ;
/ * *
* Variable used to track how long the user has been holding the redo keybind .
* /
var redoHeldTime: Float = 0.0 ;
/ * *
* Whether the undo / redo histories have changed since the last time the UI was updated .
* /
var commandHistoryDirty: Bool = true ;
/ * *
* The notes which are currently in the user ' s s e l e c t i o n .
* /
var currentNoteSelection: Array < SongNoteData > = [ ] ;
/ * *
* The events which are currently in the user ' s s e l e c t i o n .
* /
var currentEventSelection: Array < SongEventData > = [ ] ;
/ * *
* The position where the user clicked to start a selection .
* The selection box e xtends from this point to the current mouse position .
* /
var selectionBoxStartPos: FlxPoint = null ;
/ * *
* Whether the user ' s l a s t m o u s e c l i c k w a s o n t h e p l a y h e a d s c r o l l a r e a .
* /
var gridPlayheadScrollAreaPressed: Bool = false ;
/ * *
* The SongNoteData which is currently being placed .
* As the user drags , we will update this note ' s s u s t a i n l e n g t h .
* /
var currentPlaceNoteData: SongNoteData = null ;
/ * *
* The Dialog components representing the currently available tool windows .
* Dialogs are retained here even when collapsed or hidden .
* /
var activeToolboxes: Map < String , Dialog > = new Map < String , Dialog > ( ) ;
/ * *
* AUDIO AND SOUND DATA
* /
// ==============================
/ * *
* The audio track for t h e i n s t r u m e n t a l .
* /
var audioInstTrack: FlxSound ;
/ * *
* The audio track for t h e v o c a l s .
* /
var audioVocalTrackGroup: VocalGroup ;
/ * *
* A map of the audio tracks for e a c h c h a r a c t e r ' s v o c a l s .
* - Keys are the character IDs .
* - Values are the FlxSound objects to play that character ' s v o c a l s .
*
* When switching characters , the elements of the VocalGroup will be swapped to match the new character .
* /
var audioVocalTracks: Map < String , FlxSound > = new Map < String , FlxSound > ( ) ;
/ * *
* CHART DATA
* /
// ==============================
/ * *
* The song metadata .
* - Keys are the variation IDs . At least one ( ` d efault ` ) must exist .
* - Values are the relevant metadata , ready to be serialized to JSON .
* /
var songMetadata: Map < String , SongMetadata > ;
var availableVariations( get , null ) : Array < String > ;
function get_availableVariations ( ) : Array < String >
{
return [ for ( x in songMetadata . keys ( ) ) x ] ;
}
/ * *
* The song chart data .
* - Keys are the variation IDs . At least one ( ` d efault ` ) 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 t h e c u r r e n t v a r i a t i o n .
* /
var currentSongMetadata( get , set ) : SongMetadata ;
function get_currentSongMetadata ( ) : SongMetadata
{
var result = songMetadata . get ( selectedVariation ) ;
if ( result == null )
{
result = new SongMetadata ( ' D a d B a t t l e ' , ' K a w a i S p r i t e ' , 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 t h e c u r r e n t v a r i a t i o n .
* /
var currentSongChartData( get , set ) : SongChartData ;
function get_currentSongChartData ( ) : SongChartData
{
var result = songChartData . get ( selectedVariation ) ;
if ( result == null )
{
result = new SongChartData ( 1.0 , [ ] , [ ] ) ;
songChartData . set ( selectedVariation , result ) ;
}
return result ;
}
function set_currentSongChartData ( value : SongChartData ) : SongChartData
{
songChartData . set ( selectedVariation , value ) ;
return value ;
}
/ * *
* Convenience property to get ( and s e t ) the scroll speed for t h e c u r r e n t d i f f i c u l t y .
* /
var currentSongChartScrollSpeed( get , set ) : Float ;
function get_currentSongChartScrollSpeed ( ) : Float
{
var result = 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 t h e c u r r e n t d i f f i c u l t y .
* /
var currentSongChartNoteData( get , set ) : Array < SongNoteData > ;
function get_currentSongChartNoteData ( ) : Array < SongNoteData >
{
var result = currentSongChartData . notes . get ( selectedDifficulty ) ;
if ( result == null )
{
// Initialize to the default value if not set.
result = [ ] ;
currentSongChartData . notes . set ( selectedDifficulty , result ) ;
return result ;
}
return result ;
}
function set_currentSongChartNoteData ( value : Array < SongNoteData > ) : Array < SongNoteData >
{
currentSongChartData . notes . set ( selectedDifficulty , value ) ;
return value ;
}
/ * *
* Convenience property to get the event data for t h e c u r r e n t d i f f i c u l t y .
* /
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 >
{
currentSongChartData . events = value ;
return value ;
}
public var currentSongNoteSkin( get , set ) : String ;
function get_currentSongNoteSkin ( ) : String
{
if ( currentSongMetadata . playData . noteSkin == null )
{
// Initialize to the default value if not set.
currentSongMetadata . playData . noteSkin = ' N o r m a l ' ;
}
return currentSongMetadata . playData . noteSkin ;
}
function set_currentSongNoteSkin ( value : String ) : String
{
return currentSongMetadata . playData . noteSkin = 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 = ' m a i n S t a g e ' ;
}
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 = ' N e w S o n g ' ;
}
return currentSongMetadata . songName ;
}
function set_currentSongName ( value : String ) : String
{
return currentSongMetadata . songName = value ;
}
var currentSongId( get , null ) : String ;
function get_currentSongId ( ) : String
{
return currentSongName . toLowerKebabCase ( ) ;
}
var currentSongArtist( get , set ) : String ;
function get_currentSongArtist ( ) : String
{
if ( currentSongMetadata . artist == null )
{
// Initialize to the default value if not set.
currentSongMetadata . artist = ' U n k n o w n ' ;
}
return currentSongMetadata . artist ;
}
function set_currentSongArtist ( value : String ) : String
{
return currentSongMetadata . artist = value ;
}
/ * *
* RENDER OBJECTS
* /
// ==============================
/ * *
* The IMAGE used for t h e g r i d . U p d a t e d b y C h a r t E d i t o r T h e m e H a n d l e r .
* /
var gridBitmap: BitmapData ;
/ * *
* The IMAGE used for t h e s e l e c t i o n s q u a r e s . U p d a t e d b y C h a r t E d i t o r T h e m e H a n d l e r .
* 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 - s l i c e s p r i t e f o r t h e s e l e c t i o n b o x .
* /
var selectionSquareBitmap: 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: FlxSprite ;
/ * *
* The playhead representing the current position in the song .
* Can move around on the grid independently of the view .
* /
var gridPlayhead: FlxSpriteGroup ;
var gridPlayheadScrollArea: FlxSprite ;
/ * *
* A sprite used to indicate the note that will be placed on click .
* /
var gridGhostNote: ChartEditorNoteSprite ;
/ * *
* A sprite used to indicate the event that will be placed on click .
* /
var gridGhostEvent: ChartEditorEventSprite ;
/ * *
* The waveform which ( optionally ) displays over the grid , underneath the notes and playhead .
* /
var gridSpectrogram: PolygonSpectogram ;
/ * *
* The rectangle used for t h e n o t e p r e v i e w a r e a .
* Should span the full height of the song . We scribble on this to draw the preview .
* /
var notePreviewBitmap: BitmapData ;
/ * *
* The sprite used to display the note preview area .
* We move this up and down to scroll the preview .
* /
var notePreviewSprite: FlxSprite ;
/ * *
* The rectangular sprite used for r e n d e r i n g t h e s e l e c t i o n b o x .
* Uses a 9 - slice to stretch the selection box to the correct size without warping .
* /
var selectionBoxSprite: FlxSliceSprite ;
/ * *
* The opponent ' s h e a l t h i c o n .
* /
var healthIconDad: HealthIcon ;
/ * *
* The player ' s h e a l t h i c o n .
* /
var healthIconBF: HealthIcon ;
/ * *
* The purple background sprite .
* /
var menuBG: FlxSprite ;
/ * *
* 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 > ;
/ * *
* 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 > ;
var renderedSelectionSquares: FlxTypedSpriteGroup < FlxSprite > ;
var playbarHead: Slider ;
public function n e w ( )
{
// Load the HaxeUI XML file.
super ( CHART_EDITOR_LAYOUT ) ;
}
override function create ( )
{
// Get rid of any music from the previous state.
FlxG . sound . music . stop ( ) ;
buildDefaultSongData ( ) ;
buildBackground ( ) ;
currentTheme = ChartEditorTheme . Light ;
buildGrid ( ) ;
buildSelectionBox ( ) ;
// Add the HaxeUI components after the grid so they're on top.
super . create ( ) ;
buildAdditionalUI ( ) ;
// Setup the onClick listeners for the UI after it's been created.
setupUIListeners ( ) ;
setupAutoSave ( ) ;
// TODO: We should be loading the music later when the user requests it.
// loadDefaultMusic();
// TODO: Change to false.
var canCloseInitialDialog = true ;
ChartEditorDialogHandler . openWelcomeDialog ( this , canCloseInitialDialog ) ;
}
function buildDefaultSongData ( )
{
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 > ( ) ;
audioVocalTrackGroup = new VocalGroup ( ) ;
}
/ * *
* Builds and displays the background sprite .
* /
function buildBackground ( )
{
menuBG = new FlxSprite ( ) . loadGraphic ( Paths . image ( ' m e n u D e s a t ' ) ) ;
add ( menuBG ) ;
menuBG . setGraphicSize ( Std . int ( menuBG . width * 1.1 ) ) ;
menuBG . updateHitbox ( ) ;
menuBG . screenCenter ( ) ;
menuBG . scrollFactor . set ( 0 , 0 ) ;
}
/ * *
* Builds and displays the chart editor grid , including the playhead and cursor .
* /
function buildGrid ( )
{
gridTiledSprite = new FlxTiledSprite ( gridBitmap , gridBitmap . width , 1000 , false , true ) ;
gridTiledSprite . x = FlxG . width / 2 - GRID_SIZE * STRUMLINE_SIZE ; // Center the grid.
gridTiledSprite . y = MENU_BAR_HEIGHT + GRID_TOP_PAD ; // Push down to account for the menu bar.
add ( gridTiledSprite ) ;
gridGhostNote = new ChartEditorNoteSprite ( this ) ;
gridGhostNote . alpha = 0.6 ;
gridGhostNote . noteData = new SongNoteData ( - 1 , - 1 , 0 , " " ) ;
gridGhostNote . visible = false ;
add ( gridGhostNote ) ;
gridGhostEvent = new ChartEditorEventSprite ( this ) ;
gridGhostEvent . alpha = 0.6 ;
gridGhostEvent . eventData = new SongEventData ( - 1 , " " , { } ) ;
gridGhostEvent . visible = false ;
add ( gridGhostEvent ) ;
buildNoteGroup ( ) ;
gridPlayheadScrollArea = new FlxSprite ( gridTiledSprite . x - PLAYHEAD_SCROLL_AREA_WIDTH ,
MENU_BAR_HEIGHT ) . makeGraphic ( PLAYHEAD_SCROLL_AREA_WIDTH , FlxG . height - MENU_BAR_HEIGHT , PLAYHEAD_SCROLL_AREA_COLOR ) ;
add ( gridPlayheadScrollArea ) ;
// The playhead that show the current position in the song.
gridPlayhead = new FlxSpriteGroup ( ) ;
add ( gridPlayhead ) ;
var playheadWidth = GRID_SIZE * ( STRUMLINE_SIZE * 2 + 1 ) + ( PLAYHEAD_SCROLL_AREA_WIDTH * 2 ) ;
var playheadBaseYPos = MENU_BAR_HEIGHT + GRID_TOP_PAD ;
gridPlayhead . setPosition ( gridTiledSprite . x , playheadBaseYPos ) ;
var playheadSprite = new FlxSprite ( ) . makeGraphic ( playheadWidth , PLAYHEAD_HEIGHT , PLAYHEAD_COLOR ) ;
playheadSprite . x = - PLAYHEAD_SCROLL_AREA_WIDTH ;
playheadSprite . y = 0 ;
gridPlayhead . add ( playheadSprite ) ;
var playheadBlock = ChartEditorThemeHandler . buildPlayheadBlock ( ) ;
playheadBlock . x = - PLAYHEAD_SCROLL_AREA_WIDTH ;
playheadBlock . y = - PLAYHEAD_HEIGHT / 2 ;
gridPlayhead . add ( playheadBlock ) ;
// Character icons.
healthIconDad = new HealthIcon ( ' d a d ' ) ;
healthIconDad . autoUpdate = false ;
healthIconDad . size . set ( 0.5 , 0.5 ) ;
healthIconDad . x = gridTiledSprite . x - 15 - ( HealthIcon . HEALTH_ICON_SIZE * 0.5 ) ;
healthIconDad . y = gridTiledSprite . y + 5 ;
add ( healthIconDad ) ;
healthIconBF = new HealthIcon ( ' b f ' ) ;
healthIconBF . autoUpdate = false ;
healthIconBF . size . set ( 0.5 , 0.5 ) ;
healthIconBF . x = gridTiledSprite . x + GRID_SIZE * ( STRUMLINE_SIZE * 2 + 1 ) + 15 ;
healthIconBF . y = gridTiledSprite . y + 5 ;
healthIconBF . flipX = true ;
add ( healthIconBF ) ;
}
function buildSelectionBox ( )
{
selectionBoxSprite . scrollFactor . set ( 0 , 0 ) ;
add ( selectionBoxSprite ) ;
setSelectionBoxBounds ( ) ;
}
function setSelectionBoxBounds ( ? bounds : FlxRect = null )
{
if ( bounds == null )
{
selectionBoxSprite . visible = false ;
selectionBoxSprite . x = - 9999 ;
selectionBoxSprite . y = - 9999 ;
}
e lse
{
selectionBoxSprite . visible = true ;
selectionBoxSprite . x = bounds . x ;
selectionBoxSprite . y = bounds . y ;
selectionBoxSprite . width = bounds . width ;
selectionBoxSprite . height = bounds . height ;
}
}
function buildSpectrogram ( target : FlxSound )
{
gridSpectrogram = new PolygonSpectogram ( target , SPECTROGRAM_COLOR , FlxG . height / 2 , Math . floor ( FlxG . height / 2 ) ) ;
// Halfway through the grid.
// gridSpectrogram.x = gridTiledSprite.x + STRUMLINE_SIZE * GRID_SIZE;
// gridSpectrogram.y = gridTiledSprite.y;
gridSpectrogram . x = 200 ;
gridSpectrogram . y = 200 ;
gridSpectrogram . visType = STATIC ; // We move the spectrogram manually.
gridSpectrogram . waveAmplitude = 50 ;
gridSpectrogram . scrollFactor . set ( 0 , 0 ) ;
add ( gridSpectrogram ) ;
}
/ * *
* Builds the group that will hold all the notes .
* /
function buildNoteGroup ( )
{
renderedNotes = new FlxTypedSpriteGroup < ChartEditorNoteSprite > ( ) ;
renderedNotes . setPosition ( gridTiledSprite . x , gridTiledSprite . y ) ;
add ( renderedNotes ) ;
renderedEvents = new FlxTypedSpriteGroup < ChartEditorEventSprite > ( ) ;
renderedEvents . setPosition ( gridTiledSprite . x , gridTiledSprite . y ) ;
add ( renderedEvents ) ;
renderedSelectionSquares = new FlxTypedSpriteGroup < FlxSprite > ( ) ;
renderedSelectionSquares . setPosition ( gridTiledSprite . x , gridTiledSprite . y ) ;
add ( renderedSelectionSquares ) ;
}
var playbarHeadLayout: Component ;
function buildAdditionalUI ( ) : Void
{
playbarHeadLayout = buildComponent ( CHART_EDITOR_PLAYBARHEAD_LAYOUT ) ;
playbarHeadLayout . width = FlxG . width - 8 ;
playbarHeadLayout . height = 10 ;
playbarHeadLayout . x = 4 ;
playbarHeadLayout . y = FlxG . height - 48 - 8 ;
playbarHead = playbarHeadLayout . findComponent ( ' p l a y b a r H e a d ' , Slider ) ;
playbarHead . allowFocus = false ;
playbarHead . width = FlxG . width ;
playbarHead . height = 10 ;
playbarHead . styleString = " p a d d i n g - l e f t : 0 p x ; p a d d i n g - r i g h t : 0 p x ; b o r d e r - l e f t : 0 p x ; b o r d e r - r i g h t : 0 p x ; " ;
2023-02-02 18:26:03 -05:00
playbarHead . onDragStart = function ( _ : DragEvent ) {
2023-01-22 19:55:30 -05:00
playbarHeadDragging = true ;
// If we were dragging the playhead while the song was playing, resume playing.
if ( audioInstTrack != null && audioInstTrack . playing )
{
playbarHeadDraggingWasPlaying = true ;
stopAudioPlayback ( ) ;
}
e lse
{
playbarHeadDraggingWasPlaying = false ;
}
}
2023-02-02 18:26:03 -05:00
playbarHead . onDragEnd = function ( _ : DragEvent ) {
2023-01-22 19:55:30 -05:00
playbarHeadDragging = false ;
// Set the song position to where the playhead was moved to.
scrollPositionInPixels = songLengthInPixels * ( playbarHead . value / 100 ) ;
// Update the conductor and audio tracks to match.
moveSongToScrollPosition ( ) ;
// If we were dragging the playhead while the song was playing, resume playing.
if ( playbarHeadDraggingWasPlaying )
{
playbarHeadDraggingWasPlaying = false ;
startAudioPlayback ( ) ;
}
}
2023-02-02 18:26:03 -05:00
add ( playbarHeadLayout ) ;
2023-01-22 19:55:30 -05:00
}
/ * *
* Sets up the onClick listeners for t h e U I .
* /
function setupUIListeners ( ) : Void
{
// Add functionality to the playbar.
addUIClickListener ( ' p l a y b a r P l a y ' , ( event : MouseEvent ) - > toggleAudioPlayback ( ) ) ;
addUIClickListener ( ' p l a y b a r S t a r t ' , ( event : MouseEvent ) - > playbarButtonPressed = ' p l a y b a r S t a r t ' ) ;
addUIClickListener ( ' p l a y b a r B a c k ' , ( event : MouseEvent ) - > playbarButtonPressed = ' p l a y b a r B a c k ' ) ;
addUIClickListener ( ' p l a y b a r F o r w a r d ' , ( event : MouseEvent ) - > playbarButtonPressed = ' p l a y b a r F o r w a r d ' ) ;
addUIClickListener ( ' p l a y b a r E n d ' , ( event : MouseEvent ) - > playbarButtonPressed = ' p l a y b a r E n d ' ) ;
// Add functionality to the menu items.
addUIClickListener ( ' m e n u b a r I t e m N e w C h a r t ' , ( event : MouseEvent ) - > ChartEditorDialogHandler . openWelcomeDialog ( this , true ) ) ;
addUIClickListener ( ' m e n u b a r I t e m S a v e C h a r t A s ' , ( event : MouseEvent ) - > exportAllSongData ( ) ) ;
addUIClickListener ( ' m e n u b a r I t e m L o a d I n s t ' , ( event : MouseEvent ) - > ChartEditorDialogHandler . openUploadInstDialog ( this , true ) ) ;
addUIClickListener ( ' m e n u b a r I t e m U n d o ' , ( event : MouseEvent ) - > undoLastCommand ( ) ) ;
addUIClickListener ( ' m e n u b a r I t e m R e d o ' , ( event : MouseEvent ) - > redoLastCommand ( ) ) ;
2023-02-02 18:26:03 -05:00
addUIClickListener ( ' m e n u b a r I t e m C o p y ' , ( event : MouseEvent ) - > {
2023-01-22 19:55:30 -05:00
// Doesn't use a command because it's not undoable.
2023-01-22 22:25:45 -05:00
SongDataUtils . writeItemsToClipboard (
{
notes : SongDataUtils . buildNoteClipboard ( currentNoteSelection ) ,
events : SongDataUtils . buildEventClipboard ( currentEventSelection ) ,
} ) ;
2023-01-22 19:55:30 -05:00
} ) ;
2023-02-02 18:26:03 -05:00
addUIClickListener ( ' m e n u b a r I t e m C u t ' , ( event : MouseEvent ) - > {
2023-01-22 19:55:30 -05:00
performCommand ( new CutItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIClickListener ( ' m e n u b a r I t e m P a s t e ' , ( event : MouseEvent ) - > {
2023-01-22 19:55:30 -05:00
performCommand ( new PasteItemsCommand ( scrollPositionInMs + playheadPositionInMs ) ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIClickListener ( ' m e n u b a r I t e m D e l e t e ' , ( event : MouseEvent ) - > {
2023-01-22 19:55:30 -05:00
if ( currentNoteSelection . length > 0 && currentEventSelection . length > 0 )
{
performCommand ( new RemoveItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
}
e lse if ( currentNoteSelection . length > 0 )
{
performCommand ( new RemoveNotesCommand ( currentNoteSelection ) ) ;
}
e lse if ( currentEventSelection . length > 0 )
{
performCommand ( new RemoveEventsCommand ( currentEventSelection ) ) ;
}
e lse
{
// Do nothing???
}
} ) ;
2023-02-02 18:26:03 -05:00
addUIClickListener ( ' m e n u b a r I t e m S e l e c t A l l ' , ( event : MouseEvent ) - > {
2023-01-22 19:55:30 -05:00
performCommand ( new SelectAllItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIClickListener ( ' m e n u b a r I t e m S e l e c t I n v e r s e ' , ( event : MouseEvent ) - > {
2023-01-22 19:55:30 -05:00
performCommand ( new InvertSelectedItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIClickListener ( ' m e n u b a r I t e m S e l e c t N o n e ' , ( event : MouseEvent ) - > {
2023-01-22 19:55:30 -05:00
performCommand ( new DeselectAllItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
} ) ;
2023-01-22 22:25:45 -05:00
addUIClickListener ( ' m e n u b a r I t e m S e l e c t R e g i o n ' , ( event : MouseEvent ) - >
{
// TODO: Implement this.
} ) ;
2023-01-22 19:55:30 -05:00
2023-01-22 22:25:45 -05:00
addUIClickListener ( ' m e n u b a r I t e m S e l e c t B e f o r e C u r s o r ' , ( event : MouseEvent ) - >
{
// TODO: Implement this.
} ) ;
2023-01-22 19:55:30 -05:00
2023-01-22 22:25:45 -05:00
addUIClickListener ( ' m e n u b a r I t e m S e l e c t A f t e r C u r s o r ' , ( event : MouseEvent ) - >
{
// TODO: Implement this.
} ) ;
2023-01-22 19:55:30 -05:00
addUIClickListener ( ' m e n u b a r I t e m A b o u t ' , ( event : MouseEvent ) - > ChartEditorDialogHandler . openAboutDialog ( this ) ) ;
addUIClickListener ( ' m e n u b a r I t e m U s e r G u i d e ' , ( event : MouseEvent ) - > ChartEditorDialogHandler . openUserGuideDialog ( this ) ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m D o w n s c r o l l ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
isViewDownscroll = event . value ;
} ) ;
setUICheckboxSelected ( ' m e n u b a r I t e m D o w n s c r o l l ' , isViewDownscroll ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u B a r I t e m T h e m e L i g h t ' , ( event : UIEvent ) - > {
2023-01-22 22:25:45 -05:00
if ( event . target . value ) currentTheme = ChartEditorTheme . Light ;
2023-01-22 19:55:30 -05:00
} ) ;
setUICheckboxSelected ( ' m e n u B a r I t e m T h e m e L i g h t ' , currentTheme == ChartEditorTheme . Light ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u B a r I t e m T h e m e D a r k ' , ( event : UIEvent ) - > {
2023-01-22 22:25:45 -05:00
if ( event . target . value ) currentTheme = ChartEditorTheme . Dark ;
2023-01-22 19:55:30 -05:00
} ) ;
setUICheckboxSelected ( ' m e n u B a r I t e m T h e m e D a r k ' , currentTheme == ChartEditorTheme . Dark ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m M e t r o n o m e E n a b l e d ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
shouldPlayMetronome = event . value ;
} ) ;
setUICheckboxSelected ( ' m e n u b a r I t e m M e t r o n o m e E n a b l e d ' , shouldPlayMetronome ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m P l a y e r H i t s o u n d s ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
hitsoundsEnabledPlayer = event . value ;
} ) ;
setUICheckboxSelected ( ' m e n u b a r I t e m P l a y e r H i t s o u n d s ' , hitsoundsEnabledPlayer ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m O p p o n e n t H i t s o u n d s ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
hitsoundsEnabledOpponent = event . value ;
} ) ;
setUICheckboxSelected ( ' m e n u b a r I t e m O p p o n e n t H i t s o u n d s ' , hitsoundsEnabledOpponent ) ;
var instVolumeLabel: Label = findComponent ( ' m e n u b a r L a b e l V o l u m e I n s t r u m e n t a l ' , Label ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m V o l u m e I n s t r u m e n t a l ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
var volume: Float = event . value / 100.0 ;
2023-01-22 22:25:45 -05:00
if ( audioInstTrack != null ) audioInstTrack . volume = volume ;
2023-01-22 19:55:30 -05:00
instVolumeLabel . text = ' I n s t r u m e n t a l - ${ Std . int ( event . value ) } % ' ;
} ) ;
var vocalsVolumeLabel: Label = findComponent ( ' m e n u b a r L a b e l V o l u m e V o c a l s ' , Label ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m V o l u m e V o c a l s ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
var volume: Float = event . value / 100.0 ;
2023-01-22 22:25:45 -05:00
if ( audioVocalTrackGroup != null ) audioVocalTrackGroup . volume = volume ;
2023-01-22 19:55:30 -05:00
vocalsVolumeLabel . text = ' V o c a l s - ${ Std . int ( event . value ) } % ' ;
} ) ;
var playbackSpeedLabel: Label = findComponent ( ' m e n u b a r L a b e l P l a y b a c k S p e e d ' , Label ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m P l a y b a c k S p e e d ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
var pitch = event . value * 2.0 / 100.0 ;
#if FLX_PITCH
2023-01-22 22:25:45 -05:00
if ( audioInstTrack != null ) audioInstTrack . pitch = pitch ;
if ( audioVocalTrackGroup != null ) audioVocalTrackGroup . pitch = pitch ;
2023-01-22 19:55:30 -05:00
#end
2023-02-28 13:17:28 -05:00
playbackSpeedLabel . text = ' P l a y b a c k S p e e d - ${ Std . int ( pitch * 100 ) / 100 } x ' ;
2023-01-22 19:55:30 -05:00
} ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m T o g g l e T o o l b o x T o o l s ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
ChartEditorToolboxHandler . setToolboxState ( this , CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT , event . value ) ;
} ) ;
// setUICheckboxSelected('menubarItemToggleToolboxTools', true);
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m T o g g l e T o o l b o x N o t e s ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
ChartEditorToolboxHandler . setToolboxState ( this , CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT , event . value ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m T o g g l e T o o l b o x E v e n t s ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
ChartEditorToolboxHandler . setToolboxState ( this , CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT , event . value ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m T o g g l e T o o l b o x D i f f i c u l t y ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
ChartEditorToolboxHandler . setToolboxState ( this , CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT , event . value ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m T o g g l e T o o l b o x M e t a d a t a ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
ChartEditorToolboxHandler . setToolboxState ( this , CHART_EDITOR_TOOLBOX_METADATA_LAYOUT , event . value ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m T o g g l e T o o l b o x C h a r a c t e r s ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
ChartEditorToolboxHandler . setToolboxState ( this , CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT , event . value ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m T o g g l e T o o l b o x P l a y e r P r e v i e w ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
ChartEditorToolboxHandler . setToolboxState ( this , CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT , event . value ) ;
} ) ;
2023-02-02 18:26:03 -05:00
addUIChangeListener ( ' m e n u b a r I t e m T o g g l e T o o l b o x O p p o n e n t P r e v i e w ' , ( event : UIEvent ) - > {
2023-01-22 19:55:30 -05:00
ChartEditorToolboxHandler . setToolboxState ( this , CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT , event . value ) ;
} ) ;
// TODO: Pass specific HaxeUI components to add context menus to them.
registerContextMenu ( null , Paths . ui ( ' c h a r t - e d i t o r / c o n t e x t / t e s t ' ) ) ;
}
/ * *
* Setup timers and listerners to handle auto - save .
* /
function setupAutoSave ( )
{
WindowUtil . windowExit . add ( onWindowClose ) ;
saveDataDirty = false ;
}
/ * *
* Called after 5 minutes without saving .
* /
function autoSave ( )
{
saveDataDirty = false ;
// Auto-save the chart.
#if html5
// Auto-save to local storage.
#else
// Auto-save to temp file.
exportAllSongData ( true , true ) ;
#end
}
2023-02-28 13:17:28 -05:00
function onWindowClose ( exitCode : Int ) : Void
2023-01-22 19:55:30 -05:00
{
trace ( ' W i n d o w e x i t e d w i t h e x i t c o d e : $ exitCode ' ) ;
trace ( ' S h o u l d s a v e c h a r t ? $ saveDataDirty ' ) ;
if ( saveDataDirty )
{
exportAllSongData ( true ) ;
}
}
2023-02-28 13:17:28 -05:00
function cleanupAutoSave ( ) : Void
2023-01-22 19:55:30 -05:00
{
WindowUtil . windowExit . remove ( onWindowClose ) ;
}
2023-02-28 13:17:28 -05:00
public override function update ( elapsed : Float ) : Void
2023-01-22 19:55:30 -05:00
{
// dispatchEvent gets called here.
super . update ( elapsed ) ;
FlxG . mouse . visible = true ;
// These ones happen even if the modal dialog is open.
handleMusicPlayback ( ) ;
handleNoteDisplay ( ) ;
// These ones only happen if the modal dialog is not open.
handleScrollKeybinds ( ) ;
// handleZoom();
// handleSnap();
handleCursor ( ) ;
handleMenubar ( ) ;
handleToolboxes ( ) ;
handlePlaybar ( ) ;
handlePlayhead ( ) ;
handleFileKeybinds ( ) ;
handleEditKeybinds ( ) ;
handleViewKeybinds ( ) ;
handleHelpKeybinds ( ) ;
// DEBUG
#if debug
if ( FlxG . keys . justPressed . F )
{
2023-02-28 13:17:28 -05:00
NotificationManager . instance . addNotification (
{
title : ' T h i s i s a N o t i f i c a t i o n ' ,
body : ' H e l l o , w o r l d ! ' ,
type : NotificationType . Info ,
expiryMs : NOTIFICATION_DISMISS_TIME
// styleNames: 'cssStyleName',
// icon: 'assetPath',
// actions: ['action1', 'action2']
} ) ;
2023-01-22 19:55:30 -05:00
}
if ( FlxG . keys . justPressed . E )
{
currentSongMetadata . timeChanges [ 0 ] . timeSignatureNum = ( currentSongMetadata . timeChanges [ 0 ] . timeSignatureNum == 4 ? 3 : 4 ) ;
}
#end
// Right align the BF health icon.
// Base X position to the right of the grid.
var baseHealthIconXPos = gridTiledSprite . x + GRID_SIZE * ( STRUMLINE_SIZE * 2 + 1 ) + 15 ;
// Will be 0 when not bopping. When bopping, will increase to push the icon left.
var healthIconOffset = healthIconBF . width - ( HealthIcon . HEALTH_ICON_SIZE * 0.5 ) ;
healthIconBF . x = baseHealthIconXPos - healthIconOffset ;
}
/ * *
* Beat hit while t h e s o n g i s p l a y i n g .
* /
override function beatHit ( ) : Bool
{
// dispatchEvent gets called here.
2023-01-22 22:25:45 -05:00
if ( ! super . beatHit ( ) ) return false ;
2023-01-22 19:55:30 -05:00
2023-02-28 13:17:28 -05:00
if ( shouldPlayMetronome && ( audioInstTrack != null && audioInstTrack . playing ) )
2023-01-22 19:55:30 -05:00
{
playMetronomeTick ( Conductor . currentBeat % 4 == 0 ) ;
}
return true ;
}
/ * *
* Step hit while t h e s o n g i s p l a y i n g .
* /
override function stepHit ( ) : Bool
{
// dispatchEvent gets called here.
2023-01-22 22:25:45 -05:00
if ( ! super . stepHit ( ) ) return false ;
2023-01-22 19:55:30 -05:00
2023-02-28 13:17:28 -05:00
if ( audioInstTrack != null && audioInstTrack . playing )
2023-01-22 19:55:30 -05:00
{
healthIconDad . onStepHit ( Conductor . currentStep ) ;
healthIconBF . onStepHit ( Conductor . currentStep ) ;
}
// if (shouldPlayMetronome)
// playMetronomeTick(false);
return true ;
}
/ * *
* Handle keybinds for s c r o l l i n g t h e c h a r t e d i t o r g r i d .
* * /
2023-02-28 13:17:28 -05:00
function handleScrollKeybinds ( ) : Void
2023-01-22 19:55:30 -05:00
{
// Don't scroll when the cursor is over the UI.
2023-01-22 22:25:45 -05:00
if ( isCursorOverHaxeUI ) return ;
2023-01-22 19:55:30 -05:00
// Amount to scroll the grid.
var scrollAmount: Float = 0 ;
// Amount to scroll the playhead relative to the grid.
var playheadAmount: Float = 0 ;
2023-02-28 13:17:28 -05:00
var shouldPause: Bool = false ;
2023-01-22 19:55:30 -05:00
// Up Arrow = Scroll Up
if ( FlxG . keys . justPressed . UP )
{
2023-02-28 13:17:28 -05:00
scrollAmount = - GRID_SIZE * 0.25 * 5 ;
2023-01-22 19:55:30 -05:00
}
// Down Arrow = Scroll Down
if ( FlxG . keys . justPressed . DOWN )
{
2023-02-28 13:17:28 -05:00
scrollAmount = GRID_SIZE * 0.25 * 5 ;
2023-01-22 19:55:30 -05:00
}
// PAGE UP = Jump Up 1 Measure
if ( FlxG . keys . justPressed . PAGEUP )
{
scrollAmount = - GRID_SIZE * 4 * Conductor . beatsPerMeasure ;
}
if ( playbarButtonPressed == ' p l a y b a r B a c k ' )
{
playbarButtonPressed = ' ' ;
scrollAmount = - GRID_SIZE * 4 * Conductor . beatsPerMeasure ;
}
// PAGE DOWN = Jump Down 1 Measure
if ( FlxG . keys . justPressed . PAGEDOWN )
{
scrollAmount = GRID_SIZE * 4 * Conductor . beatsPerMeasure ;
}
if ( playbarButtonPressed == ' p l a y b a r F o r w a r d ' )
{
playbarButtonPressed = ' ' ;
scrollAmount = GRID_SIZE * 4 * Conductor . beatsPerMeasure ;
}
// Mouse Wheel = Scroll
if ( FlxG . mouse . wheel != 0 && ! FlxG . keys . pressed . CONTROL )
{
scrollAmount = - 10 * FlxG . mouse . wheel ;
}
// Middle Mouse + Drag = Scroll but move the playhead the same amount.
if ( FlxG . mouse . pressedMiddle )
{
if ( FlxG . mouse . deltaY != 0 )
{
// Scroll down by the amount dragged.
scrollAmount += - FlxG . mouse . deltaY ;
// Move the playhead by the same amount in the other direction so it is stationary.
playheadAmount += FlxG . mouse . deltaY ;
}
}
// SHIFT + Scroll = Scroll Fast
if ( FlxG . keys . pressed . SHIFT )
{
scrollAmount *= 5 ;
}
// CONTROL + Scroll = Scroll Precise
if ( FlxG . keys . pressed . CONTROL )
{
scrollAmount /= 10 ;
}
// ALT = Move playhead instead.
if ( FlxG . keys . pressed . ALT )
{
playheadAmount = scrollAmount ;
scrollAmount = 0 ;
}
// 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 ;
}
if ( playbarButtonPressed == ' p l a y b a r S t a r t ' )
{
playbarButtonPressed = ' ' ;
scrollAmount = 0 - this . scrollPositionInPixels ;
playheadAmount = 0 - this . playheadPositionInPixels ;
}
// 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 ;
}
if ( playbarButtonPressed == ' p l a y b a r E n d ' )
{
playbarButtonPressed = ' ' ;
scrollAmount = this . songLengthInPixels - this . scrollPositionInPixels ;
}
// Apply the scroll amount.
this . scrollPositionInPixels += scrollAmount ;
this . playheadPositionInPixels += playheadAmount ;
// Resync the conductor and audio tracks.
2023-01-22 22:25:45 -05:00
if ( scrollAmount != 0 || playheadAmount != 0 ) moveSongToScrollPosition ( ) ;
2023-01-22 19:55:30 -05:00
}
function handleZoom ( )
{
if ( FlxG . keys . justPressed . MINUS )
{
currentZoomLevel /= 2 ;
// Update the grid.
ChartEditorThemeHandler . updateTheme ( this ) ;
// Update the note positions.
noteDisplayDirty = true ;
}
if ( FlxG . keys . justPressed . PLUS )
{
currentZoomLevel *= 2 ;
// Update the grid.
ChartEditorThemeHandler . updateTheme ( this ) ;
// Update the note positions.
noteDisplayDirty = true ;
}
}
function handleSnap ( )
{
if ( FlxG . keys . justPressed . LEFT )
{
noteSnapQuantIndex -- ;
}
if ( FlxG . keys . justPressed . RIGHT )
{
noteSnapQuantIndex ++ ;
}
}
/ * *
* Handle display of the mouse cursor .
* /
function handleCursor ( )
{
// Note: If a menu is open in HaxeUI, don't handle cursor behavior.
var shouldHandleCursor = ! isCursorOverHaxeUI || ( selectionBoxStartPos != null ) ;
var eventColumn = ( STRUMLINE_SIZE * 2 + 1 ) - 1 ;
if ( shouldHandleCursor )
{
var overlapsGrid: Bool = FlxG . mouse . overlaps ( gridTiledSprite ) ;
// Cursor position relative to the grid.
var cursorX: Float = FlxG . mouse . screenX - gridTiledSprite . x ;
var cursorY: Float = FlxG . mouse . screenY - gridTiledSprite . y ;
var overlapsSelectionBorder = 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 ) ) ;
if ( FlxG . mouse . justPressed )
{
if ( FlxG . mouse . overlaps ( gridPlayheadScrollArea ) )
{
gridPlayheadScrollAreaPressed = true ;
}
e lse if ( ! overlapsGrid || overlapsSelectionBorder )
{
selectionBoxStartPos = new FlxPoint ( FlxG . mouse . screenX , FlxG . mouse . screenY ) ;
}
}
if ( gridPlayheadScrollAreaPressed )
{
Cursor . cursorMode = Grabbing ;
}
e lse if ( FlxG . mouse . overlaps ( gridPlayheadScrollArea ) )
{
Cursor . cursorMode = Pointer ;
}
e lse
{
Cursor . cursorMode = Default ;
}
if ( gridPlayheadScrollAreaPressed && FlxG . mouse . released )
{
gridPlayheadScrollAreaPressed = false ;
}
if ( gridPlayheadScrollAreaPressed )
{
// Clicked on the playhead scroll area.
// Move the playhead to the cursor position.
this . playheadPositionInPixels = FlxG . mouse . screenY - MENU_BAR_HEIGHT - GRID_TOP_PAD ;
moveSongToScrollPosition ( ) ;
}
// Cursor position snapped to the grid.
// The song position of the cursor, in steps.
var cursorFractionalStep: Float = cursorY / GRID_SIZE / ( 16 / noteSnapQuant ) ;
var cursorStep: Int = Std . int ( Math . floor ( cursorFractionalStep ) ) ;
var cursorMs: Float = cursorStep * Conductor . stepCrochet * ( 16 / noteSnapQuant ) ;
// The direction value for the column at the cursor.
var cursorColumn: Int = Math . floor ( cursorX / GRID_SIZE ) ;
2023-01-22 22:25:45 -05:00
if ( cursorColumn < 0 ) cursorColumn = 0 ;
2023-01-22 19:55:30 -05:00
if ( cursorColumn >= ( STRUMLINE_SIZE * 2 + 1 - 1 ) )
{
// Don't invert the event column.
cursorColumn = ( STRUMLINE_SIZE * 2 + 1 - 1 ) ;
}
e lse
{
// Invert player and opponent columns.
if ( cursorColumn >= STRUMLINE_SIZE )
{
cursorColumn -= STRUMLINE_SIZE ;
}
e lse
{
cursorColumn += STRUMLINE_SIZE ;
}
}
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 = cursorStepStart * Conductor . stepCrochet ;
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 ) ) ;
2023-02-02 18:26:03 -05:00
var columns: Array < Int > = [ for ( i in columnStart ... ( columnEnd + 1 ) ) i ] . map ( function ( i : Int ) : Int {
2023-01-22 19:55:30 -05:00
if ( i >= eventColumn )
{
// Don't invert the event column.
return eventColumn ;
}
e lse if ( i >= STRUMLINE_SIZE )
{
// Invert the player columns.
return i - STRUMLINE_SIZE ;
}
e lse if ( i >= 0 )
{
// Invert the opponent columns.
return i + STRUMLINE_SIZE ;
}
e lse
{
// 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 ) ) ;
}
e lse
{
// Set the selection.
performCommand ( new SetItemSelectionCommand ( notesToSelect , eventsToSelect , currentNoteSelection , currentEventSelection ) ) ;
}
}
e lse
{
// We made a selection box, but it didn't select anything.
}
}
e lse
{
// We made a selection box, but it didn't select any columns.
}
// Clear the selection box.
selectionBoxStartPos = null ;
setSelectionBoxBounds ( ) ;
}
e lse
{
// Render the selection box.
var selectionRect = 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 ) ;
}
}
e lse 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.
2023-02-02 18:26:03 -05:00
var highlightedNote: ChartEditorNoteSprite = renderedNotes . members . find ( function ( note : ChartEditorNoteSprite ) : Bool {
2023-01-22 19:55:30 -05:00
// If note.alive is false, the note is dead and awaiting recycling.
return note . alive && FlxG . mouse . overlaps ( note ) ;
} ) ;
var highlightedEvent: ChartEditorEventSprite = null ;
if ( highlightedNote == null )
{
2023-02-02 18:26:03 -05:00
highlightedEvent = renderedEvents . members . find ( function ( event : ChartEditorEventSprite ) : Bool {
2023-01-22 19:55:30 -05:00
return event . alive && FlxG . mouse . overlaps ( event ) ;
} ) ;
}
if ( FlxG . keys . pressed . CONTROL )
{
if ( highlightedNote != null )
{
// Handle the case of clicking on a sustain piece.
highlightedNote = highlightedNote . getBaseNoteSprite ( ) ;
// Control click to select/deselect an individual note.
if ( isNoteSelected ( highlightedNote . noteData ) )
{
performCommand ( new DeselectItemsCommand ( [ highlightedNote . noteData ] , [ ] ) ) ;
}
e lse
{
performCommand ( new SelectItemsCommand ( [ highlightedNote . noteData ] , [ ] ) ) ;
}
}
e lse if ( highlightedEvent != null )
{
// Control click to select/deselect an individual note.
if ( isEventSelected ( highlightedEvent . eventData ) )
{
performCommand ( new DeselectItemsCommand ( [ ] , [ highlightedEvent . eventData ] ) ) ;
}
e lse
{
performCommand ( new SelectItemsCommand ( [ ] , [ highlightedEvent . eventData ] ) ) ;
}
}
e lse
{
// Do nothing if you control-clicked on an empty space.
}
}
e lse
{
if ( highlightedNote != null )
{
// Click a note to select it.
performCommand ( new SetItemSelectionCommand ( [ highlightedNote . noteData ] , [ ] , currentNoteSelection , currentEventSelection ) ) ;
}
e lse if ( highlightedEvent != null )
{
// Click an event to select it.
performCommand ( new SetItemSelectionCommand ( [ ] , [ highlightedEvent . eventData ] , currentNoteSelection , currentEventSelection ) ) ;
}
e lse
{
// Click on an empty space to deselect everything.
performCommand ( new DeselectAllItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
}
}
}
e lse
{
// If we clicked and released outside the grid, do nothing.
}
}
}
e lse if ( currentPlaceNoteData != null )
{
// Handle extending the note as you drag.
// Since use Math.floor and stepCrochet here, the hold notes will be beat snapped.
var dragLengthSteps: Float = Math . floor ( ( cursorMs - currentPlaceNoteData . time ) / Conductor . stepCrochet ) ;
// Without this, the newly placed note feels too short compared to the user's input.
var INCREMENT: Float = 1.0 ;
var dragLengthMs: Float = ( dragLengthSteps + INCREMENT ) * Conductor . stepCrochet ;
// TODO: Add and update some sort of preview?
if ( FlxG . mouse . justReleased )
{
if ( dragLengthSteps > 0 )
{
// Apply the new length.
performCommand ( new ExtendNoteLengthCommand ( currentPlaceNoteData , dragLengthMs ) ) ;
}
// Finished dragging. Release the note.
currentPlaceNoteData = null ;
}
}
e lse
{
if ( FlxG . mouse . justPressed )
{
// Just clicked to place a note.
if ( overlapsGrid && ! overlapsSelectionBorder )
{
// We clicked on the grid without moving the mouse.
// Find the first note that is at the cursor position.
2023-02-02 18:26:03 -05:00
var highlightedNote: ChartEditorNoteSprite = renderedNotes . members . find ( function ( note : ChartEditorNoteSprite ) : Bool {
2023-01-22 19:55:30 -05:00
// If note.alive is false, the note is dead and awaiting recycling.
return note . alive && FlxG . mouse . overlaps ( note ) ;
} ) ;
var highlightedEvent: ChartEditorEventSprite = null ;
if ( highlightedNote == null )
{
2023-02-02 18:26:03 -05:00
highlightedEvent = renderedEvents . members . find ( function ( event : ChartEditorEventSprite ) : Bool {
2023-01-22 19:55:30 -05:00
// If event.alive is false, the event is dead and awaiting recycling.
return event . alive && FlxG . mouse . overlaps ( event ) ;
} ) ;
}
if ( FlxG . keys . pressed . CONTROL )
{
// Control click to select/deselect an individual note.
if ( highlightedNote != null )
{
if ( isNoteSelected ( highlightedNote . noteData ) )
{
performCommand ( new DeselectItemsCommand ( [ highlightedNote . noteData ] , [ ] ) ) ;
}
e lse
{
performCommand ( new SelectItemsCommand ( [ highlightedNote . noteData ] , [ ] ) ) ;
}
}
e lse if ( highlightedEvent != null )
{
if ( isEventSelected ( highlightedEvent . eventData ) )
{
performCommand ( new DeselectItemsCommand ( [ ] , [ highlightedEvent . eventData ] ) ) ;
}
e lse
{
performCommand ( new SelectItemsCommand ( [ ] , [ highlightedEvent . eventData ] ) ) ;
}
}
e lse
{
// Do nothing when control clicking nothing.
}
}
e lse
{
if ( highlightedNote != null )
{
// Click a note to select it.
performCommand ( new SetItemSelectionCommand ( [ highlightedNote . noteData ] , [ ] , currentNoteSelection , currentEventSelection ) ) ;
}
e lse if ( highlightedEvent != null )
{
// Click an event to select it.
performCommand ( new SetItemSelectionCommand ( [ ] , [ highlightedEvent . eventData ] , currentNoteSelection , currentEventSelection ) ) ;
}
e lse
{
// Click a blank space to place a note and select it.
if ( cursorColumn == eventColumn )
{
// Create an event and place it in the chart.
// TODO: Figure out configuring event data.
var newEventData: SongEventData = new SongEventData ( cursorMs , selectedEventKind , selectedEventData ) ;
performCommand ( new AddEventsCommand ( [ newEventData ] , FlxG . keys . pressed . CONTROL ) ) ;
}
e lse
{
// Create a note and place it in the chart.
var newNoteData: SongNoteData = new SongNoteData ( cursorMs , cursorColumn , 0 , selectedNoteKind ) ;
performCommand ( new AddNotesCommand ( [ newNoteData ] , FlxG . keys . pressed . CONTROL ) ) ;
currentPlaceNoteData = newNoteData ;
}
}
}
}
e lse
{
// If we clicked and released outside the grid, 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.
2023-02-02 18:26:03 -05:00
var highlightedNote: ChartEditorNoteSprite = renderedNotes . members . find ( function ( note : ChartEditorNoteSprite ) : Bool {
2023-01-22 19:55:30 -05:00
// If note.alive is false, the note is dead and awaiting recycling.
return note . alive && FlxG . mouse . overlaps ( note ) ;
} ) ;
var highlightedEvent: ChartEditorEventSprite = null ;
if ( highlightedNote == null )
{
2023-02-02 18:26:03 -05:00
highlightedEvent = renderedEvents . members . find ( function ( event : ChartEditorEventSprite ) : Bool {
2023-01-22 19:55:30 -05:00
// If event.alive is false, the event is dead and awaiting recycling.
return event . alive && FlxG . mouse . overlaps ( event ) ;
} ) ;
}
if ( highlightedNote != null )
{
// Handle the case of clicking on a sustain piece.
highlightedNote = highlightedNote . getBaseNoteSprite ( ) ;
// Remove the note.
performCommand ( new RemoveNotesCommand ( [ highlightedNote . noteData ] ) ) ;
}
e lse if ( highlightedEvent != null )
{
// Remove the event.
performCommand ( new RemoveEventsCommand ( [ highlightedEvent . eventData ] ) ) ;
}
e lse
{
// Right clicked on nothing.
}
}
// Handle grid cursor.
if ( overlapsGrid && ! overlapsSelectionBorder && ! gridPlayheadScrollAreaPressed )
{
Cursor . cursorMode = Pointer ;
// Indicate that we can place a note here.
if ( cursorColumn == eventColumn )
{
gridGhostEvent . visible = true ;
gridGhostNote . visible = false ;
if ( selectedEventKind != gridGhostEvent . eventData . event )
{
gridGhostEvent . eventData . event = selectedEventKind ;
}
gridGhostEvent . eventData . time = cursorMs ;
gridGhostEvent . updateEventPosition ( renderedEvents ) ;
}
e lse
{
gridGhostEvent . visible = false ;
gridGhostNote . visible = true ;
if ( cursorColumn != gridGhostNote . noteData . data || selectedNoteKind != gridGhostNote . noteData . kind )
{
gridGhostNote . noteData . kind = selectedNoteKind ;
gridGhostNote . noteData . data = cursorColumn ;
gridGhostNote . playNoteAnimation ( ) ;
}
gridGhostNote . noteData . time = cursorMs ;
gridGhostNote . updateNotePosition ( renderedNotes ) ;
}
// gridCursor.visible = true;
// // X and Y are the cursor position relative to the grid, snapped to the top left of the grid square.
// gridCursor.x = Math.floor(cursorX / GRID_SIZE) * GRID_SIZE + gridTiledSprite.x + (GRID_SELECTION_BORDER_WIDTH / 2);
// gridCursor.y = cursorStep * GRID_SIZE + gridTiledSprite.y + (GRID_SELECTION_BORDER_WIDTH / 2);
}
e lse
{
gridGhostNote . visible = false ;
gridGhostEvent . visible = false ;
Cursor . cursorMode = Default ;
}
}
}
e lse
{
gridGhostNote . visible = false ;
gridGhostEvent . visible = false ;
}
if ( isCursorOverHaxeUIButton && Cursor . cursorMode == Default )
{
Cursor . cursorMode = Pointer ;
}
}
/ * *
* Handle using ` r e n d e r e d N o t e s ` t o d i s p l a y n o t e s f r o m ` c u r r e n t S o n g C h a r t N o t e D a t a ` .
* /
2023-02-28 13:17:28 -05:00
function handleNoteDisplay ( ) : Void
2023-01-22 19:55:30 -05:00
{
if ( noteDisplayDirty )
{
noteDisplayDirty = false ;
// Update for whether downscroll is enabled.
renderedNotes . flipX = ( isViewDownscroll ) ;
// Calculate the view bounds.
var viewAreaTop: Float = this . scrollPositionInPixels - GRID_TOP_PAD ;
var viewHeight: Float = ( FlxG . height - MENU_BAR_HEIGHT ) ;
var viewAreaBottom: Float = this . scrollPositionInPixels + viewHeight ;
// Remove notes that are no longer visible and list the ones that are.
var displayedNoteData: Array < SongNoteData > = [ ] ;
for ( noteSprite in renderedNotes . members )
{
2023-01-22 22:25:45 -05:00
if ( noteSprite == null || ! noteSprite . exists || ! noteSprite . visible ) continue ;
2023-01-22 19:55:30 -05:00
if ( ! noteSprite . isNoteVisible ( viewAreaBottom , viewAreaTop ) )
{
// This sprite is off-screen.
// Kill the note sprite and recycle it.
noteSprite . noteData = null ;
}
e lse if ( currentSongChartNoteData . indexOf ( noteSprite . noteData ) == - 1 )
{
// This note was deleted.
// Kill the note sprite and recycle it.
noteSprite . noteData = null ;
}
e lse if ( noteSprite . noteData . length > 0 && ( noteSprite . parentNoteSprite == null && noteSprite . childNoteSprite == null ) )
{
// Note was extended.
// Kill the note sprite and recycle it.
noteSprite . noteData = null ;
}
e lse if ( noteSprite . noteData . length == 0 && ( noteSprite . parentNoteSprite != null || noteSprite . childNoteSprite != null ) )
{
// Note was shortened.
// Kill the note sprite and recycle it.
noteSprite . noteData = null ;
}
e lse
{
// Note is already displayed and should remain displayed.
displayedNoteData . push ( noteSprite . noteData ) ;
// Update the note sprite's position.
noteSprite . updateNotePosition ( renderedNotes ) ;
}
}
// Remove events that are no longer visible and list the ones that are.
var displayedEventData: Array < SongEventData > = [ ] ;
for ( eventSprite in renderedEvents . members )
{
2023-01-22 22:25:45 -05:00
if ( eventSprite == null || ! eventSprite . exists || ! eventSprite . visible ) continue ;
2023-01-22 19:55:30 -05:00
if ( ! eventSprite . isEventVisible ( viewAreaBottom , viewAreaTop ) )
{
// This sprite is off-screen.
// Kill the event sprite and recycle it.
eventSprite . eventData = null ;
}
e lse if ( currentSongChartEventData . indexOf ( eventSprite . eventData ) == - 1 )
{
// This event was deleted.
// Kill the event sprite and recycle it.
eventSprite . eventData = null ;
}
e lse
{
// Event is already displayed and should remain displayed.
displayedEventData . push ( eventSprite . eventData ) ;
// Update the event sprite's position.
eventSprite . updateEventPosition ( renderedEvents ) ;
}
}
// Add notes that are now visible.
for ( noteData in currentSongChartNoteData )
{
// Remember if we are already displaying this note.
if ( displayedNoteData . indexOf ( noteData ) != - 1 )
{
continue ;
}
// Get the position the note should be at.
var noteTimePixels: Float = noteData . time / Conductor . stepCrochet * GRID_SIZE ;
// Make sure the note appears when scrolling up.
var modifiedViewAreaTop = viewAreaTop - GRID_SIZE ;
2023-01-22 22:25:45 -05:00
if ( noteTimePixels < modifiedViewAreaTop || noteTimePixels > viewAreaBottom ) continue ;
2023-01-22 19:55:30 -05:00
// 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 ) ) ;
noteSprite . parentState = this ;
// The note sprite handles animation playback and positioning.
noteSprite . noteData = noteData ;
// Setting note data resets position relative to the grid so we fix that.
noteSprite . x += renderedNotes . x ;
noteSprite . y += renderedNotes . y ;
if ( noteSprite . noteData . length > 0 )
{
// If the note is a hold, we need to make sure it's long enough.
var noteLengthMs: Float = noteSprite . noteData . length ;
var noteLengthSteps: Float = ( noteLengthMs / Conductor . stepCrochet ) ;
var lastNoteSprite: ChartEditorNoteSprite = noteSprite ;
while ( noteLengthSteps > 0 )
{
if ( noteLengthSteps <= 1.0 )
{
// Last note in the hold.
// TODO: We may need to make it shorter and clip it visually.
}
var nextNoteSprite: ChartEditorNoteSprite = renderedNotes . recycle ( ChartEditorNoteSprite ) ;
nextNoteSprite . parentState = this ;
nextNoteSprite . parentNoteSprite = lastNoteSprite ;
lastNoteSprite . childNoteSprite = nextNoteSprite ;
lastNoteSprite = nextNoteSprite ;
noteLengthSteps -= 1 ;
}
// Make sure the last note sprite shows the end cap properly.
lastNoteSprite . childNoteSprite = null ;
// var noteLengthPixels:Float = (noteLengthMs / Conductor.stepCrochet + 1) * GRID_SIZE;
// add(new FlxSprite(noteSprite.x, noteSprite.y - renderedNotes.y + noteLengthPixels).makeGraphic(40, 2, 0xFFFF0000));
}
}
// Add events that are now visible.
for ( eventData in currentSongChartEventData )
{
// Remember if we are already displaying this event.
if ( displayedEventData . indexOf ( eventData ) != - 1 )
{
continue ;
}
// Get the position the event should be at.
var eventTimePixels: Float = eventData . time / Conductor . stepCrochet * GRID_SIZE ;
// Make sure the event appears when scrolling up.
var modifiedViewAreaTop = viewAreaTop - GRID_SIZE ;
2023-01-22 22:25:45 -05:00
if ( eventTimePixels < modifiedViewAreaTop || eventTimePixels > viewAreaBottom ) continue ;
2023-01-22 19:55:30 -05:00
// 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 ) ) ;
eventSprite . parentState = this ;
// The event sprite handles animation playback and positioning.
eventSprite . eventData = eventData ;
// Setting event data resets position relative to the grid so we fix that.
eventSprite . x += renderedEvents . x ;
eventSprite . y += renderedEvents . y ;
}
// 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 )
{
if ( isNoteSelected ( noteSprite . noteData ) && noteSprite . parentNoteSprite == null )
{
var selectionSquare: FlxSprite = renderedSelectionSquares . recycle ( buildSelectionSquare ) ;
// Set the position and size (because we might be recycling one with bad values).
selectionSquare . x = noteSprite . x ;
selectionSquare . y = noteSprite . y ;
selectionSquare . width = noteSprite . width ;
selectionSquare . height = noteSprite . height ;
}
}
for ( eventSprite in renderedEvents . members )
{
if ( isEventSelected ( eventSprite . eventData ) )
{
var selectionSquare: FlxSprite = renderedSelectionSquares . recycle ( buildSelectionSquare ) ;
// Set the position and size (because we might be recycling one with bad values).
selectionSquare . x = eventSprite . x ;
selectionSquare . y = eventSprite . y ;
selectionSquare . width = eventSprite . width ;
selectionSquare . height = eventSprite . height ;
}
}
// Sort the notes DESCENDING. This keeps the sustain behind the associated note.
renderedNotes . sort ( FlxSort . byY , FlxSort . DESCENDING ) ;
// Sort the events DESCENDING. This keeps the sustain behind the associated note.
renderedEvents . sort ( FlxSort . byY , FlxSort . DESCENDING ) ;
}
}
function buildSelectionSquare ( ) : FlxSprite
{
return new FlxSprite ( ) . loadGraphic ( selectionSquareBitmap ) ;
}
/ * *
* Handles display elements for t h e p l a y b a r a t t h e b o t t o m .
* /
function handlePlaybar ( )
{
// Make sure the playbar is never nudged out of the correct spot.
playbarHeadLayout . x = 4 ;
playbarHeadLayout . y = FlxG . height - 48 - 8 ;
var songPos = Conductor . songPosition ;
var songRemaining = songLengthInMs - songPos ;
// Move the playhead to match the song position, if we aren't dragging it.
if ( ! playbarHeadDragging )
{
var songPosPercent: Float = songPos / songLengthInMs ;
playbarHead . value = songPosPercent * 100 ;
}
var songPosSeconds: String = Std . string ( Math . floor ( ( songPos / 1000 ) % 60 ) ) . lpad ( ' 0 ' , 2 ) ;
var songPosMinutes: String = Std . string ( Math . floor ( ( songPos / 1000 ) / 60 ) ) . lpad ( ' 0 ' , 2 ) ;
var songPosString: String = ' ${ songPosMinutes } : ${ songPosSeconds } ' ;
setUIValue ( ' p l a y b a r S o n g P o s ' , songPosString ) ;
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 } ' ;
setUIValue ( ' p l a y b a r S o n g R e m a i n i n g ' , songRemainingString ) ;
}
/ * *
* Handle keybinds for F i l e m e n u i t e m s .
* /
function handleFileKeybinds ( )
{
// CTRL + Q = Quit to Menu
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . justPressed . Q )
{
FlxG . switchState ( new MainMenuState ( ) ) ;
}
}
/ * *
* Handle keybinds for e d i t m e n u i t e m s .
* /
function handleEditKeybinds ( )
{
// CTRL + Z = Undo
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . justPressed . Z )
{
undoLastCommand ( ) ;
}
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . pressed . Z && ! FlxG . keys . pressed . Y )
{
undoHeldTime += FlxG . elapsed ;
}
e lse
{
undoHeldTime = 0 ;
}
if ( undoHeldTime > RAPID_UNDO_DELAY + RAPID_UNDO_INTERVAL )
{
undoLastCommand ( ) ;
undoHeldTime -= RAPID_UNDO_INTERVAL ;
}
// CTRL + Y = Redo
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . justPressed . Y )
{
redoLastCommand ( ) ;
}
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . pressed . Y && ! FlxG . keys . pressed . Z )
{
redoHeldTime += FlxG . elapsed ;
}
e lse
{
redoHeldTime = 0 ;
}
if ( redoHeldTime > RAPID_UNDO_DELAY + RAPID_UNDO_INTERVAL )
{
redoLastCommand ( ) ;
redoHeldTime -= RAPID_UNDO_INTERVAL ;
}
// CTRL + C = Copy
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . justPressed . C )
{
// Copy selected notes.
// We don't need a command for this since we can't undo it.
2023-01-22 22:25:45 -05:00
SongDataUtils . writeItemsToClipboard (
{
notes : SongDataUtils . buildNoteClipboard ( currentNoteSelection ) ,
events : SongDataUtils . buildEventClipboard ( currentEventSelection ) ,
} ) ;
2023-01-22 19:55:30 -05:00
}
// 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 )
{
// Paste notes from clipboard, at the playhead.
performCommand ( new PasteItemsCommand ( scrollPositionInMs + playheadPositionInMs ) ) ;
}
// DELETE = Delete
if ( FlxG . keys . justPressed . DELETE )
{
// Delete selected items.
if ( currentNoteSelection . length > 0 && currentEventSelection . length > 0 )
{
performCommand ( new RemoveItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
}
e lse if ( currentNoteSelection . length > 0 )
{
performCommand ( new RemoveNotesCommand ( currentNoteSelection ) ) ;
}
e lse if ( currentEventSelection . length > 0 )
{
performCommand ( new RemoveEventsCommand ( currentEventSelection ) ) ;
}
}
// CTRL + A = Select All
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . justPressed . A )
{
// Select all items.
performCommand ( new SelectAllItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
}
// CTRL + I = Select Inverse
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . justPressed . I )
{
// Select unselected items and deselect selected items.
performCommand ( new InvertSelectedItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
}
// CTRL + D = Select None
if ( FlxG . keys . pressed . CONTROL && FlxG . keys . justPressed . D )
{
// Deselect all items.
performCommand ( new DeselectAllItemsCommand ( currentNoteSelection , currentEventSelection ) ) ;
}
}
/ * *
* Handle keybinds for V i e w m e n u i t e m s .
* /
function handleViewKeybinds ( ) { }
/ * *
* Handle keybinds for H e l p m e n u i t e m s .
* /
function handleHelpKeybinds ( )
{
// F1 = Open Help
2023-01-22 22:25:45 -05:00
if ( FlxG . keys . justPressed . F1 ) ChartEditorDialogHandler . openUserGuideDialog ( this ) ;
2023-01-22 19:55:30 -05:00
}
function handleToolboxes ( )
{
handleDifficultyToolbox ( ) ;
handlePlayerPreviewToolbox ( ) ;
handleOpponentPreviewToolbox ( ) ;
}
function handleDifficultyToolbox ( )
{
if ( difficultySelectDirty )
{
difficultySelectDirty = false ;
// Manage the Select Difficulty tree view.
var difficultyToolbox = ChartEditorToolboxHandler . getToolbox ( this , CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT ) ;
2023-01-22 22:25:45 -05:00
if ( difficultyToolbox == null ) return ;
2023-01-22 19:55:30 -05:00
var treeView: TreeView = difficultyToolbox . findComponent ( ' d i f f i c u l t y T o o l b o x T r e e ' ) ;
2023-01-22 22:25:45 -05:00
if ( treeView == null ) return ;
2023-01-22 19:55:30 -05:00
// Clear the tree view so we can rebuild it.
treeView . clearNodes ( ) ;
var treeSong = treeView . addNode ( { id : ' s t v _ s o n g ' , text : ' S : $ currentSongName ' , icon : " h a x e u i - c o r e / s t y l e s / d e f a u l t / h a x e u i _ t i n y . p n g " } ) ;
treeSong . expanded = true ;
for ( curVariation in availableVariations )
{
var variationMetadata: SongMetadata = songMetadata . get ( curVariation ) ;
2023-01-22 22:25:45 -05:00
var treeVariation = treeSong . addNode (
{
id : ' s t v _ v a r i a t i o n _ $ curVariation ' ,
text : ' V : ${ curVariation . toTitleCase ( ) } ' ,
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
} ) ;
2023-01-22 19:55:30 -05:00
treeVariation . expanded = true ;
var difficultyList = variationMetadata . playData . difficulties ;
for ( difficulty in difficultyList )
{
2023-01-22 22:25:45 -05:00
var treeDifficulty = treeVariation . addNode (
{
id : ' s t v _ d i f f i c u l t y _ ${ curVariation } _ $ difficulty ' ,
text : ' D : ${ difficulty . toTitleCase ( ) } ' ,
// icon: "haxeui-core/styles/default/haxeui_tiny.png"
} ) ;
2023-01-22 19:55:30 -05:00
}
}
treeView . onChange = onChangeTreeDifficulty ;
treeView . selectedNode = getCurrentTreeDifficultyNode ( ) ;
}
}
function handlePlayerPreviewToolbox ( )
{
// Manage the Select Difficulty tree view.
var charPreviewToolbox = ChartEditorToolboxHandler . getToolbox ( this , CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT ) ;
2023-01-22 22:25:45 -05:00
if ( charPreviewToolbox == null ) return ;
2023-01-22 19:55:30 -05:00
var charPlayer: CharacterPlayer = charPreviewToolbox . findComponent ( ' c h a r P l a y e r ' ) ;
2023-01-22 22:25:45 -05:00
if ( charPlayer == null ) return ;
2023-01-22 19:55:30 -05:00
currentPlayerCharacterPlayer = charPlayer ;
}
function handleOpponentPreviewToolbox ( )
{
// Manage the Select Difficulty tree view.
var charPreviewToolbox = ChartEditorToolboxHandler . getToolbox ( this , CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT ) ;
2023-01-22 22:25:45 -05:00
if ( charPreviewToolbox == null ) return ;
2023-01-22 19:55:30 -05:00
var charPlayer: CharacterPlayer = charPreviewToolbox . findComponent ( ' c h a r P l a y e r ' ) ;
2023-01-22 22:25:45 -05:00
if ( charPlayer == null ) return ;
2023-01-22 19:55:30 -05:00
currentOpponentCharacterPlayer = charPlayer ;
}
override function dispatchEvent ( event : ScriptEvent )
{
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 )
{
c ase ScriptEvent . UPDATE :
currentPlayerCharacterPlayer . onUpdate ( cast event ) ;
c ase ScriptEvent . SONG_BEAT_HIT :
currentPlayerCharacterPlayer . onBeatHit ( cast event ) ;
c ase ScriptEvent . SONG_STEP_HIT :
currentPlayerCharacterPlayer . onStepHit ( cast event ) ;
c ase ScriptEvent . NOTE_HIT :
currentPlayerCharacterPlayer . onNoteHit ( cast event ) ;
}
}
if ( currentOpponentCharacterPlayer != null )
{
switch ( event . type )
{
c ase ScriptEvent . UPDATE :
currentOpponentCharacterPlayer . onUpdate ( cast event ) ;
c ase ScriptEvent . SONG_BEAT_HIT :
currentOpponentCharacterPlayer . onBeatHit ( cast event ) ;
c ase ScriptEvent . SONG_STEP_HIT :
currentOpponentCharacterPlayer . onStepHit ( cast event ) ;
c ase ScriptEvent . NOTE_HIT :
currentOpponentCharacterPlayer . onNoteHit ( cast event ) ;
}
}
}
function getCurrentTreeDifficultyNode ( ) : TreeViewNode
{
var treeView: TreeView = findComponent ( ' d i f f i c u l t y T o o l b o x T r e e ' ) ;
2023-01-22 22:25:45 -05:00
if ( treeView == null ) return null ;
2023-01-22 19:55:30 -05:00
var result = treeView . findNodeByPath ( ' s t v _ s o n g / s t v _ v a r i a t i o n _ $ selectedVariation / s t v _ d i f f i c u l t y _ ${ selectedVariation } _ $ selectedDifficulty ' , ' i d ' ) ;
2023-01-22 22:25:45 -05:00
if ( result == null ) return null ;
2023-01-22 19:55:30 -05:00
return result ;
}
function onChangeTreeDifficulty ( event : UIEvent ) : Void
{
// Get the newly selected node.
var treeView: TreeView = cast event . target ;
var targetNode: TreeViewNode = treeView . selectedNode ;
if ( targetNode == null )
{
trace ( ' N o t a r g e t n o d e ! ' ) ;
// Reset the user's selection.
treeView . selectedNode = getCurrentTreeDifficultyNode ( ) ;
return ;
}
switch ( targetNode . data . id . split ( ' _ ' ) [ 1 ] )
{
c ase ' d i f f i c u l t y ' :
var variation = targetNode . data . id . split ( ' _ ' ) [ 2 ] ;
var difficulty = targetNode . data . id . split ( ' _ ' ) [ 3 ] ;
if ( variation != null && difficulty != null )
{
trace ( ' C h a n g i n g d i f f i c u l t y t o $ variation : $ difficulty ' ) ;
selectedVariation = variation ;
selectedDifficulty = difficulty ;
}
// case 'song':
// case 'variation':
d efault :
// Reset the user's selection.
trace ( ' S e l e c t e d w r o n g n o d e t y p e , r e s e t t i n g s e l e c t i o n . ' ) ;
treeView . selectedNode = getCurrentTreeDifficultyNode ( ) ;
}
}
function addDifficulty ( variation : String ) { }
function addVariation ( variationId : String )
{
// Create a new variation with the specified ID.
songMetadata . set ( variationId , currentSongMetadata . clone ( variationId ) ) ;
// Switch to the new variation.
selectedVariation = variationId ;
}
/ * *
* Handle the player preview / gameplay test area on the left side .
* /
function handlePlayerDisplay ( ) { }
/ * *
* Handles the note preview / scroll area on the right side .
* Notes are rendered here as small bars .
* This function also h a n d l e s :
* - Moving the viewport preview box around based on its current position .
* - Scrolling the note preview area down if t h e n o t e p r e v i e w i s t a l l e r t h a n t h e s c r e e n ,
* and the viewport nears the end of the visible area .
* /
function handleNotePreview ( )
{
//
if ( notePreviewDirty )
{
notePreviewDirty = false ;
var PREVIEW_WIDTH: Int = GRID_SIZE * 2 ;
var STEP_HEIGHT: Int = 1 ;
var PREVIEW_HEIGHT: Int = Std . int ( Conductor . getTimeInSteps ( audioInstTrack . length ) * STEP_HEIGHT ) ;
notePreviewBitmap = new BitmapData ( PREVIEW_WIDTH , PREVIEW_HEIGHT , true ) ;
notePreviewBitmap . fillRect ( new Rectangle ( 0 , 0 , PREVIEW_WIDTH , PREVIEW_HEIGHT ) , PREVIEW_BG_COLOR ) ;
}
}
/ * *
* Perform a spot update on the note preview , by editing the note preview
* only where necessary . More efficient than a full update .
* /
function updateNotePreview ( note : SongNoteData , ? deleteNote : Bool = 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 ( )
{
if ( commandHistoryDirty )
{
commandHistoryDirty = false ;
// Update the Undo and Redo buttons.
var undoButton: MenuItem = findComponent ( ' m e n u b a r I t e m U n d o ' , MenuItem ) ;
if ( undoButton != null )
{
if ( undoHistory . length == 0 )
{
// Disable the Undo button.
undoButton . disabled = true ;
undoButton . text = " U n d o " ;
}
e lse
{
// Change the label to the last command.
undoButton . disabled = false ;
undoButton . text = ' U n d o ${ undoHistory [ undoHistory . length - 1 ] . toString ( ) } ' ;
}
}
e lse
{
trace ( " u n d o B u t t o n i s n u l l " ) ;
}
var redoButton: MenuItem = findComponent ( ' m e n u b a r I t e m R e d o ' , MenuItem ) ;
if ( redoButton != null )
{
if ( redoHistory . length == 0 )
{
// Disable the Redo button.
redoButton . disabled = true ;
redoButton . text = " R e d o " ;
}
e lse
{
// Change the label to the last command.
redoButton . disabled = false ;
redoButton . text = ' R e d o ${ redoHistory [ redoHistory . length - 1 ] . toString ( ) } ' ;
}
}
e lse
{
trace ( " r e d o B u t t o n i s n u l l " ) ;
}
}
}
/ * *
* Handle syncronizing the conductor with the music playback .
* /
function handleMusicPlayback ( )
{
if ( audioInstTrack != null && audioInstTrack . playing )
{
if ( FlxG . mouse . pressedMiddle )
{
// If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat!
var oldStepTime = Conductor . currentStepTime ;
var oldSongPosition = Conductor . songPosition ;
Conductor . update ( audioInstTrack . time ) ;
handleHitsounds ( oldSongPosition , Conductor . songPosition ) ;
// Resync vocals.
2023-01-22 22:25:45 -05:00
if ( Math . abs ( audioInstTrack . time - audioVocalTrackGroup . time ) > 100 ) audioVocalTrackGroup . time = audioInstTrack . time ;
2023-01-22 19:55:30 -05:00
var diffStepTime = Conductor . currentStepTime - oldStepTime ;
// Move the playhead.
playheadPositionInPixels += diffStepTime * GRID_SIZE ;
// We don't move the song to scroll position, or update the note sprites.
}
e lse
{
// Else, move the entire view.
var oldSongPosition = Conductor . songPosition ;
Conductor . update ( audioInstTrack . time ) ;
handleHitsounds ( oldSongPosition , Conductor . songPosition ) ;
// Resync vocals.
2023-01-22 22:25:45 -05:00
if ( audioVocalTrackGroup != null
& & Math . abs ( audioInstTrack . time - audioVocalTrackGroup . time ) > 100 ) audioVocalTrackGroup . time = audioInstTrack . time ;
2023-01-22 19:55:30 -05:00
// We need time in fractional steps here to allow the song to actually play.
// Also account for a potentially offset playhead.
scrollPositionInPixels = Conductor . currentStepTime * GRID_SIZE - playheadPositionInPixels ;
// DO NOT move song to scroll position here specifically.
// We need to update the note sprites.
noteDisplayDirty = true ;
}
}
if ( FlxG . keys . justPressed . SPACE && ! isHaxeUIDialogOpen )
{
toggleAudioPlayback ( ) ;
}
}
/ * *
* Handle the playback of hitsounds .
* /
function handleHitsounds ( oldSongPosition : Float , newSongPosition : Float ) : Void
{
2023-01-22 22:25:45 -05:00
if ( ! hitsoundsEnabled ) return ;
2023-01-22 19:55:30 -05:00
// Assume notes are sorted by time.
for ( noteData in currentSongChartNoteData )
{
2023-01-22 22:25:45 -05:00
if ( noteData . time < oldSongPosition ) // Note is in the past.
2023-01-22 19:55:30 -05:00
continue ;
2023-01-22 22:25:45 -05:00
if ( noteData . time >= newSongPosition ) // Note is in the future.
2023-01-22 19:55:30 -05:00
return ;
// Note was just hit.
// Character preview.
// Why does NOTESCRIPTEVENT TAKE A SPRITE AAAAA
var tempNote: Note = new Note ( noteData . time , noteData . data , null , false , NORMAL ) ;
tempNote . mustPress = noteData . getMustHitNote ( ) ;
tempNote . data . sustainLength = noteData . length ;
tempNote . data . noteKind = noteData . kind ;
tempNote . scrollFactor . set ( 0 , 0 ) ;
var event: NoteScriptEvent = new NoteScriptEvent ( ScriptEvent . NOTE_HIT , tempNote , 1 , true ) ;
dispatchEvent ( event ) ;
// Calling event.cancelEvent() skips all the other logic! Neat!
2023-01-22 22:25:45 -05:00
if ( event . eventCanceled ) continue ;
2023-01-22 19:55:30 -05:00
// Hitsounds.
switch ( noteData . getStrumlineIndex ( ) )
{
c ase 0 : // Player
2023-01-22 22:25:45 -05:00
if ( hitsoundsEnabledPlayer ) playSound ( Paths . sound ( ' f u n n y N o i s e / f u n n y N o i s e - 0 9 ' ) ) ;
2023-01-22 19:55:30 -05:00
c ase 1 : // Opponent
2023-01-22 22:25:45 -05:00
if ( hitsoundsEnabledOpponent ) playSound ( Paths . sound ( ' f u n n y N o i s e / f u n n y N o i s e - 0 1 0 ' ) ) ;
2023-01-22 19:55:30 -05:00
}
}
}
function startAudioPlayback ( )
{
2023-01-22 22:25:45 -05:00
if ( audioInstTrack != null ) audioInstTrack . play ( ) ;
if ( audioVocalTrackGroup != null ) audioVocalTrackGroup . play ( ) ;
if ( audioVocalTrackGroup != null ) audioVocalTrackGroup . play ( ) ;
2023-01-22 19:55:30 -05:00
}
function stopAudioPlayback ( )
{
2023-01-22 22:25:45 -05:00
if ( audioInstTrack != null ) audioInstTrack . pause ( ) ;
if ( audioVocalTrackGroup != null ) audioVocalTrackGroup . pause ( ) ;
if ( audioVocalTrackGroup != null ) audioVocalTrackGroup . pause ( ) ;
2023-01-22 19:55:30 -05:00
}
function toggleAudioPlayback ( )
{
2023-01-22 22:25:45 -05:00
if ( audioInstTrack == null ) return ;
2023-01-22 19:55:30 -05:00
if ( audioInstTrack . playing )
{
stopAudioPlayback ( ) ;
}
e lse
{
startAudioPlayback ( ) ;
}
}
function handlePlayhead ( )
{
// Place notes at the playhead.
// TODO: Add the ability to switch modes.
if ( true )
{
2023-01-22 22:25:45 -05:00
if ( FlxG . keys . justPressed . ONE ) placeNoteAtPlayhead ( 0 ) ;
if ( FlxG . keys . justPressed . TWO ) placeNoteAtPlayhead ( 1 ) ;
if ( FlxG . keys . justPressed . THREE ) placeNoteAtPlayhead ( 2 ) ;
if ( FlxG . keys . justPressed . FOUR ) placeNoteAtPlayhead ( 3 ) ;
if ( FlxG . keys . justPressed . FIVE ) placeNoteAtPlayhead ( 4 ) ;
if ( FlxG . keys . justPressed . SIX ) placeNoteAtPlayhead ( 5 ) ;
if ( FlxG . keys . justPressed . SEVEN ) placeNoteAtPlayhead ( 6 ) ;
if ( FlxG . keys . justPressed . EIGHT ) placeNoteAtPlayhead ( 7 ) ;
2023-01-22 19:55:30 -05:00
}
}
function placeNoteAtPlayhead ( column : Int ) : Void
{
var gridSnappedPlayheadPos = scrollPositionInPixels - ( scrollPositionInPixels % GRID_SIZE ) ;
}
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 = scrollPositionInPixels - value ;
playheadPositionInPixels -= amount ;
}
value = 0 ;
}
2023-01-22 22:25:45 -05:00
if ( value > songLengthInPixels ) value = songLengthInPixels ;
2023-01-22 19:55:30 -05:00
2023-01-22 22:25:45 -05:00
if ( value == scrollPositionInPixels ) return value ;
2023-01-22 19:55:30 -05:00
this . scrollPositionInPixels = value ;
// Move the grid sprite to the correct position.
if ( isViewDownscroll )
{
gridTiledSprite . y = - scrollPositionInPixels + ( MENU_BAR_HEIGHT + GRID_TOP_PAD ) ;
}
e lse
{
gridTiledSprite . y = - scrollPositionInPixels + ( MENU_BAR_HEIGHT + GRID_TOP_PAD ) ;
}
// Move the rendered notes to the correct position.
renderedNotes . setPosition ( gridTiledSprite . x , gridTiledSprite . y ) ;
renderedEvents . setPosition ( gridTiledSprite . x , gridTiledSprite . y ) ;
renderedSelectionSquares . setPosition ( gridTiledSprite . x , gridTiledSprite . y ) ;
if ( gridSpectrogram != null )
{
// Move the spectrogram to the correct position.
gridSpectrogram . y = gridTiledSprite . y ;
gridSpectrogram . setPosition ( 0 , 0 ) ;
}
return this . scrollPositionInPixels ;
}
function get_playheadPositionInPixels ( ) : Float
{
return this . playheadPositionInPixels ;
}
function set_playheadPositionInPixels ( value : Float ) : Float
{
// Make sure playhead doesn't go outside the song.
2023-01-22 22:25:45 -05:00
if ( value + scrollPositionInPixels < 0 ) value = - scrollPositionInPixels ;
if ( value + scrollPositionInPixels > songLengthInPixels ) value = songLengthInPixels - scrollPositionInPixels ;
2023-01-22 19:55:30 -05:00
this . playheadPositionInPixels = value ;
// Move the playhead sprite to the correct position.
gridPlayhead . y = this . playheadPositionInPixels + ( MENU_BAR_HEIGHT + GRID_TOP_PAD ) ;
return this . playheadPositionInPixels ;
}
/ * *
* Loads an instrumental from an absolute file path , replacing the current instrumental .
2023-02-28 13:17:28 -05:00
*
* @ param path The absolute path to the audio file .
2023-01-22 19:55:30 -05:00
* /
public function loadInstrumentalFromPath ( path : String ) : Void
{
#if sys
2023-02-28 13:17:28 -05:00
// Validate file extension.
var fileExtension: String = Path . extension ( path ) ;
if ( ! SUPPORTED_MUSIC_FORMATS . contains ( fileExtension ) )
{
trace ( ' [ W A R N ] U n s u p p o r t e d f i l e e x t e n s i o n : $ fileExtension ' ) ;
return ;
}
2023-01-22 19:55:30 -05:00
var fileBytes: haxe . io . Bytes = sys . io . File . getBytes ( path ) ;
loadInstrumentalFromBytes ( fileBytes ) ;
#else
trace ( " [ W A R N ] T h i s p l a t f o r m c a n ' t l o a d a u d i o f r o m a f i l e p a t h , y o u ' l l n e e d t o f e t c h t h e b y t e s s o m e o t h e r w a y . " ) ;
#end
}
/ * *
* Loads an instrumental from audio byte data , replacing the current instrumental .
* /
public function loadInstrumentalFromBytes ( bytes : haxe . io . Bytes ) : Void
{
var openflSound = new openfl . media . Sound ( ) ;
openflSound . loadCompressedDataFromByteArray ( openfl . utils . ByteArray . fromBytes ( bytes ) , bytes . length ) ;
audioInstTrack = FlxG . sound . load ( openflSound , 1.0 , false ) ;
audioInstTrack . autoDestroy = false ;
audioInstTrack . pause ( ) ;
// Tell the user the load was successful.
// TODO: Un-bork this.
// showNotification('Loaded instrumental track successfully.');
postLoadInstrumental ( ) ;
}
public function loadInstrumentalFromAsset ( path : String ) : Void
{
var instTrack = FlxG . sound . load ( path , 1.0 , false ) ;
audioInstTrack = instTrack ;
postLoadInstrumental ( ) ;
}
function postLoadInstrumental ( )
{
// Prevent the time from skipping back to 0 when the song ends.
2023-02-02 18:26:03 -05:00
audioInstTrack . onComplete = function ( ) {
2023-01-22 22:25:45 -05:00
if ( audioInstTrack != null ) audioInstTrack . pause ( ) ;
if ( audioVocalTrackGroup != null ) audioVocalTrackGroup . pause ( ) ;
2023-01-22 19:55:30 -05:00
} ;
songLengthInMs = audioInstTrack . length ;
gridTiledSprite . height = songLengthInPixels ;
if ( gridSpectrogram != null )
{
gridSpectrogram . setSound ( audioInstTrack ) ;
gridSpectrogram . generateSection ( 0 , songLengthInMs / 1000 ) ;
}
scrollPositionInPixels = 0 ;
playheadPositionInPixels = 0 ;
moveSongToScrollPosition ( ) ;
}
/ * *
* Loads a vocal track from an absolute file path .
* /
2023-02-28 13:17:28 -05:00
public function loadVocalsFromPath ( path : String , ? charKey : String ) : Void
2023-01-22 19:55:30 -05:00
{
#if sys
var fileBytes: haxe . io . Bytes = sys . io . File . getBytes ( path ) ;
2023-02-28 13:17:28 -05:00
loadVocalsFromBytes ( fileBytes , charKey ) ;
2023-01-22 19:55:30 -05:00
#else
trace ( " [ W A R N ] T h i s p l a t f o r m c a n ' t l o a d a u d i o f r o m a f i l e p a t h , y o u ' l l n e e d t o f e t c h t h e b y t e s s o m e o t h e r w a y . " ) ;
#end
}
2023-02-28 13:17:28 -05:00
public function loadVocalsFromAsset ( path : String , ? charKey : String ) : Void
2023-01-22 19:55:30 -05:00
{
var vocalTrack: FlxSound = FlxG . sound . load ( path , 1.0 , false ) ;
audioVocalTrackGroup . add ( vocalTrack ) ;
}
/ * *
* Loads a vocal track from audio byte data .
* /
2023-02-28 13:17:28 -05:00
public function loadVocalsFromBytes ( bytes : haxe . io . Bytes , ? charKey : String ) : Void
2023-01-22 19:55:30 -05:00
{
var openflSound = new openfl . media . Sound ( ) ;
openflSound . loadCompressedDataFromByteArray ( openfl . utils . ByteArray . fromBytes ( bytes ) , bytes . length ) ;
var vocalTrack: FlxSound = FlxG . sound . load ( openflSound , 1.0 , false ) ;
audioVocalTrackGroup . add ( vocalTrack ) ;
// Tell the user the load was successful.
// TODO: Un-bork this.
// showNotification('Loaded instrumental track successfully.');
}
/ * *
* Fetch ' s a s o n g ' s existing chart and audio and loads it , replacing the current song .
* /
public function loadSongAsTemplate ( songId : String )
{
var song: Song = SongDataParser . fetchSong ( songId ) ;
if ( song == null )
{
2023-02-28 13:17:28 -05:00
// showNotification('Failed to load song.');
2023-01-22 19:55:30 -05:00
return ;
}
// Load the song metadata.
var rawSongMetadata: Array < SongMetadata > = song . getRawMetadata ( ) ;
this . songMetadata = new Map < String , SongMetadata > ( ) ;
for ( metadata in rawSongMetadata )
{
var variation = ( metadata . variation == null || metadata . variation == ' ' ) ? ' d e f a u l t ' : metadata . variation ;
this . songMetadata . set ( variation , metadata ) ;
}
this . songChartData = new Map < String , SongChartData > ( ) ;
for ( metadata in rawSongMetadata )
{
var variation = ( metadata . variation == null || metadata . variation == ' ' ) ? ' d e f a u l t ' : metadata . variation ;
this . songChartData . set ( variation , SongDataParser . parseSongChartData ( songId , metadata . variation ) ) ;
}
Conductor . forceBPM ( null ) ; // Disable the forced BPM.
Conductor . mapTimeChanges ( currentSongMetadata . timeChanges ) ;
loadInstrumentalFromAsset ( Paths . inst ( songId ) ) ;
loadVocalsFromAsset ( Paths . voices ( songId ) ) ;
// showNotification('Loaded song ${songId}.');
}
/ * *
* When setting the scroll position , except when automatically scrolling during song playback ,
* we need to update the conductor ' s c u r r e n t s t e p t i m e a n d t h e t i m e s t a m p o f t h e a u d i o t r a c k s .
* /
function moveSongToScrollPosition ( )
{
// Update the songPosition in the Conductor.
Conductor . update ( scrollPositionInMs ) ;
// Update the songPosition in the audio tracks.
2023-01-22 22:25:45 -05:00
if ( audioInstTrack != null ) audioInstTrack . time = scrollPositionInMs + playheadPositionInMs ;
if ( audioVocalTrackGroup != null ) audioVocalTrackGroup . time = scrollPositionInMs + playheadPositionInMs ;
2023-01-22 19:55:30 -05:00
// We need to update the note sprites because we changed the scroll position.
noteDisplayDirty = true ;
}
/ * *
* Perform ( or r e d o ) a command , then add it to the undo stack .
*
* @ param command The command to perform .
* @ param purgeRedoStack If true , the redo stack will be cleared .
* /
function performCommand ( command : ChartEditorCommand , ? purgeRedoStack : Bool = true ) : Void
{
command . execute ( this ) ;
undoHistory . push ( command ) ;
commandHistoryDirty = true ;
2023-01-22 22:25:45 -05:00
if ( purgeRedoStack ) redoHistory = [ ] ;
2023-01-22 19:55:30 -05:00
}
/ * *
* Undo a command , then add it to the redo stack .
* @ param command The command to undo .
* /
function undoCommand ( command : ChartEditorCommand ) : Void
{
command . undo ( this ) ;
redoHistory . push ( command ) ;
commandHistoryDirty = true ;
}
/ * *
* Undo the last command in the undo stack , then add it to the redo stack .
* /
function undoLastCommand ( ) : Void
{
if ( undoHistory . length == 0 )
{
trace ( ' N o a c t i o n s t o u n d o . ' ) ;
return ;
}
var command = undoHistory . pop ( ) ;
undoCommand ( command ) ;
}
/ * *
* Redo the last command in the redo stack , then add it to the undo stack .
* /
function redoLastCommand ( ) : Void
{
if ( redoHistory . length == 0 )
{
trace ( ' N o a c t i o n s t o r e d o . ' ) ;
return ;
}
var command = redoHistory . pop ( ) ;
performCommand ( command , false ) ;
}
function sortChartData ( )
{
2023-02-02 18:26:03 -05:00
currentSongChartNoteData . sort ( function ( a : SongNoteData , b : SongNoteData ) : Int {
2023-01-22 19:55:30 -05:00
return FlxSort . byValues ( FlxSort . ASCENDING , a . time , b . time ) ;
} ) ;
2023-02-02 18:26:03 -05:00
currentSongChartEventData . sort ( function ( a : SongEventData , b : SongEventData ) : Int {
2023-01-22 19:55:30 -05:00
return FlxSort . byValues ( FlxSort . ASCENDING , a . time , b . time ) ;
} ) ;
}
function playMetronomeTick ( ? high : Bool = false )
{
playSound ( Paths . sound ( ' p i a n o S t u f f / p i a n o - ${ high ? ' 0 0 1 ' : ' 0 0 8 ' } ' ) ) ;
}
function isNoteSelected ( note : SongNoteData ) : Bool
{
return currentNoteSelection . indexOf ( note ) != - 1 ;
}
function isEventSelected ( event : SongEventData ) : Bool
{
return currentEventSelection . indexOf ( event ) != - 1 ;
}
/ * *
* Play a sound effect .
* Automatically cleans up after itself and recycles previous FlxSound instances if a v a i l a b l e , f o r p e r f o r m a n c e .
* /
function playSound ( path : String )
{
var snd: FlxSound = FlxG . sound . list . recycle ( FlxSound ) ;
snd . loadEmbedded ( FlxG . sound . cache ( path ) ) ;
snd . autoDestroy = true ;
FlxG . sound . list . add ( snd ) ;
snd . play ( ) ;
}
override function destroy ( )
{
super . destroy ( ) ;
cleanupAutoSave ( ) ;
@ : privateAccess
ChartEditorNoteSprite . noteFrameCollection = null ;
}
/ * *
* Dismiss any existing notifications , if t h e r e a r e a n y .
* /
2023-02-28 13:17:28 -05:00
function dismissNotifications ( ) : Void
2023-01-22 19:55:30 -05:00
{
2023-02-28 13:17:28 -05:00
NotificationManager . instance . clearNotifications ( ) ;
2023-01-22 19:55:30 -05:00
}
/ * *
* @ param force Whether to force the export without prompting the user for a f i l e l o c a t i o n .
* @ param tmp If true , save to the temporary directory instead of the local ` backup ` directory .
* /
public function exportAllSongData ( ? force : Bool = false , ? tmp : Bool = false ) : Void
{
var zipEntries = [ ] ;
for ( variation in availableVariations )
{
var variationId = variation ;
if ( variation == ' ' || variation == ' d e f a u l t ' || variation == ' n o r m a l ' )
{
variationId = ' ' ;
}
if ( variationId == ' ' )
{
var variationMetadata = songMetadata . get ( variation ) ;
zipEntries . push ( FileUtil . makeZIPEntry ( ' $ currentSongId - m e t a d a t a . j s o n ' , SerializerUtil . toJSON ( variationMetadata ) ) ) ;
var variationChart = songChartData . get ( variation ) ;
zipEntries . push ( FileUtil . makeZIPEntry ( ' $ currentSongId - c h a r t . j s o n ' , SerializerUtil . toJSON ( variationChart ) ) ) ;
}
e lse
{
var variationMetadata = songMetadata . get ( variation ) ;
zipEntries . push ( FileUtil . makeZIPEntry ( ' $ currentSongId - m e t a d a t a - $ variationId . j s o n ' , SerializerUtil . toJSON ( variationMetadata ) ) ) ;
var variationChart = songChartData . get ( variation ) ;
zipEntries . push ( FileUtil . makeZIPEntry ( ' $ currentSongId - c h a r t - $ variationId . j s o n ' , SerializerUtil . toJSON ( variationChart ) ) ) ;
}
}
// TODO: Add audio files to the ZIP.
trace ( ' E x p o r t i n g ${ zipEntries . length } f i l e s t o Z I P . . . ' ) ;
if ( force )
{
var targetPath: String = tmp ? Path . join ( [ FileUtil . getTempDir ( ) , ' c h a r t - e d i t o r - e x i t - ${ DateUtil . generateTimestamp ( ) } . z i p ' ] ) : Path . join ( [ ' . / b a c k u p s / ' , ' c h a r t - e d i t o r - e x i t - ${ DateUtil . generateTimestamp ( ) } . z i p ' ] ) ;
// We have to force write because the program will die before the save dialog is closed.
trace ( ' F o r c e e x p o r t i n g t o $ targetPath . . . ' ) ;
FileUtil . saveFilesAsZIPToPath ( zipEntries , targetPath ) ;
return ;
}
// Prompt and save.
2023-02-02 18:26:03 -05:00
var onSave: Array < String > -> Void = ( paths : Array < String > ) - > {
2023-01-22 19:55:30 -05:00
trace ( ' S u c c e s s f u l l y e x p o r t e d f i l e s . ' ) ;
} ;
2023-02-02 18:26:03 -05:00
var onCancel: Void -> Void = ( ) - > {
2023-01-22 19:55:30 -05:00
trace ( ' E x p o r t c a n c e l l e d . ' ) ;
} ;
FileUtil . saveMultipleFiles ( zipEntries , onSave , onCancel , ' $ currentSongId - c h a r t . z i p ' ) ;
}
2022-09-07 19:07:08 -04:00
}