From c3577b32efa9882c44509363cc866e46bd62e3a9 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 8 Jun 2023 16:53:52 -0400 Subject: [PATCH] Port changes to ChartEditorState --- .../debug/charting/ChartEditorNotePreview.hx | 2 - .../ui/debug/charting/ChartEditorState.hx | 763 +++++++++++++----- 2 files changed, 552 insertions(+), 213 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx index 29c844c2f..27951f079 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx @@ -1,7 +1,5 @@ package funkin.ui.debug.charting; -package funkin.ui.debug.charting; - import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongNoteData; import flixel.math.FlxMath; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 4e050b19b..c24c2db1b 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,25 +1,28 @@ package funkin.ui.debug.charting; import funkin.ui.debug.charting.ChartEditorCommand; -import flixel.input.keyboard.FlxKey; -import funkin.input.TurboKeyHandler; -import haxe.ui.notifications.NotificationType; -import haxe.ui.notifications.NotificationManager; -import haxe.DynamicAccess; -import haxe.io.Path; +import funkin.play.song.SongData.SongPlayableChar; +import funkin.play.character.BaseCharacter.CharacterType; +import haxe.ui.containers.dialogs.CollapsibleDialog; +import openfl.Assets; import flixel.addons.display.FlxSliceSprite; import flixel.addons.display.FlxTiledSprite; import flixel.FlxSprite; import flixel.group.FlxSpriteGroup; +import flixel.input.keyboard.FlxKey; import flixel.math.FlxPoint; import flixel.math.FlxRect; -import flixel.sound.FlxSound; +import flixel.system.FlxSound; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.tweens.misc.VarTween; import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.visualize.PolygonSpectogram; -import funkin.audio.VoicesGroup; +import funkin.audio.VocalGroup; import funkin.input.Cursor; +import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; import funkin.play.HealthIcon; import funkin.play.song.Song; @@ -34,12 +37,15 @@ import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; import funkin.util.Constants; -import funkin.util.FileUtil; import funkin.util.DateUtil; +import funkin.util.FileUtil; import funkin.util.SerializerUtil; +import funkin.util.WindowUtil; +import haxe.DynamicAccess; +import haxe.io.Bytes; +import haxe.io.Path; import haxe.ui.components.Label; import haxe.ui.components.Slider; -import haxe.ui.containers.dialogs.Dialog; import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; @@ -47,7 +53,8 @@ import haxe.ui.core.Component; import haxe.ui.core.Screen; import haxe.ui.events.DragEvent; import haxe.ui.events.UIEvent; -import funkin.util.WindowUtil; +import haxe.ui.notifications.NotificationManager; +import haxe.ui.notifications.NotificationType; import openfl.display.BitmapData; import openfl.geom.Rectangle; @@ -58,10 +65,11 @@ using Lambda; * Built with HaxeUI for use by both developers and modders. * * Some functionality is moved to other classes to help maintain my sanity. - * + * * @author MasterEric */ // Give other classes access to private instance fields + @:allow(funkin.ui.debug.charting.ChartEditorCommand) @:allow(funkin.ui.debug.charting.ChartEditorDialogHandler) @:allow(funkin.ui.debug.charting.ChartEditorThemeHandler) @@ -73,19 +81,19 @@ class ChartEditorState extends HaxeUIState */ // ============================== // XML Layouts - static final CHART_EDITOR_LAYOUT = Paths.ui('chart-editor/main-view'); + static final CHART_EDITOR_LAYOUT:String = Paths.ui('chart-editor/main-view'); - static final CHART_EDITOR_NOTIFBAR_LAYOUT = Paths.ui('chart-editor/components/notifbar'); - static final CHART_EDITOR_PLAYBARHEAD_LAYOUT = Paths.ui('chart-editor/components/playbar-head'); + static final CHART_EDITOR_NOTIFBAR_LAYOUT:String = Paths.ui('chart-editor/components/notifbar'); + static final CHART_EDITOR_PLAYBARHEAD_LAYOUT:String = Paths.ui('chart-editor/components/playbar-head'); - static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT = Paths.ui('chart-editor/toolbox/tools'); - static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT = Paths.ui('chart-editor/toolbox/notedata'); - static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT = Paths.ui('chart-editor/toolbox/eventdata'); - static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT = Paths.ui('chart-editor/toolbox/metadata'); - static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT = Paths.ui('chart-editor/toolbox/difficulty'); - static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT = Paths.ui('chart-editor/toolbox/characters'); - static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/player-preview'); - static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/opponent-preview'); + static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:String = Paths.ui('chart-editor/toolbox/tools'); + static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); + static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); + static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); + static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty'); + static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:String = Paths.ui('chart-editor/toolbox/characters'); + static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/player-preview'); + static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview'); // Validation static final SUPPORTED_MUSIC_FORMATS:Array = ['ogg']; @@ -104,12 +112,12 @@ class ChartEditorState extends HaxeUIState /** * Number of notes in each player's strumline. */ - public static final STRUMLINE_SIZE = 4; + public static final STRUMLINE_SIZE:Int = 4; /** * The height of the menu bar in the layout. */ - static final MENU_BAR_HEIGHT = 32; + static final MENU_BAR_HEIGHT:Int = 32; /** * Duration to wait before autosaving the chart. @@ -126,10 +134,10 @@ class ChartEditorState extends HaxeUIState */ static final NOTIFICATION_DISMISS_TIME:Int = 5000; - // 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; + /** + * Duration, in seconds, for the scroll easing animation. + */ + static final SCROLL_EASE_DURATION:Float = 0.2; // UI Element Colors // Background color tint. @@ -189,12 +197,12 @@ class ChartEditorState extends HaxeUIState function get_scrollPositionInMs():Float { - return scrollPositionInSteps * Conductor.stepCrochet; + return scrollPositionInSteps * Conductor.stepLengthMs; } function set_scrollPositionInMs(value:Float):Float { - scrollPositionInPixels = value / Conductor.stepCrochet; + scrollPositionInPixels = value / Conductor.stepLengthMs; return value; } @@ -223,7 +231,7 @@ class ChartEditorState extends HaxeUIState function get_playheadPositionInMs():Float { - return playheadPositionInSteps * Conductor.stepCrochet; + return playheadPositionInSteps * Conductor.stepLengthMs; } /** @@ -263,7 +271,7 @@ class ChartEditorState extends HaxeUIState function get_songLengthInMs():Float { - return songLengthInSteps * Conductor.stepCrochet; + return songLengthInSteps * Conductor.stepLengthMs; } function set_songLengthInMs(value:Float):Float @@ -335,6 +343,11 @@ class ChartEditorState extends HaxeUIState */ var currentOpponentCharacterPlayer:CharacterPlayer = null; + /** + * The currently selected live input style. + */ + var currentLiveInputStyle:LiveInputStyle = LiveInputStyle.None; + /** * Whether the current view is in downscroll mode. */ @@ -428,6 +441,22 @@ class ChartEditorState extends HaxeUIState return selectedDifficulty; } + /** + * The character ID for the character which is currently selected. + */ + var selectedCharacter(default, set):String = Constants.DEFAULT_CHARACTER; + + function set_selectedCharacter(value:String):String + { + selectedCharacter = value; + + // Make sure view is updated when the character changes. + noteDisplayDirty = true; + notePreviewDirty = true; + + return selectedCharacter; + } + /** * Whether the user is currently in Pattern Mode. * This overrides the chart editor's normal behavior. @@ -503,6 +532,18 @@ class ChartEditorState extends HaxeUIState */ var characterSelectDirty:Bool = true; + /** + * Whether the player preview toolbox have been modified and need to be updated. + * This happens when we switch characters. + */ + var playerPreviewDirty:Bool = true; + + /** + * Whether the opponent preview toolbox have been modified and need to be updated. + * This happens when we switch characters. + */ + var opponentPreviewDirty:Bool = true; + var isInPlaytestMode:Bool = false; /** @@ -581,7 +622,7 @@ class ChartEditorState extends HaxeUIState * The Dialog components representing the currently available tool windows. * Dialogs are retained here even when collapsed or hidden. */ - var activeToolboxes:Map = new Map(); + var activeToolboxes:Map = new Map(); /** * AUDIO AND SOUND DATA @@ -594,18 +635,19 @@ class ChartEditorState extends HaxeUIState var audioInstTrack:FlxSound; /** - * The audio track for the vocals. + * The raw byte data for the instrumental audio track. */ - var audioVocalTrackGroup:VoicesGroup; + var audioInstTrackData:Bytes = null; /** - * A map of the audio tracks for each character's vocals. - * - Keys are the character IDs. - * - Values are the FlxSound objects to play that character's vocals. - * - * When switching characters, the elements of the VoicesGroup will be swapped to match the new character. + * The audio track for the vocals. */ - var audioVocalTracks:Map = new Map(); + var audioVocalTrackGroup:VocalGroup; + + /** + * The raw byte data for the vocal audio tracks. + */ + var audioVocalTrackData:Map = []; /** * CHART DATA @@ -640,7 +682,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongMetadata():SongMetadata { - var result = songMetadata.get(selectedVariation); + var result:SongMetadata = songMetadata.get(selectedVariation); if (result == null) { result = new SongMetadata('Dad Battle', 'Kawai Sprite', selectedVariation); @@ -662,7 +704,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartData():SongChartData { - var result = songChartData.get(selectedVariation); + var result:SongChartData = songChartData.get(selectedVariation); if (result == null) { result = new SongChartData(1.0, [], []); @@ -684,7 +726,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartScrollSpeed():Float { - var result = currentSongChartData.scrollSpeed.get(selectedDifficulty); + var result:Null = currentSongChartData.scrollSpeed.get(selectedDifficulty); if (result == null) { // Initialize to the default value if not set. @@ -707,11 +749,12 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartNoteData():Array { - var result = currentSongChartData.notes.get(selectedDifficulty); + var result:Array = currentSongChartData.notes.get(selectedDifficulty); if (result == null) { // Initialize to the default value if not set. result = []; + trace('Initializing blank note data for difficulty ' + selectedDifficulty); currentSongChartData.notes.set(selectedDifficulty, result); return result; } @@ -819,6 +862,59 @@ class ChartEditorState extends HaxeUIState return currentSongMetadata.artist = value; } + var currentSongPlayableCharacters(get, null):Array; + + function get_currentSongPlayableCharacters():Array + { + return currentSongMetadata.playData.playableChars.keys().array(); + } + + var currentSongCharacterPlayer(get, set):String; + + function get_currentSongCharacterPlayer():String + { + // Validate selected character before returning it. + if (!currentSongPlayableCharacters.contains(selectedCharacter)) + { + trace('Invalid character selected: ' + selectedCharacter); + selectedCharacter = currentSongPlayableCharacters[0]; + } + + return selectedCharacter; + } + + function set_currentSongCharacterPlayer(value:String):String + { + if (!currentSongPlayableCharacters.contains(value)) + { + trace('Invalid character selected: ' + value); + return value; + } + + return selectedCharacter = value; + } + + var currentSongCharacterOpponent(get, set):String; + + function get_currentSongCharacterOpponent():String + { + // Validate selected character before returning it. + if (!currentSongPlayableCharacters.contains(selectedCharacter)) + { + trace('Invalid character selected: ' + selectedCharacter); + selectedCharacter = currentSongPlayableCharacters[0]; + } + + var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter); + return playableCharData.opponent; + } + + function set_currentSongCharacterOpponent(value:String):String + { + var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter); + return playableCharData.opponent = value; + } + /** * RENDER OBJECTS */ @@ -866,17 +962,11 @@ class ChartEditorState extends HaxeUIState */ var gridSpectrogram:PolygonSpectogram; - /** - * The rectangle used for the note preview area. - * 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; + var notePreview:ChartEditorNotePreview; /** * The rectangular sprite used for rendering the selection box. @@ -935,6 +1025,8 @@ class ChartEditorState extends HaxeUIState currentTheme = ChartEditorTheme.Light; buildGrid(); + // buildSpectrogram(audioInstTrack); + buildNotePreview(); buildSelectionBox(); // Add the HaxeUI components after the grid so they're on top. @@ -961,7 +1053,7 @@ class ChartEditorState extends HaxeUIState // Initialize the song chart data. songChartData = new Map(); - audioVocalTrackGroup = new VoicesGroup(); + audioVocalTrackGroup = new VocalGroup(); } /** @@ -990,13 +1082,13 @@ class ChartEditorState extends HaxeUIState gridGhostNote = new ChartEditorNoteSprite(this); gridGhostNote.alpha = 0.6; - gridGhostNote.noteData = new SongNoteData(-1, -1, 0, ""); + 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.eventData = new SongEventData(-1, '', {}); gridGhostEvent.visible = false; add(gridGhostEvent); @@ -1010,15 +1102,15 @@ class ChartEditorState extends HaxeUIState 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; + var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); + var playheadBaseYPos:Float = MENU_BAR_HEIGHT + GRID_TOP_PAD; gridPlayhead.setPosition(gridTiledSprite.x, playheadBaseYPos); - var playheadSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); + var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadSprite.y = 0; gridPlayhead.add(playheadSprite); - var playheadBlock = ChartEditorThemeHandler.buildPlayheadBlock(); + var playheadBlock:FlxSprite = ChartEditorThemeHandler.buildPlayheadBlock(); playheadBlock.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadBlock.y = -PLAYHEAD_HEIGHT / 2; gridPlayhead.add(playheadBlock); @@ -1048,7 +1140,7 @@ class ChartEditorState extends HaxeUIState setSelectionBoxBounds(); } - function setSelectionBoxBounds(?bounds:FlxRect = null):Void + function setSelectionBoxBounds(bounds:FlxRect = null):Void { if (bounds == null) { @@ -1066,17 +1158,21 @@ class ChartEditorState extends HaxeUIState } } + function buildNotePreview():Void + { + var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - 200; + notePreview = new ChartEditorNotePreview(height); + notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; + add(notePreview); + } + function buildSpectrogram(target:FlxSound):Void { - 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 = new PolygonSpectogram(FlxG.sound.music, FlxColor.RED, FlxG.height / 2, Math.floor(FlxG.height / 2)); + gridSpectrogram.x += 170; + gridSpectrogram.scrollFactor.set(); gridSpectrogram.waveAmplitude = 50; - gridSpectrogram.scrollFactor.set(0, 0); + gridSpectrogram.visType = UPDATED; add(gridSpectrogram); } @@ -1113,7 +1209,7 @@ class ChartEditorState extends HaxeUIState playbarHead.allowFocus = false; playbarHead.width = FlxG.width; playbarHead.height = 10; - playbarHead.styleString = "padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;"; + playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;'; playbarHead.onDragStart = function(_:DragEvent) { playbarHeadDragging = true; @@ -1142,11 +1238,17 @@ class ChartEditorState extends HaxeUIState if (playbarHeadDraggingWasPlaying) { playbarHeadDraggingWasPlaying = false; - startAudioPlayback(); + // Disabled code to resume song playback on drag. + // startAudioPlayback(); } } add(playbarHeadLayout); + + // Setup notifications. + @:privateAccess + // NotificationManager.GUTTER_SIZE = 56; + NotificationManager.GUTTER_SIZE = 20; } /** @@ -1165,8 +1267,10 @@ class ChartEditorState extends HaxeUIState // Add functionality to the menu items. addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); + addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseWizard(this, true)); addUIClickListener('menubarItemSaveChartAs', _ -> exportAllSongData()); addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true)); + addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true)); addUIClickListener('menubarItemUndo', _ -> undoLastCommand()); @@ -1174,10 +1278,21 @@ class ChartEditorState extends HaxeUIState addUIClickListener('menubarItemCopy', function(_) { // Doesn't use a command because it's not undoable. + + // Calculate a single time offset for all the notes and events. + var timeOffset:Null = currentNoteSelection.length > 0 ? Std.int(currentNoteSelection[0].time) : null; + if (currentEventSelection.length > 0) + { + if (timeOffset == null || currentEventSelection[0].time < timeOffset) + { + timeOffset = Std.int(currentEventSelection[0].time); + } + } + SongDataUtils.writeItemsToClipboard( { - notes: SongDataUtils.buildNoteClipboard(currentNoteSelection), - events: SongDataUtils.buildEventClipboard(currentEventSelection), + notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset), + events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset), }); }); @@ -1215,6 +1330,10 @@ class ChartEditorState extends HaxeUIState // addUIClickListener('menubarItemSelectBeforeCursor', _ -> doSomething()); // addUIClickListener('menubarItemSelectAfterCursor', _ -> doSomething()); + addUIChangeListener('menubarItemInputStyleGroup', function(event:UIEvent) { + trace('Change input style: ${event.target}'); + }); + addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this)); addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this)); @@ -1258,11 +1377,13 @@ class ChartEditorState extends HaxeUIState var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label); addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) { var pitch:Float = event.value * 2.0 / 100.0; + pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25. #if FLX_PITCH if (audioInstTrack != null) audioInstTrack.pitch = pitch; if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch; #end - playbackSpeedLabel.text = 'Playback Speed - ${Std.int(pitch * 100) / 100}x'; + var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places. + playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x'; }); addUIChangeListener('menubarItemToggleToolboxTools', @@ -1371,21 +1492,7 @@ class ChartEditorState extends HaxeUIState // DEBUG #if debug - if (FlxG.keys.justPressed.F) - { - NotificationManager.instance.addNotification( - { - title: 'This is a Notification', - body: 'Hello, world!', - type: NotificationType.Info, - expiryMs: NOTIFICATION_DISMISS_TIME - // styleNames: 'cssStyleName', - // icon: 'assetPath', - // actions: ['action1', 'action2'] - }); - } - - if (FlxG.keys.justPressed.E) + if (FlxG.keys.justPressed.E && !isHaxeUIDialogOpen) { currentSongMetadata.timeChanges[0].timeSignatureNum = (currentSongMetadata.timeChanges[0].timeSignatureNum == 4 ? 3 : 4); } @@ -1394,9 +1501,9 @@ class ChartEditorState extends HaxeUIState // 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; + var baseHealthIconXPos:Float = 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); + var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); healthIconBF.x = baseHealthIconXPos - healthIconOffset; } @@ -1430,8 +1537,9 @@ class ChartEditorState extends HaxeUIState healthIconBF.onStepHit(Conductor.currentStep); } - // if (shouldPlayMetronome) - // playMetronomeTick(false); + // Updating these every step keeps it more accurate. + // playerPreviewDirty = true; + // opponentPreviewDirty = true; return true; } @@ -1444,29 +1552,36 @@ class ChartEditorState extends HaxeUIState // Don't scroll when the cursor is over the UI. if (isCursorOverHaxeUI) return; - // Amount to scroll the grid. - var scrollAmount:Float = 0; - // Amount to scroll the playhead relative to the grid. - var playheadAmount:Float = 0; - var shouldPause:Bool = false; + var scrollAmount:Float = 0; // Amount to scroll the grid. + var playheadAmount:Float = 0; // Amount to scroll the playhead relative to the grid. + var shouldPause:Bool = false; // Whether to pause the song when scrolling. + var shouldEase:Bool = false; // Whether to ease the scroll. // Up Arrow = Scroll Up - if (upKeyHandler.activated) + if (upKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD) { scrollAmount = -GRID_SIZE * 0.25 * 5.0; shouldPause = true; } // Down Arrow = Scroll Down - if (downKeyHandler.activated) + if (downKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD) { scrollAmount = GRID_SIZE * 0.25 * 5.0; shouldPause = true; } - // PAGE UP = Jump Up 1 Measure + // PAGE UP = Jump up to nearest measure if (pageUpKeyHandler.activated) { - scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var targetScrollPosition:Float = Math.floor(scrollPositionInPixels / measureHeight) * measureHeight; + // If we would move less than one grid, instead move to the top of the previous measure. + if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE) + { + targetScrollPosition -= GRID_SIZE * 4 * Conductor.beatsPerMeasure; + } + scrollAmount = targetScrollPosition - scrollPositionInPixels; + shouldPause = true; } if (playbarButtonPressed == 'playbarBack') @@ -1476,10 +1591,18 @@ class ChartEditorState extends HaxeUIState shouldPause = true; } - // PAGE DOWN = Jump Down 1 Measure + // PAGE DOWN = Jump down to nearest measure if (pageDownKeyHandler.activated) { - scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var targetScrollPosition:Float = Math.ceil(scrollPositionInPixels / measureHeight) * measureHeight; + // If we would move less than one grid, instead move to the top of the next measure. + if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE) + { + targetScrollPosition += GRID_SIZE * 4 * Conductor.beatsPerMeasure; + } + scrollAmount = targetScrollPosition - scrollPositionInPixels; + shouldPause = true; } if (playbarButtonPressed == 'playbarForward') @@ -1557,12 +1680,26 @@ class ChartEditorState extends HaxeUIState shouldPause = true; } - // Apply the scroll amount. - this.scrollPositionInPixels += scrollAmount; - this.playheadPositionInPixels += playheadAmount; + if (Math.abs(scrollAmount) > GRID_SIZE * 8) + { + shouldEase = true; + } // Resync the conductor and audio tracks. - if (scrollAmount != 0 || playheadAmount != 0) moveSongToScrollPosition(); + if (scrollAmount != 0 || playheadAmount != 0) + { + this.playheadPositionInPixels += playheadAmount; + if (shouldEase) + { + easeSongToScrollPosition(this.scrollPositionInPixels + scrollAmount); + } + else + { + // Apply the scroll amount. + this.scrollPositionInPixels += scrollAmount; + moveSongToScrollPosition(); + } + } if (shouldPause) stopAudioPlayback(); } @@ -1608,8 +1745,8 @@ class ChartEditorState extends HaxeUIState function handleCursor():Void { // 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; + var shouldHandleCursor:Bool = !isCursorOverHaxeUI || (selectionBoxStartPos != null); + var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1; if (shouldHandleCursor) { @@ -1619,7 +1756,7 @@ class ChartEditorState extends HaxeUIState var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x; var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y; - var overlapsSelectionBorder = overlapsGrid + var overlapsSelectionBorder:Bool = overlapsGrid && (cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorX % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)) || (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)); @@ -1634,6 +1771,13 @@ class ChartEditorState extends HaxeUIState { selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); } + else + { + trace('Clicked outside grid, deselecting all items.'); + + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } if (gridPlayheadScrollAreaPressed) @@ -1667,7 +1811,7 @@ class ChartEditorState extends HaxeUIState // 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); + var cursorMs:Float = cursorStep * Conductor.stepLengthMs * (16 / noteSnapQuant); // The direction value for the column at the cursor. var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE); if (cursorColumn < 0) cursorColumn = 0; @@ -1705,7 +1849,7 @@ class ChartEditorState extends HaxeUIState // 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 cursorMsStart:Float = cursorStepStart * Conductor.stepLengthMs; var cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE); var cursorColumnBaseStart:Int = Math.floor(cursorXStart / GRID_SIZE); @@ -1770,6 +1914,13 @@ class ChartEditorState extends HaxeUIState else { // We made a selection box, but it didn't select anything. + + if (!FlxG.keys.pressed.CONTROL) + { + trace('Clicked and dragged outside grid, deselecting all items.'); + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } } else @@ -1783,8 +1934,26 @@ class ChartEditorState extends HaxeUIState } else { + // Scroll the screen if the mouse is above or below the grid. + if (FlxG.mouse.screenY < MENU_BAR_HEIGHT) + { + // Scroll up. + var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY; + scrollPositionInPixels -= diff * 0.5; // Too fast! + trace('Scroll up: ' + diff); + moveSongToScrollPosition(); + } + else if (FlxG.mouse.screenY > playbarHeadLayout.y) + { + // Scroll down. + var diff:Float = FlxG.mouse.screenY - playbarHeadLayout.y; + scrollPositionInPixels += diff * 0.5; // Too fast! + trace('Scroll down: ' + diff); + moveSongToScrollPosition(); + } + // Render the selection box. - var selectionRect = new FlxRect(); + var selectionRect:FlxRect = new FlxRect(); selectionRect.x = Math.min(FlxG.mouse.screenX, selectionBoxStartPos.x); selectionRect.y = Math.min(FlxG.mouse.screenY, selectionBoxStartPos.y); selectionRect.width = Math.abs(FlxG.mouse.screenX - selectionBoxStartPos.x); @@ -1869,7 +2038,14 @@ class ChartEditorState extends HaxeUIState } else { - // If we clicked and released outside the grid, do nothing. + // If we clicked and released outside the grid. + + if (!FlxG.keys.pressed.CONTROL) + { + trace('Clicked outside grid, deselecting all items.'); + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } } } @@ -1877,12 +2053,12 @@ class ChartEditorState extends HaxeUIState { // 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); + // Since use Math.floor and stepLengthMs here, the hold notes will be beat snapped. + var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs); // 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; + var dragLengthMs:Float = (dragLengthSteps + INCREMENT) * Conductor.stepLengthMs; // TODO: Add and update some sort of preview? @@ -2187,10 +2363,10 @@ class ChartEditorState extends HaxeUIState } // Get the position the note should be at. - var noteTimePixels:Float = noteData.time / Conductor.stepCrochet * GRID_SIZE; + var noteTimePixels:Float = noteData.time / Conductor.stepLengthMs * GRID_SIZE; // Make sure the note appears when scrolling up. - var modifiedViewAreaTop = viewAreaTop - GRID_SIZE; + var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE; if (noteTimePixels < modifiedViewAreaTop || noteTimePixels > viewAreaBottom) continue; @@ -2213,7 +2389,7 @@ class ChartEditorState extends HaxeUIState { // 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 noteLengthSteps:Float = (noteLengthMs / Conductor.stepLengthMs); var lastNoteSprite:ChartEditorNoteSprite = noteSprite; while (noteLengthSteps > 0) @@ -2237,7 +2413,7 @@ class ChartEditorState extends HaxeUIState // Make sure the last note sprite shows the end cap properly. lastNoteSprite.childNoteSprite = null; - // var noteLengthPixels:Float = (noteLengthMs / Conductor.stepCrochet + 1) * GRID_SIZE; + // var noteLengthPixels:Float = (noteLengthMs / Conductor.stepLengthMs + 1) * GRID_SIZE; // add(new FlxSprite(noteSprite.x, noteSprite.y - renderedNotes.y + noteLengthPixels).makeGraphic(40, 2, 0xFFFF0000)); } } @@ -2252,10 +2428,10 @@ class ChartEditorState extends HaxeUIState } // Get the position the event should be at. - var eventTimePixels:Float = eventData.time / Conductor.stepCrochet * GRID_SIZE; + var eventTimePixels:Float = eventData.time / Conductor.stepLengthMs * GRID_SIZE; // Make sure the event appears when scrolling up. - var modifiedViewAreaTop = viewAreaTop - GRID_SIZE; + var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE; if (eventTimePixels < modifiedViewAreaTop || eventTimePixels > viewAreaBottom) continue; @@ -2334,8 +2510,8 @@ class ChartEditorState extends HaxeUIState playbarHeadLayout.x = 4; playbarHeadLayout.y = FlxG.height - 48 - 8; - var songPos = Conductor.songPosition; - var songRemaining = songLengthInMs - songPos; + var songPos:Float = Conductor.songPosition; + var songRemaining:Float = Math.max(songLengthInMs - songPos, 0.0); // Move the playhead to match the song position, if we aren't dragging it. if (!playbarHeadDragging) @@ -2480,7 +2656,7 @@ class ChartEditorState extends HaxeUIState difficultySelectDirty = false; // Manage the Select Difficulty tree view. - var difficultyToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (difficultyToolbox == null) return; var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree'); @@ -2489,30 +2665,28 @@ class ChartEditorState extends HaxeUIState // Clear the tree view so we can rebuild it. treeView.clearNodes(); - var treeSong = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: "haxeui-core/styles/default/haxeui_tiny.png"}); + var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: 'haxeui-core/styles/default/haxeui_tiny.png'}); treeSong.expanded = true; for (curVariation in availableVariations) { var variationMetadata:SongMetadata = songMetadata.get(curVariation); - var treeVariation = treeSong.addNode( + var treeVariation:TreeViewNode = treeSong.addNode( { id: 'stv_variation_$curVariation', - text: 'V: ${curVariation.toTitleCase()}', - // icon: "haxeui-core/styles/default/haxeui_tiny.png" + text: 'V: ${curVariation.toTitleCase()}' }); treeVariation.expanded = true; - var difficultyList = variationMetadata.playData.difficulties; + var difficultyList:Array = variationMetadata.playData.difficulties; for (difficulty in difficultyList) { - var treeDifficulty = treeVariation.addNode( + var _treeDifficulty:TreeViewNode = treeVariation.addNode( { id: 'stv_difficulty_${curVariation}_$difficulty', - text: 'D: ${difficulty.toTitleCase()}', - // icon: "haxeui-core/styles/default/haxeui_tiny.png" + text: 'D: ${difficulty.toTitleCase()}' }); } } @@ -2525,25 +2699,71 @@ class ChartEditorState extends HaxeUIState function handlePlayerPreviewToolbox():Void { // Manage the Select Difficulty tree view. - var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); + var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer'); if (charPlayer == null) return; currentPlayerCharacterPlayer = charPlayer; + + if (playerPreviewDirty) + { + playerPreviewDirty = false; + + if (currentSongCharacterPlayer != charPlayer.charId) + { + healthIconBF.characterId = currentSongCharacterPlayer; + + charPlayer.loadCharacter(currentSongCharacterPlayer); + charPlayer.characterType = CharacterType.BF; + charPlayer.flip = true; + charPlayer.targetScale = 0.5; + + charPreviewToolbox.title = 'Player Preview - ${charPlayer.charName}'; + } + + if (charPreviewToolbox != null && !charPreviewToolbox.minimized) + { + charPreviewToolbox.width = charPlayer.width + 32; + charPreviewToolbox.height = charPlayer.height + 64; + } + } } function handleOpponentPreviewToolbox():Void { // Manage the Select Difficulty tree view. - var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); + var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer'); if (charPlayer == null) return; currentOpponentCharacterPlayer = charPlayer; + + if (opponentPreviewDirty) + { + opponentPreviewDirty = false; + + if (currentSongCharacterOpponent != charPlayer.charId) + { + healthIconDad.characterId = currentSongCharacterOpponent; + + charPlayer.loadCharacter(currentSongCharacterOpponent); + charPlayer.characterType = CharacterType.DAD; + charPlayer.flip = false; + charPlayer.targetScale = 0.5; + + charPreviewToolbox.title = 'Opponent Preview - ${charPlayer.charName}'; + } + + if (charPreviewToolbox != null && !charPreviewToolbox.minimized) + { + charPreviewToolbox.width = charPlayer.width + 32; + charPreviewToolbox.height = charPlayer.height + 64; + } + } } override function dispatchEvent(event:ScriptEvent):Void @@ -2588,7 +2808,8 @@ class ChartEditorState extends HaxeUIState if (treeView == null) return null; - var result = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty', 'id'); + var result:TreeViewNode = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty', + 'id'); if (result == null) return null; @@ -2612,8 +2833,8 @@ class ChartEditorState extends HaxeUIState switch (targetNode.data.id.split('_')[1]) { case 'difficulty': - var variation = targetNode.data.id.split('_')[2]; - var difficulty = targetNode.data.id.split('_')[3]; + var variation:String = targetNode.data.id.split('_')[2]; + var difficulty:String = targetNode.data.id.split('_')[3]; if (variation != null && difficulty != null) { @@ -2660,12 +2881,10 @@ class ChartEditorState extends HaxeUIState { 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); + // TODO: Only update the notes that have changed. + notePreview.erase(); + notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); + notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); } } @@ -2694,7 +2913,7 @@ class ChartEditorState extends HaxeUIState { // Disable the Undo button. undoButton.disabled = true; - undoButton.text = "Undo"; + undoButton.text = 'Undo'; } else { @@ -2705,7 +2924,7 @@ class ChartEditorState extends HaxeUIState } else { - trace("undoButton is null"); + trace('undoButton is null'); } var redoButton:MenuItem = findComponent('menubarItemRedo', MenuItem); @@ -2716,7 +2935,7 @@ class ChartEditorState extends HaxeUIState { // Disable the Redo button. redoButton.disabled = true; - redoButton.text = "Redo"; + redoButton.text = 'Redo'; } else { @@ -2727,7 +2946,7 @@ class ChartEditorState extends HaxeUIState } else { - trace("redoButton is null"); + trace('redoButton is null'); } } } @@ -2743,13 +2962,16 @@ class ChartEditorState extends HaxeUIState { // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! - var oldStepTime = Conductor.currentStepTime; - var oldSongPosition = Conductor.songPosition; + var oldStepTime:Float = Conductor.currentStepTime; + var oldSongPosition:Float = Conductor.songPosition; Conductor.update(audioInstTrack.time); handleHitsounds(oldSongPosition, Conductor.songPosition); // Resync vocals. - if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time; - var diffStepTime = Conductor.currentStepTime - oldStepTime; + if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) + { + audioVocalTrackGroup.time = audioInstTrack.time; + } + var diffStepTime:Float = Conductor.currentStepTime - oldStepTime; // Move the playhead. playheadPositionInPixels += diffStepTime * GRID_SIZE; @@ -2759,12 +2981,14 @@ class ChartEditorState extends HaxeUIState else { // Else, move the entire view. - var oldSongPosition = Conductor.songPosition; + var oldSongPosition:Float = Conductor.songPosition; Conductor.update(audioInstTrack.time); handleHitsounds(oldSongPosition, Conductor.songPosition); // Resync vocals. - if (audioVocalTrackGroup != null - && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time; + if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) + { + audioVocalTrackGroup.time = audioInstTrack.time; + } // We need time in fractional steps here to allow the song to actually play. // Also account for a potentially offset playhead. @@ -2830,7 +3054,6 @@ class ChartEditorState extends HaxeUIState { if (audioInstTrack != null) audioInstTrack.play(); if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(); setComponentText('playbarPlay', '||'); } @@ -2839,7 +3062,6 @@ class ChartEditorState extends HaxeUIState { if (audioInstTrack != null) audioInstTrack.pause(); if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); setComponentText('playbarPlay', '>'); } @@ -2861,23 +3083,42 @@ class ChartEditorState extends HaxeUIState function handlePlayhead():Void { // Place notes at the playhead. - // TODO: Add the ability to switch modes. - if (true) + switch (currentLiveInputStyle) { - 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); + case LiveInputStyle.WASD: + if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(0); + if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(1); + if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(2); + if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(3); + + if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(4); + if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(5); + if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(6); + if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(7); + case LiveInputStyle.NumberKeys: + 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); + case LiveInputStyle.None: + // Do nothing. } } function placeNoteAtPlayhead(column:Int):Void { - var gridSnappedPlayheadPos = scrollPositionInPixels - (scrollPositionInPixels % GRID_SIZE); + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; + var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant); + var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); + var playheadPosMs:Float = playheadPosStep * Conductor.stepLengthMs * (16 / noteSnapQuant); + + var newNoteData:SongNoteData = new SongNoteData(playheadPosMs, column, 0, selectedNoteKind); + performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); } function set_scrollPositionInPixels(value:Float):Float @@ -2888,7 +3129,7 @@ class ChartEditorState extends HaxeUIState // but the playhead is in the middle, move the playhead up. if (playheadPositionInPixels > 0) { - var amount = scrollPositionInPixels - value; + var amount:Float = scrollPositionInPixels - value; playheadPositionInPixels -= amount; } @@ -2899,6 +3140,9 @@ class ChartEditorState extends HaxeUIState if (value == scrollPositionInPixels) return value; + // Difference in pixels. + var diff:Float = value - scrollPositionInPixels; + this.scrollPositionInPixels = value; // Move the grid sprite to the correct position. @@ -2914,12 +3158,9 @@ class ChartEditorState extends HaxeUIState 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); - } + + // Offset the selection box start position, if we are dragging. + if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; return this.scrollPositionInPixels; } @@ -2945,7 +3186,7 @@ class ChartEditorState extends HaxeUIState /** * Loads an instrumental from an absolute file path, replacing the current instrumental. - * + * * @param path The absolute path to the audio file. * @return Success or failure. */ @@ -2974,12 +3215,19 @@ class ChartEditorState extends HaxeUIState */ public function loadInstrumentalFromBytes(bytes:haxe.io.Bytes, fileName:String = null):Bool { + if (bytes == null) + { + return false; + } + var openflSound:openfl.media.Sound = 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(); + audioInstTrackData = bytes; + postLoadInstrumental(); return true; @@ -2997,6 +3245,8 @@ class ChartEditorState extends HaxeUIState { audioInstTrack = instTrack; + audioInstTrackData = Assets.getBytes(path); + postLoadInstrumental(); return true; } @@ -3004,7 +3254,7 @@ class ChartEditorState extends HaxeUIState return false; } - function postLoadInstrumental():Void + public function postLoadInstrumental():Void { // Prevent the time from skipping back to 0 when the song ends. audioInstTrack.onComplete = function() { @@ -3015,11 +3265,8 @@ class ChartEditorState extends HaxeUIState songLengthInMs = audioInstTrack.length; gridTiledSprite.height = songLengthInPixels; - if (gridSpectrogram != null) - { - gridSpectrogram.setSound(audioInstTrack); - gridSpectrogram.generateSection(0, songLengthInMs / 1000); - } + + buildSpectrogram(audioInstTrack); scrollPositionInPixels = 0; playheadPositionInPixels = 0; @@ -3030,8 +3277,9 @@ class ChartEditorState extends HaxeUIState * Loads a vocal track from an absolute file path. * @param path The absolute path to the audio file. * @param charKey The character to load the vocal track for. + * @return Success or failure. */ - public function loadVocalsFromPath(path:Path, charKey:String = null):Bool + public function loadVocalsFromPath(path:Path, charKey:String = 'default'):Bool { #if sys var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString()); @@ -3042,12 +3290,21 @@ class ChartEditorState extends HaxeUIState #end } - public function loadVocalsFromAsset(path:String, charKey:String = null):Bool + /** + * Loads a vocal track from an OpenFL asset. + * @param path ID of the asset. + * @param charKey Character to load the vocal track for. + * @return Success or failure. + */ + public function loadVocalsFromAsset(path:String, charKey:String = 'default'):Bool { var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false); if (vocalTrack != null) { audioVocalTrackGroup.add(vocalTrack); + + audioVocalTrackData.set(charKey, Assets.getBytes(path)); + return true; } return false; @@ -3058,10 +3315,11 @@ class ChartEditorState extends HaxeUIState */ public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = null):Bool { - var openflSound = new openfl.media.Sound(); + var openflSound:openfl.media.Sound = 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); + audioVocalTrackData.set(charKey, bytes); return true; } @@ -3074,45 +3332,84 @@ class ChartEditorState extends HaxeUIState if (song == null) { - // showNotification('Failed to load song.'); return; } // Load the song metadata. var rawSongMetadata:Array = song.getRawMetadata(); - var songName:String = rawSongMetadata[0].songName; - - this.songMetadata = new Map(); + var songMetadata:Map = []; + var songChartData:Map = []; for (metadata in rawSongMetadata) { - var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; - this.songMetadata.set(variation, metadata); + var variation:String = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; + + songMetadata.set(variation, metadata); + songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation)); } - this.songChartData = new Map(); + loadSong(songMetadata, songChartData); - for (metadata in rawSongMetadata) + if (audioInstTrack != null) { - var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; - this.songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation)); + audioInstTrack.stop(); + audioInstTrack = null; } - - Conductor.forceBPM(null); // Disable the forced BPM. - Conductor.mapTimeChanges(currentSongMetadata.timeChanges); - loadInstrumentalFromAsset(Paths.inst(songId)); - loadVocalsFromAsset(Paths.voices(songId)); + + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.stop(); + audioVocalTrackGroup.clear(); + } + // Add player vocals. + if (currentSongCharacterPlayer != null) audioVocalTrackGroup.setPlayerVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, + '-$currentSongCharacterPlayer')))); + // Add opponent vocals. + if (currentSongCharacterOpponent != null) audioVocalTrackGroup.setOpponentVocals(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId, + '-$currentSongCharacterOpponent')))); + + postLoadInstrumental(); NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded song ($songName)', + body: 'Loaded song (${rawSongMetadata[0].songName})', type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); } + /** + * Loads song metadata and chart data into the editor. + * @param newSongMetadata The song metadata to load. + * @param newSongChartData The song chart data to load. + */ + public function loadSong(newSongMetadata:Map, newSongChartData:Map):Void + { + this.songMetadata = newSongMetadata; + this.songChartData = newSongChartData; + + Conductor.forceBPM(null); // Disable the forced BPM. + Conductor.mapTimeChanges(currentSongMetadata.timeChanges); + + difficultySelectDirty = true; + opponentPreviewDirty = true; + playerPreviewDirty = true; + + // Remove instrumental and vocal tracks, they will be loaded next. + if (audioInstTrack != null) + { + audioInstTrack.stop(); + audioInstTrack = null; + } + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.stop(); + audioVocalTrackGroup.clear(); + } + } + /** * When setting the scroll position, except when automatically scrolling during song playback, * we need to update the conductor's current step time and the timestamp of the audio tracks. @@ -3130,9 +3427,42 @@ class ChartEditorState extends HaxeUIState noteDisplayDirty = true; } + var currentScrollEase:VarTween; + + function easeSongToScrollPosition(targetScrollPosition:Float):Void + { + if (currentScrollEase != null) cancelScrollEase(currentScrollEase); + + currentScrollEase = FlxTween.tween(this, {scrollPositionInPixels: targetScrollPosition}, SCROLL_EASE_DURATION, + { + ease: FlxEase.quintInOut, + onUpdate: this.onScrollEaseUpdate, + onComplete: this.cancelScrollEase, + type: ONESHOT + }); + } + + function onScrollEaseUpdate(_:FlxTween):Void + { + moveSongToScrollPosition(); + } + + function cancelScrollEase(_:FlxTween):Void + { + if (currentScrollEase != null) + { + @:privateAccess + var targetScrollPosition:Float = currentScrollEase._properties.scrollPositionInPixels; + + currentScrollEase.cancel(); + currentScrollEase = null; + this.scrollPositionInPixels = targetScrollPosition; + } + } + /** * Perform (or redo) a command, then add it to the undo stack. - * + * * @param command The command to perform. * @param purgeRedoStack If true, the redo stack will be cleared. */ @@ -3248,11 +3578,11 @@ class ChartEditorState extends HaxeUIState */ public function exportAllSongData(?force:Bool = false, ?tmp:Bool = false):Void { - var zipEntries = []; + var zipEntries:Array = []; for (variation in availableVariations) { - var variationId = variation; + var variationId:String = variation; if (variation == '' || variation == 'default' || variation == 'normal') { variationId = ''; @@ -3260,21 +3590,25 @@ class ChartEditorState extends HaxeUIState if (variationId == '') { - var variationMetadata = songMetadata.get(variation); + var variationMetadata:SongMetadata = songMetadata.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata))); - var variationChart = songChartData.get(variation); + var variationChart:SongChartData = songChartData.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart))); } else { - var variationMetadata = songMetadata.get(variation); + var variationMetadata:SongMetadata = songMetadata.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json', SerializerUtil.toJSON(variationMetadata))); - var variationChart = songChartData.get(variation); + var variationChart:SongChartData = songChartData.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart))); } } - // TODO: Add audio files to the ZIP. + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData)); + for (charId in audioVocalTrackData.keys()) + { + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', audioVocalTrackData.get(charId))); + } trace('Exporting ${zipEntries.length} files to ZIP...'); @@ -3307,3 +3641,10 @@ class ChartEditorState extends HaxeUIState FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '$currentSongId-chart.zip'); } } + +enum LiveInputStyle +{ + None; + NumberKeys; + WASD; +}