diff --git a/hmm.json b/hmm.json index 57c9378aa..0f06acaa7 100644 --- a/hmm.json +++ b/hmm.json @@ -49,7 +49,7 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "91ed8d7867c52af5ea2a9513204057d69ab33c8e", + "ref": "5d4ac180f85b39e72624f4b8d17925d91ebe4278", "url": "https://github.com/haxeui/haxeui-core" }, { diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index 9a9f758b1..4ae4b1426 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -32,10 +32,7 @@ class SongDataUtils return new SongNoteData(time, data, length, kind); }; - trace(notes); - trace(notes[0]); var result = [for (i in 0...notes.length) offsetNote(notes[i])]; - trace(result); return result; } @@ -54,6 +51,36 @@ class SongDataUtils }); } + /** + * Given an array of SongNoteData objects, return a new array of SongNoteData objects + * which excludes any notes whose timestamps are outside of the given range. + * @param notes The notes to modify. + * @param startTime The start of the range in milliseconds. + * @param endTime The end of the range in milliseconds. + * @return The filtered array of notes. + */ + public static function clampSongNoteData(notes:Array<SongNoteData>, startTime:Float, endTime:Float):Array<SongNoteData> + { + return notes.filter(function(note:SongNoteData):Bool { + return note.time >= startTime && note.time <= endTime; + }); + } + + /** + * Given an array of SongEventData objects, return a new array of SongEventData objects + * which excludes any events whose timestamps are outside of the given range. + * @param events The events to modify. + * @param startTime The start of the range in milliseconds. + * @param endTime The end of the range in milliseconds. + * @return The filtered array of events. + */ + public static function clampSongEventData(events:Array<SongEventData>, startTime:Float, endTime:Float):Array<SongEventData> + { + return events.filter(function(event:SongEventData):Bool { + return event.time >= startTime && event.time <= endTime; + }); + } + /** * Return a new array without a certain subset of notes from an array of SongNoteData objects. * Does not mutate the original array. diff --git a/source/funkin/input/Cursor.hx b/source/funkin/input/Cursor.hx index c609c9e30..b4bf43808 100644 --- a/source/funkin/input/Cursor.hx +++ b/source/funkin/input/Cursor.hx @@ -142,12 +142,14 @@ class Cursor }; static var assetCursorCell:Null<BitmapData> = null; - // DESIRED CURSOR: Resize NS (vertical) - // DESIRED CURSOR: Resize EW (horizontal) - // DESIRED CURSOR: Resize NESW (diagonal) - // DESIRED CURSOR: Resize NWSE (diagonal) - // DESIRED CURSOR: Help (Cursor with question mark) - // DESIRED CURSOR: Menu (Cursor with menu icon) + public static final CURSOR_SCROLL_PARAMS:CursorParams = + { + graphic: "assets/images/cursor/cursor-scroll.png", + scale: 0.2, + offsetX: -15, + offsetY: -15, + }; + static var assetCursorScroll:Null<BitmapData> = null; static function set_cursorMode(value:Null<CursorMode>):Null<CursorMode> { @@ -304,6 +306,18 @@ class Cursor applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS); } + case Scroll: + if (assetCursorScroll == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_SCROLL_PARAMS.graphic); + assetCursorScroll = bitmapData; + applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS); + } + else + { + applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS); + } + default: setCursorGraphic(null); } @@ -487,6 +501,21 @@ class Cursor applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS); } + case Scroll: + if (assetCursorScroll == null) + { + var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_SCROLL_PARAMS.graphic); + future.onComplete(function(bitmapData:BitmapData) { + assetCursorScroll = bitmapData; + applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS); + }); + future.onError(onCursorError.bind(Scroll)); + } + else + { + applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS); + } + default: loadCursorGraphic(null); } @@ -517,6 +546,7 @@ class Cursor registerHaxeUICursor('zoom-out', CURSOR_ZOOM_OUT_PARAMS); registerHaxeUICursor('crosshair', CURSOR_CROSSHAIR_PARAMS); registerHaxeUICursor('cell', CURSOR_CELL_PARAMS); + registerHaxeUICursor('scroll', CURSOR_SCROLL_PARAMS); } public static function registerHaxeUICursor(id:String, params:CursorParams):Void @@ -539,6 +569,7 @@ enum CursorMode ZoomOut; Crosshair; Cell; + Scroll; } /** diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 6136cf1b7..a26addbe6 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -483,6 +483,8 @@ class PlayState extends MusicBeatSubState var generatedMusic:Bool = false; var perfectMode:Bool = false; + static final BACKGROUND_COLOR:FlxColor = FlxColor.MAGENTA; + /** * Instantiate a new PlayState. * @param params The parameters used to initialize the PlayState. @@ -647,6 +649,24 @@ class PlayState extends MusicBeatSubState initialized = true; } + public override function draw():Void + { + // if (FlxG.renderBlit) + // { + // camGame.fill(BACKGROUND_COLOR); + // } + // else if (FlxG.renderTile) + // { + // FlxG.log.warn("PlayState background not displayed properly on tile renderer!"); + // } + // else + // { + // FlxG.log.warn("PlayState background not displayed properly, unknown renderer!"); + // } + + super.draw(); + } + function assertChartExists():Bool { // Returns null if the song failed to load or doesn't have the selected difficulty. @@ -1297,6 +1317,7 @@ class PlayState extends MusicBeatSubState function initCameras():Void { camGame = new SwagCamera(); + camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage. camHUD = new FlxCamera(); camHUD.bgColor.alpha = 0; // Show the game scene behind the camera. camCutscene = new FlxCamera(); diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index db1f2b69a..810d0fd93 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -105,7 +105,7 @@ abstract Save(RawSaveData) theme: ChartEditorTheme.Light, playtestStartTime: false, downscroll: false, - metronomeEnabled: true, + metronomeVolume: 1.0, hitsoundsEnabledPlayer: true, hitsoundsEnabledOpponent: true, instVolume: 1.0, @@ -279,21 +279,38 @@ abstract Save(RawSaveData) return this.optionsChartEditor.theme; } - public var chartEditorMetronomeEnabled(get, set):Bool; + public var chartEditorMetronomeVolume(get, set):Float; - function get_chartEditorMetronomeEnabled():Bool + function get_chartEditorMetronomeVolume():Float { - if (this.optionsChartEditor.metronomeEnabled == null) this.optionsChartEditor.metronomeEnabled = true; + if (this.optionsChartEditor.metronomeVolume == null) this.optionsChartEditor.metronomeVolume = 1.0; - return this.optionsChartEditor.metronomeEnabled; + return this.optionsChartEditor.metronomeVolume; } - function set_chartEditorMetronomeEnabled(value:Bool):Bool + function set_chartEditorMetronomeVolume(value:Float):Float { // Set and apply. - this.optionsChartEditor.metronomeEnabled = value; + this.optionsChartEditor.metronomeVolume = value; flush(); - return this.optionsChartEditor.metronomeEnabled; + return this.optionsChartEditor.metronomeVolume; + } + + public var chartEditorHitsoundVolume(get, set):Float; + + function get_chartEditorHitsoundVolume():Float + { + if (this.optionsChartEditor.hitsoundVolume == null) this.optionsChartEditor.hitsoundVolume = 1.0; + + return this.optionsChartEditor.hitsoundVolume; + } + + function set_chartEditorHitsoundVolume(value:Float):Float + { + // Set and apply. + this.optionsChartEditor.hitsoundVolume = value; + flush(); + return this.optionsChartEditor.hitsoundVolume; } public var chartEditorHitsoundsEnabledPlayer(get, set):Bool; @@ -981,10 +998,16 @@ typedef SaveDataChartEditorOptions = var ?downscroll:Bool; /** - * Metronome sounds in the Chart Editor. - * @default `true` + * Metronome volume in the Chart Editor. + * @default `1.0` */ - var ?metronomeEnabled:Bool; + var ?metronomeVolume:Float; + + /** + * Hitsound volume in the Chart Editor. + * @default `1.0` + */ + var ?hitsoundVolume:Float; /** * If true, playtest songs from the current position in the Chart Editor. diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 519d1a54a..7c5d864cc 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -92,6 +92,7 @@ import haxe.ui.backend.flixel.UIRuntimeState; import haxe.ui.backend.flixel.UIState; import haxe.ui.components.DropDown; import haxe.ui.components.Label; +import haxe.ui.components.Button; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; @@ -100,6 +101,7 @@ import haxe.ui.containers.Frame; import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuItem; +import haxe.ui.containers.menus.MenuCheckBox; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; import haxe.ui.core.Component; @@ -603,9 +605,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Audio /** - * Whether to play a metronome sound while the playhead is moving. + * Whether to play a metronome sound while the playhead is moving, and what volume. */ - var isMetronomeEnabled:Bool = true; + var metronomeVolume:Float = 1.0; + + /** + * The volume to play hitsounds at. + */ + var hitsoundVolume:Float = 1.0; /** * Whether hitsounds are enabled for the player. @@ -653,6 +660,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var currentScrollEase:Null<VarTween>; + /** + * The position where the user middle clicked to place a scroll anchor. + * Scroll each frame with speed based on the distance between the mouse and the scroll anchor. + * `null` if no scroll anchor is present. + */ + var scrollAnchorScreenPos:Null<FlxPoint> = null; + // Note Placement /** @@ -1230,98 +1244,257 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var playbarHeadLayout:Null<ChartEditorPlaybarHead> = null; // NOTE: All the components below are automatically assigned via HaxeUI macros. + /** * The menubar at the top of the screen. */ - // var menubar:MenuBar; + var menubar:MenuBar; + /** * The `File -> New Chart` menu item. */ - // var menubarItemNewChart:MenuItem; + var menubarItemNewChart:MenuItem; + /** * The `File -> Open Chart` menu item. */ - // var menubarItemOpenChart:MenuItem; + var menubarItemOpenChart:MenuItem; + /** * The `File -> Open Recent` menu. */ - // var menubarOpenRecent:Menu; + var menubarOpenRecent:Menu; + /** * The `File -> Save Chart` menu item. */ - // var menubarItemSaveChart:MenuItem; + var menubarItemSaveChart:MenuItem; + /** * The `File -> Save Chart As` menu item. */ - // var menubarItemSaveChartAs:MenuItem; + var menubarItemSaveChartAs:MenuItem; + /** * The `File -> Preferences` menu item. */ - // var menubarItemPreferences:MenuItem; + var menubarItemPreferences:MenuItem; + /** * The `File -> Exit` menu item. */ - // var menubarItemExit:MenuItem; + var menubarItemExit:MenuItem; + /** * The `Edit -> Undo` menu item. */ - // var menubarItemUndo:MenuItem; + var menubarItemUndo:MenuItem; + /** * The `Edit -> Redo` menu item. */ - // var menubarItemRedo:MenuItem; + var menubarItemRedo:MenuItem; + /** * The `Edit -> Cut` menu item. */ - // var menubarItemCut:MenuItem; + var menubarItemCut:MenuItem; + /** * The `Edit -> Copy` menu item. */ - // var menubarItemCopy:MenuItem; + var menubarItemCopy:MenuItem; + /** * The `Edit -> Paste` menu item. */ - // var menubarItemPaste:MenuItem; + var menubarItemPaste:MenuItem; + /** * The `Edit -> Paste Unsnapped` menu item. */ - // var menubarItemPasteUnsnapped:MenuItem; + var menubarItemPasteUnsnapped:MenuItem; + /** * The `Edit -> Delete` menu item. */ - // var menubarItemDelete:MenuItem; + var menubarItemDelete:MenuItem; + + /** + * The `Edit -> Flip Notes` menu item. + */ + var menubarItemFlipNotes:MenuItem; + + /** + * The `Edit -> Select All` menu item. + */ + var menubarItemSelectAll:MenuItem; + + /** + * The `Edit -> Select Inverse` menu item. + */ + var menubarItemSelectInverse:MenuItem; + + /** + * The `Edit -> Select None` menu item. + */ + var menubarItemSelectNone:MenuItem; + + /** + * The `Edit -> Select Region` menu item. + */ + var menubarItemSelectRegion:MenuItem; + + /** + * The `Edit -> Select Before Cursor` menu item. + */ + var menubarItemSelectBeforeCursor:MenuItem; + + /** + * The `Edit -> Select After Cursor` menu item. + */ + var menubarItemSelectAfterCursor:MenuItem; + + /** + * The `Edit -> Decrease Note Snap Precision` menu item. + */ + var menuBarItemNoteSnapDecrease:MenuItem; + + /** + * The `Edit -> Decrease Note Snap Precision` menu item. + */ + var menuBarItemNoteSnapIncrease:MenuItem; + + /** + * The `View -> Downscroll` menu item. + */ + var menubarItemDownscroll:MenuCheckBox; + + /** + * The `View -> Increase Difficulty` menu item. + */ + var menubarItemDifficultyUp:MenuItem; + + /** + * The `View -> Decrease Difficulty` menu item. + */ + var menubarItemDifficultyDown:MenuItem; + + /** + * The `Audio -> Play/Pause` menu item. + */ + var menubarItemPlayPause:MenuItem; + + /** + * The `Audio -> Load Instrumental` menu item. + */ + var menubarItemLoadInstrumental:MenuItem; + + /** + * The `Audio -> Load Vocals` menu item. + */ + var menubarItemLoadVocals:MenuItem; + + /** + * The `Audio -> Metronome Volume` label. + */ + var menubarLabelVolumeMetronome:Label; + + /** + * The `Audio -> Metronome Volume` slider. + */ + var menubarItemVolumeMetronome:Slider; + + /** + * The `Audio -> Enable Player Hitsounds` menu checkbox. + */ + var menubarItemPlayerHitsounds:MenuCheckBox; + + /** + * The `Audio -> Enable Opponent Hitsounds` menu checkbox. + */ + var menubarItemOpponentHitsounds:MenuCheckBox; + + /** + * The `Audio -> Hitsound Volume` label. + */ + var menubarLabelVolumeHitsounds:Label; + + /** + * The `Audio -> Hitsound Volume` slider. + */ + var menubarItemVolumeHitsounds:Slider; + + /** + * The `Audio -> Instrumental Volume` label. + */ + var menubarLabelVolumeInstrumental:Label; + + /** + * The `Audio -> Instrumental Volume` slider. + */ + var menubarItemVolumeInstrumental:Slider; + + /** + * The `Audio -> Vocal Volume` label. + */ + var menubarLabelVolumeVocals:Label; + + /** + * The `Audio -> Vocal Volume` slider. + */ + var menubarItemVolumeVocals:Slider; + + /** + * The `Audio -> Playback Speed` label. + */ + var menubarLabelPlaybackSpeed:Label; + + /** + * The `Audio -> Playback Speed` slider. + */ + var menubarItemPlaybackSpeed:Slider; + /** * The label by the playbar telling the song position. */ - // var playbarSongPos:Label; + var playbarSongPos:Label; + /** * The label by the playbar telling the song time remaining. */ - // var playbarSongRemaining:Label; + var playbarSongRemaining:Label; + /** * The label by the playbar telling the note snap. */ - // var playbarNoteSnap:Label; + var playbarNoteSnap:Label; + /** * The button by the playbar to jump to the start of the song. */ - // var playbarStart:Button; + var playbarStart:Button; + /** * The button by the playbar to jump backwards in the song. */ - // var playbarBack:Button; + var playbarBack:Button; + /** * The button by the playbar to play or pause the song. */ - // var playbarPlay:Button; + var playbarPlay:Button; + /** * The button by the playbar to jump forwards in the song. */ - // var playbarForward:Button; + var playbarForward:Button; + /** * The button by the playbar to jump to the end of the song. */ - // var playbarEnd:Button; + var playbarEnd:Button; + /** * RENDER OBJECTS */ @@ -1659,7 +1832,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState isViewDownscroll = save.chartEditorDownscroll; playtestStartTime = save.chartEditorPlaytestStartTime; currentTheme = save.chartEditorTheme; - isMetronomeEnabled = save.chartEditorMetronomeEnabled; + metronomeVolume = save.chartEditorMetronomeVolume; + hitsoundVolume = save.chartEditorHitsoundVolume; hitsoundsEnabledPlayer = save.chartEditorHitsoundsEnabledPlayer; hitsoundsEnabledOpponent = save.chartEditorHitsoundsEnabledOpponent; @@ -1687,7 +1861,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState save.chartEditorDownscroll = isViewDownscroll; save.chartEditorPlaytestStartTime = playtestStartTime; save.chartEditorTheme = currentTheme; - save.chartEditorMetronomeEnabled = isMetronomeEnabled; + save.chartEditorMetronomeVolume = metronomeVolume; + save.chartEditorHitsoundVolume = hitsoundVolume; save.chartEditorHitsoundsEnabledPlayer = hitsoundsEnabledPlayer; save.chartEditorHitsoundsEnabledOpponent = hitsoundsEnabledOpponent; @@ -2223,11 +2398,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState #if sys menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder(); #else - // Disable the menu item if we're not on a desktop platform. var menubarItemGoToBackupsFolder = findComponent('menubarItemGoToBackupsFolder', MenuItem); if (menubarItemGoToBackupsFolder != null) menubarItemGoToBackupsFolder.disabled = true; - #end menubarItemUserGuide.onClick = _ -> this.openUserGuideDialog(); @@ -2256,8 +2429,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemLoadInstrumental.onClick = _ -> this.openUploadInstDialog(true); menubarItemLoadVocals.onClick = _ -> this.openUploadVocalsDialog(true); - menubarItemMetronomeEnabled.onChange = event -> isMetronomeEnabled = event.value; - menubarItemMetronomeEnabled.selected = isMetronomeEnabled; + menubarItemVolumeMetronome.onChange = event -> { + var volume:Float = (event?.value ?? 0) / 100.0; + metronomeVolume = volume; + menubarLabelVolumeMetronome.text = 'Metronome - ${Std.int(event.value)}%'; + }; + menubarItemVolumeMetronome.value = Std.int(metronomeVolume * 100); menubarItemPlayerHitsounds.onChange = event -> hitsoundsEnabledPlayer = event.value; menubarItemPlayerHitsounds.selected = hitsoundsEnabledPlayer; @@ -2265,6 +2442,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemOpponentHitsounds.onChange = event -> hitsoundsEnabledOpponent = event.value; menubarItemOpponentHitsounds.selected = hitsoundsEnabledOpponent; + menubarItemVolumeHitsound.onChange = event -> { + var volume:Float = (event?.value ?? 0) / 100.0; + hitsoundVolume = volume; + menubarLabelVolumeHitsound.text = 'Hitsound - ${Std.int(event.value)}%'; + }; + menubarItemVolumeHitsound.value = Std.int(hitsoundVolume * 100); + menubarItemVolumeInstrumental.onChange = event -> { var volume:Float = (event?.value ?? 0) / 100.0; if (audioInstTrack != null) audioInstTrack.volume = volume; @@ -2473,7 +2657,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // dispatchEvent gets called here. if (!super.beatHit()) return false; - if (isMetronomeEnabled && this.subState == null && (audioInstTrack != null && audioInstTrack.playing)) + if (metronomeVolume > 0.0 && this.subState == null && (audioInstTrack != null && audioInstTrack.playing)) { playMetronomeTick(Conductor.currentBeat % 4 == 0); } @@ -2514,7 +2698,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (audioInstTrack != null && audioInstTrack.playing) { - if (FlxG.mouse.pressedMiddle) + if (FlxG.keys.pressed.ALT) { // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! @@ -2914,6 +3098,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var shouldPause:Bool = false; // Whether to pause the song when scrolling. var shouldEase:Bool = false; // Whether to ease the scroll. + // Handle scroll anchor + if (scrollAnchorScreenPos != null) + { + var currentScreenPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); + var distance = currentScreenPos - scrollAnchorScreenPos; + + var verticalDistance = distance.y; + + // How much scrolling should be done based on the distance of the cursor from the anchor. + final ANCHOR_SCROLL_SPEED = 0.2; + + scrollAmount = ANCHOR_SCROLL_SPEED * verticalDistance; + shouldPause = true; + } + // Mouse Wheel = Scroll if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL) { @@ -2993,18 +3192,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState shouldPause = true; } - // 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) { @@ -3016,7 +3203,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState scrollAmount /= 10; } - // ALT = Move playhead instead. + // Alt + Drag = Scroll but move the playhead the same amount. if (FlxG.keys.pressed.ALT) { playheadAmount = scrollAmount; @@ -3138,9 +3325,26 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var overlapsSelection:Bool = FlxG.mouse.overlaps(renderedSelectionSquares); + if (FlxG.mouse.justPressedMiddle) + { + if (scrollAnchorScreenPos == null) + { + scrollAnchorScreenPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); + selectionBoxStartPos = null; + } + else + { + scrollAnchorScreenPos = null; + } + } + if (FlxG.mouse.justPressed) { - if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) + if (scrollAnchorScreenPos != null) + { + scrollAnchorScreenPos = null; + } + else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) { gridPlayheadScrollAreaPressed = true; } @@ -3149,7 +3353,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Clicked note preview notePreviewScrollAreaStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); } - else if (!overlapsGrid || overlapsSelectionBorder) + else if (!isCursorOverHaxeUI && (!overlapsGrid || overlapsSelectionBorder)) { selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); // Drawing selection box. @@ -3432,6 +3636,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState scrollPositionInPixels = clickedPosInPixels; moveSongToScrollPosition(); } + else if (scrollAnchorScreenPos != null) + { + // Cursor should be a scroll anchor. + targetCursorMode = Scroll; + } else if (dragTargetNote != null || dragTargetEvent != null) { if (FlxG.mouse.justReleased) @@ -4345,7 +4554,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var startTimestamp:Float = 0; if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; - var targetSong:Song = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); + var targetSong:Song; + try + { + targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); + } + catch (e) + { + this.error("Could Not Playtest", 'Got an error trying to playtest the song.\n${e}'); + return; + } // TODO: Rework asset system so we can remove this. switch (currentSongStage) @@ -4389,6 +4607,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Override music. if (audioInstTrack != null) FlxG.sound.music = audioInstTrack; if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup; + + this.persistentUpdate = false; + this.persistentDraw = false; stopWelcomeMusic(); openSubState(targetState); } @@ -4504,7 +4725,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ function playMetronomeTick(high:Bool = false):Void { - this.playSound(Paths.sound('chartingSounds/metronome${high ? '1' : '2'}')); + this.playSound(Paths.sound('chartingSounds/metronome${high ? '1' : '2'}'), metronomeVolume); } function switchToCurrentInstrumental():Void @@ -4603,6 +4824,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var prevDifficulty = availableDifficulties[availableDifficulties.length - 1]; selectedDifficulty = prevDifficulty; + Conductor.mapTimeChanges(this.currentSongMetadata.timeChanges); + refreshDifficultyTreeSelection(); refreshMetadataToolbox(); } @@ -4721,6 +4944,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState @:nullSafety(Off) function resetConductorAfterTest(_:FlxSubState = null):Void { + this.persistentUpdate = true; + this.persistentDraw = true; + moveSongToScrollPosition(); // Reapply the volume. @@ -5000,9 +5226,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState switch (noteData.getStrumlineIndex()) { case 0: // Player - if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer')); + if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume); case 1: // Opponent - if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent')); + if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume); } } } diff --git a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx new file mode 100644 index 000000000..3c45c1168 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx @@ -0,0 +1,61 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongTimeChange; + +/** + * A command which changes the starting BPM of the song. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChangeStartingBPMCommand implements ChartEditorCommand +{ + var targetBPM:Float; + + var previousBPM:Float = 100; + + public function new(targetBPM:Float) + { + this.targetBPM = targetBPM; + } + + public function execute(state:ChartEditorState):Void + { + var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges; + if (timeChanges == null || timeChanges.length == 0) + { + previousBPM = 100; + timeChanges = [new SongTimeChange(0, targetBPM)]; + } + else + { + previousBPM = timeChanges[0].bpm; + timeChanges[0].bpm = targetBPM; + } + + state.currentSongMetadata.timeChanges = timeChanges; + + Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + } + + public function undo(state:ChartEditorState):Void + { + var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges; + if (timeChanges == null || timeChanges.length == 0) + { + timeChanges = [new SongTimeChange(0, previousBPM)]; + } + else + { + timeChanges[0].bpm = previousBPM; + } + + state.currentSongMetadata.timeChanges = timeChanges; + + Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + } + + public function toString():String + { + return 'Change Starting BPM to ${targetBPM}'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 1857b44db..75382da41 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -32,10 +32,14 @@ class PasteItemsCommand implements ChartEditorCommand return; } - trace(currentClipboard.notes); + var stepEndOfSong:Float = Conductor.getTimeInSteps(state.songLengthInMs); + var stepCutoff:Float = stepEndOfSong - 1.0; + var msCutoff:Float = Conductor.getStepTimeInMs(stepCutoff); addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp)); + addedNotes = SongDataUtils.clampSongNoteData(addedNotes, 0.0, msCutoff); addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); + addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index f82a123a4..2de3d8c20 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -202,7 +202,7 @@ class ChartEditorAudioHandler * Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance. * @param path The path to the sound effect. Use `Paths` to build this. */ - public static function playSound(_state:ChartEditorState, path:String):Void + public static function playSound(_state:ChartEditorState, path:String, volume:Float = 1.0):Void { var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound(); var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path); @@ -214,6 +214,7 @@ class ChartEditorAudioHandler snd.loadEmbedded(asset); snd.autoDestroy = true; FlxG.sound.list.add(snd); + snd.volume = volume; snd.play(); } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 79937ce6f..0e7ba374c 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -110,12 +110,12 @@ class ChartEditorDialogHandler { var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_BACKUP_AVAILABLE_LAYOUT, true, true); if (dialog == null) throw 'Could not locate Backup Available dialog'; - dialog.onDialogClosed = function(_event) { + dialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // User loaded the backup! Close the welcome dialog behind this. - if (welcomeDialog != null) welcomeDialog.hideDialog(DialogButton.CANCEL); + if (welcomeDialog != null) welcomeDialog.hideDialog(DialogButton.APPLY); } else { @@ -137,22 +137,22 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Backup Available dialog'; - buttonCancel.onClick = function(_event) { + buttonCancel.onClick = function(_) { // Don't hide the welcome dialog behind this. dialog.hideDialog(DialogButton.CANCEL); } var buttonGoToFolder:Null<Button> = dialog.findComponent('buttonGoToFolder', Button); if (buttonGoToFolder == null) throw 'Could not locate buttonGoToFolder button in Backup Available dialog'; - buttonGoToFolder.onClick = function(_event) { + buttonGoToFolder.onClick = function(_) { state.openBackupsFolder(); // Don't hide the welcome dialog behind this. - // dialog.hideDialog(DialogButton.CANCEL); + // Don't close this dialog. } var buttonOpenBackup:Null<Button> = dialog.findComponent('buttonOpenBackup', Button); if (buttonOpenBackup == null) throw 'Could not locate buttonOpenBackup button in Backup Available dialog'; - buttonOpenBackup.onClick = function(_event) { + buttonOpenBackup.onClick = function(_) { var latestBackupPath:Null<String> = ChartEditorImportExportHandler.getLatestBackupPath(); var result:Null<Array<String>> = (latestBackupPath != null) ? state.loadFromFNFCPath(latestBackupPath) : null; @@ -210,20 +210,20 @@ class ChartEditorDialogHandler // Open the "Open Chart" wizard // Step 1. Open Chart var openChartDialog:Dialog = openChartDialog(state); - openChartDialog.onDialogClosed = function(_event) { + openChartDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 2. Upload instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); - uploadInstDialog.onDialogClosed = function(_event) { + uploadInstDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog - uploadVocalsDialog.onDialogClosed = function(_event) { + uploadVocalsDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // Built from parts, so no .fnfc to save to. state.switchToCurrentInstrumental(); @@ -251,20 +251,20 @@ class ChartEditorDialogHandler // Step 1. Open Chart var openChartDialog:Null<Dialog> = openImportChartDialog(state, format); if (openChartDialog == null) throw 'Could not locate Import Chart dialog'; - openChartDialog.onDialogClosed = function(_event) { + openChartDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 2. Upload instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); - uploadInstDialog.onDialogClosed = function(_event) { + uploadInstDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog - uploadVocalsDialog.onDialogClosed = function(_event) { + uploadVocalsDialog.onDialogClosed = function(_) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); @@ -289,21 +289,21 @@ class ChartEditorDialogHandler public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata - var songMetadataDialog:Dialog = openSongMetadataDialog(state, false, Constants.DEFAULT_VARIATION); - songMetadataDialog.onDialogClosed = function(_event) { + var songMetadataDialog:Dialog = openSongMetadataDialog(state, false, Constants.DEFAULT_VARIATION, true); + songMetadataDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 2. Upload Instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); - uploadInstDialog.onDialogClosed = function(_event) { + uploadInstDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog - uploadVocalsDialog.onDialogClosed = function(_event) { + uploadVocalsDialog.onDialogClosed = function(_) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); @@ -328,21 +328,21 @@ class ChartEditorDialogHandler public static function openCreateSongWizardErectOnly(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata - var songMetadataDialog:Dialog = openSongMetadataDialog(state, true, Constants.DEFAULT_VARIATION); - songMetadataDialog.onDialogClosed = function(_event) { + var songMetadataDialog:Dialog = openSongMetadataDialog(state, true, Constants.DEFAULT_VARIATION, true); + songMetadataDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 2. Upload Instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); - uploadInstDialog.onDialogClosed = function(_event) { + uploadInstDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog - uploadVocalsDialog.onDialogClosed = function(_event) { + uploadVocalsDialog.onDialogClosed = function(_) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); @@ -367,41 +367,41 @@ class ChartEditorDialogHandler public static function openCreateSongWizardBasicErect(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata - var songMetadataDialog:Dialog = openSongMetadataDialog(state, false, Constants.DEFAULT_VARIATION); - songMetadataDialog.onDialogClosed = function(_event) { + var songMetadataDialog:Dialog = openSongMetadataDialog(state, false, Constants.DEFAULT_VARIATION, true); + songMetadataDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 2. Upload Instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); - uploadInstDialog.onDialogClosed = function(_event) { + uploadInstDialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog - uploadVocalsDialog.onDialogClosed = function(_event) { + uploadVocalsDialog.onDialogClosed = function(_) { state.switchToCurrentInstrumental(); // Step 4. Song Metadata (Erect) - var songMetadataDialogErect:Dialog = openSongMetadataDialog(state, true, 'erect'); - songMetadataDialogErect.onDialogClosed = function(_event) { + var songMetadataDialogErect:Dialog = openSongMetadataDialog(state, true, 'erect', false); + songMetadataDialogErect.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Switch to the Erect variation so uploading the instrumental applies properly. state.selectedVariation = 'erect'; // Step 5. Upload Instrumental (Erect) var uploadInstDialogErect:Dialog = openUploadInstDialog(state, closable); - uploadInstDialogErect.onDialogClosed = function(_event) { + uploadInstDialogErect.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // Step 6. Upload Vocals (Erect) // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog - uploadVocalsDialogErect.onDialogClosed = function(_event) { + uploadVocalsDialogErect.onDialogClosed = function(_) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); @@ -453,19 +453,19 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Instrumental dialog'; - buttonCancel.onClick = function(_event) { + buttonCancel.onClick = function(_) { dialog.hideDialog(DialogButton.CANCEL); } var instrumentalBox:Null<Box> = dialog.findComponent('instrumentalBox', Box); if (instrumentalBox == null) throw 'Could not locate instrumentalBox in Upload Instrumental dialog'; - instrumentalBox.onMouseOver = function(_event) { + instrumentalBox.onMouseOver = function(_) { instrumentalBox.swapClass('upload-bg', 'upload-bg-hover'); Cursor.cursorMode = Pointer; } - instrumentalBox.onMouseOut = function(_event) { + instrumentalBox.onMouseOut = function(_) { instrumentalBox.swapClass('upload-bg-hover', 'upload-bg'); Cursor.cursorMode = Default; } @@ -474,7 +474,7 @@ class ChartEditorDialogHandler var dropHandler:DialogDropTarget = {component: instrumentalBox, handler: null}; - instrumentalBox.onClick = function(_event) { + instrumentalBox.onClick = function(_) { Dialogs.openBinaryFile('Open Instrumental', [ {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) { if (selectedFile != null && selectedFile.bytes != null) @@ -533,10 +533,13 @@ class ChartEditorDialogHandler /** * Opens the dialog in the wizard where the user can set song metadata like name and artist and BPM. * @param state The ChartEditorState instance. + * @param erect Whether to create erect difficulties or normal ones. + * @param targetVariation The variation to create difficulties for. + * @param clearExistingMetadata Whether to clear existing metadata when confirming. * @return The dialog to open. */ @:haxe.warning("-WVarInit") - public static function openSongMetadataDialog(state:ChartEditorState, erect:Bool, targetVariation:String):Dialog + public static function openSongMetadataDialog(state:ChartEditorState, erect:Bool, targetVariation:String, clearExistingMetadata:Bool):Dialog { var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false); if (dialog == null) throw 'Could not locate Song Metadata dialog'; @@ -549,7 +552,7 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog'; state.isHaxeUIDialogOpen = true; - buttonCancel.onClick = function(_event) { + buttonCancel.onClick = function(_) { state.isHaxeUIDialogOpen = false; dialog.hideDialog(DialogButton.CANCEL); } @@ -661,8 +664,12 @@ class ChartEditorDialogHandler var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog'; - dialogContinue.onClick = (_event) -> { - if (targetVariation == Constants.DEFAULT_VARIATION) state.songMetadata.clear(); + dialogContinue.onClick = (_) -> { + if (clearExistingMetadata) + { + state.songMetadata.clear(); + state.songChartData.clear(); + } state.songMetadata.set(targetVariation, newSongMetadata); @@ -702,13 +709,13 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Vocals dialog'; - buttonCancel.onClick = function(_event) { + buttonCancel.onClick = function(_) { dialog.hideDialog(DialogButton.CANCEL); } var dialogNoVocals:Null<Button> = dialog.findComponent('dialogNoVocals', Button); if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog'; - dialogNoVocals.onClick = function(_event) { + dialogNoVocals.onClick = function(_) { // Dismiss state.wipeVocalData(); dialog.hideDialog(DialogButton.APPLY); @@ -820,7 +827,7 @@ class ChartEditorDialogHandler var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); if (dialogContinue == null) throw 'Could not locate dialogContinue button in Upload Vocals dialog'; - dialogContinue.onClick = function(_event) { + dialogContinue.onClick = function(_) { // Dismiss dialog.hideDialog(DialogButton.APPLY); }; @@ -842,7 +849,7 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Open Chart dialog'; - buttonCancel.onClick = function(_event) { + buttonCancel.onClick = function(_) { dialog.hideDialog(DialogButton.CANCEL); } @@ -856,7 +863,7 @@ class ChartEditorDialogHandler var buttonContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); if (buttonContinue == null) throw 'Could not locate dialogContinue button in Open Chart dialog'; - buttonContinue.onClick = function(_event) { + buttonContinue.onClick = function(_) { state.loadSong(songMetadata, songChartData); dialog.hideDialog(DialogButton.APPLY); @@ -904,11 +911,11 @@ class ChartEditorDialogHandler songVariationMetadataEntryLabel.text = 'Click to browse for <song>-metadata-${variation}.json file.'; #end - songVariationMetadataEntry.onMouseOver = function(_event) { + songVariationMetadataEntry.onMouseOver = function(_) { songVariationMetadataEntry.swapClass('upload-bg', 'upload-bg-hover'); Cursor.cursorMode = Pointer; } - songVariationMetadataEntry.onMouseOut = function(_event) { + songVariationMetadataEntry.onMouseOut = function(_) { songVariationMetadataEntry.swapClass('upload-bg-hover', 'upload-bg'); Cursor.cursorMode = Default; } @@ -928,11 +935,11 @@ class ChartEditorDialogHandler songVariationChartDataEntryLabel.text = 'Click to browse for <song>-chart-${variation}.json file.'; #end - songVariationChartDataEntry.onMouseOver = function(_event) { + songVariationChartDataEntry.onMouseOver = function(_) { songVariationChartDataEntry.swapClass('upload-bg', 'upload-bg-hover'); Cursor.cursorMode = Pointer; } - songVariationChartDataEntry.onMouseOut = function(_event) { + songVariationChartDataEntry.onMouseOut = function(_) { songVariationChartDataEntry.swapClass('upload-bg-hover', 'upload-bg'); Cursor.cursorMode = Default; } @@ -982,7 +989,7 @@ class ChartEditorDialogHandler if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations); }; - onClickMetadataVariation = function(variation:String, label:Label, _event:UIEvent) { + onClickMetadataVariation = function(variation:String, label:Label, _:UIEvent) { Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [ {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) { if (selectedFile != null && selectedFile.bytes != null) @@ -1066,7 +1073,7 @@ class ChartEditorDialogHandler } }; - onClickChartDataVariation = function(variation:String, label:Label, _event:UIEvent) { + onClickChartDataVariation = function(variation:String, label:Label, _:UIEvent) { Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [ {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) { if (selectedFile != null && selectedFile.bytes != null) @@ -1122,7 +1129,7 @@ class ChartEditorDialogHandler metadataEntry.swapClass('upload-bg', 'upload-bg-hover'); Cursor.cursorMode = Pointer; } - metadataEntry.onMouseOut = function(_event) { + metadataEntry.onMouseOut = function(_) { metadataEntry.swapClass('upload-bg-hover', 'upload-bg'); Cursor.cursorMode = Default; } @@ -1162,7 +1169,7 @@ class ChartEditorDialogHandler if (buttonCancel == null) throw 'Could not locate dialogCancel button in Import Chart dialog'; state.isHaxeUIDialogOpen = true; - buttonCancel.onClick = function(_event) { + buttonCancel.onClick = function(_) { state.isHaxeUIDialogOpen = false; dialog.hideDialog(DialogButton.CANCEL); } @@ -1170,18 +1177,18 @@ class ChartEditorDialogHandler var importBox:Null<Box> = dialog.findComponent('importBox', Box); if (importBox == null) throw 'Could not locate importBox in Import Chart dialog'; - importBox.onMouseOver = function(_event) { + importBox.onMouseOver = function(_) { importBox.swapClass('upload-bg', 'upload-bg-hover'); Cursor.cursorMode = Pointer; } - importBox.onMouseOut = function(_event) { + importBox.onMouseOut = function(_) { importBox.swapClass('upload-bg-hover', 'upload-bg'); Cursor.cursorMode = Default; } var onDropFile:String->Void; - importBox.onClick = function(_event) { + importBox.onClick = function(_) { Dialogs.openBinaryFile('Import Chart - ${prettyFormat}', fileFilter != null ? [fileFilter] : [], function(selectedFile:SelectedFileInfo) { if (selectedFile != null && selectedFile.bytes != null) { @@ -1251,13 +1258,13 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Add Variation dialog'; - buttonCancel.onClick = function(_event) { + buttonCancel.onClick = function(_) { dialog.hideDialog(DialogButton.CANCEL); } var buttonAdd:Null<Button> = dialog.findComponent('dialogAdd', Button); if (buttonAdd == null) throw 'Could not locate dialogAdd button in Add Variation dialog'; - buttonAdd.onClick = function(_event) { + buttonAdd.onClick = function(_) { // This performs validation before the onSubmit callback is called. variationForm.submit(); } @@ -1296,12 +1303,13 @@ class ChartEditorDialogHandler var dialogBPM:Null<NumberStepper> = dialog.findComponent('dialogBPM', NumberStepper); if (dialogBPM == null) throw 'Could not locate dialogBPM NumberStepper in Add Variation dialog'; - dialogBPM.value = state.currentSongMetadata.timeChanges[0].bpm; + var currentStartingBPM:Float = state.currentSongMetadata.timeChanges[0].bpm; + dialogBPM.value = currentStartingBPM; // If all validators succeeded, this callback is called. state.isHaxeUIDialogOpen = true; - variationForm.onSubmit = function(_event) { + variationForm.onSubmit = function(_) { state.isHaxeUIDialogOpen = false; trace('Add Variation dialog submitted, validation succeeded!'); @@ -1317,6 +1325,8 @@ class ChartEditorDialogHandler state.songMetadata.set(pendingVariation.variation, pendingVariation); state.difficultySelectDirty = true; // Force the Difficulty toolbox to update. + // Don't update conductor since we haven't switched to the new variation yet. + state.success('Add Variation', 'Added new variation "${pendingVariation.variation}"'); dialog.hideDialog(DialogButton.APPLY); @@ -1341,13 +1351,13 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Add Difficulty dialog'; - buttonCancel.onClick = function(_event) { + buttonCancel.onClick = function(_) { dialog.hideDialog(DialogButton.CANCEL); } var buttonAdd:Null<Button> = dialog.findComponent('dialogAdd', Button); if (buttonAdd == null) throw 'Could not locate dialogAdd button in Add Difficulty dialog'; - buttonAdd.onClick = function(_event) { + buttonAdd.onClick = function(_) { // This performs validation before the onSubmit callback is called. difficultyForm.submit(); } @@ -1367,7 +1377,7 @@ class ChartEditorDialogHandler inputScrollSpeed.value = state.currentSongChartScrollSpeed; labelScrollSpeed.text = 'Scroll Speed: ${inputScrollSpeed.value}x'; - difficultyForm.onSubmit = function(_event) { + difficultyForm.onSubmit = function(_) { trace('Add Difficulty dialog submitted, validation succeeded!'); var dialogDifficultyName:Null<TextField> = dialog.findComponent('dialogDifficultyName', TextField); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorNotificationHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorNotificationHandler.hx index 796e70381..5c340feb7 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorNotificationHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorNotificationHandler.hx @@ -21,7 +21,7 @@ class ChartEditorNotificationHandler */ public static function success(state:ChartEditorState, title:String, body:String):Notification { - return sendNotification(title, body, NotificationType.Success); + return sendNotification(state, title, body, NotificationType.Success); } /** @@ -30,7 +30,7 @@ class ChartEditorNotificationHandler */ public static function warning(state:ChartEditorState, title:String, body:String):Notification { - return sendNotification(title, body, NotificationType.Warning); + return sendNotification(state, title, body, NotificationType.Warning); } /** @@ -48,7 +48,7 @@ class ChartEditorNotificationHandler */ public static function error(state:ChartEditorState, title:String, body:String):Notification { - return sendNotification(title, body, NotificationType.Error); + return sendNotification(state, title, body, NotificationType.Error); } /** @@ -66,7 +66,7 @@ class ChartEditorNotificationHandler */ public static function info(state:ChartEditorState, title:String, body:String):Notification { - return sendNotification(title, body, NotificationType.Info); + return sendNotification(state, title, body, NotificationType.Info); } /** @@ -79,7 +79,7 @@ class ChartEditorNotificationHandler */ public static function infoWithActions(state:ChartEditorState, title:String, body:String, actions:Array<NotificationAction>):Notification { - return sendNotification(title, body, NotificationType.Info, actions); + return sendNotification(state, title, body, NotificationType.Info, actions); } /** @@ -101,7 +101,7 @@ class ChartEditorNotificationHandler NotificationManager.instance.removeNotification(notif); } - static function sendNotification(title:String, body:String, ?type:NotificationType, ?actions:Array<NotificationAction>):Notification + static function sendNotification(state:ChartEditorState, title:String, body:String, ?type:NotificationType, ?actions:Array<NotificationAction>):Notification { #if !mac var actionNames:Array<String> = actions == null ? [] : actions.map(action -> action.text); @@ -127,6 +127,8 @@ class ChartEditorNotificationHandler if (action != null && action.callback != null) { button.onClick = function(_) { + // Don't allow actions to be clicked while the playtest is open. + if (state.subState != null) return; action.callback(); }; } @@ -137,6 +139,8 @@ class ChartEditorNotificationHandler return notif; #else + // TODO: Implement notifications on Mac OS OR... make sure the null is handled properly on mac? + return null; trace('WARNING: Notifications are not supported on Mac OS.'); #end } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index af7cd774a..418f57464 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -11,6 +11,7 @@ import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.event.SongEvent; import funkin.data.event.SongEventData; import funkin.data.song.SongData.SongTimeChange; +import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; @@ -610,19 +611,12 @@ class ChartEditorToolboxHandler inputBPM.onChange = function(event:UIEvent) { if (event.value == null || event.value <= 0) return; - var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges; - if (timeChanges == null || timeChanges.length == 0) + // Use a command so we can undo/redo this action. + var startingBPM = state.currentSongMetadata.timeChanges[0].bpm; + if (event.value != startingBPM) { - timeChanges = [new SongTimeChange(0, event.value)]; + state.performCommand(new ChangeStartingBPMCommand(event.value)); } - else - { - timeChanges[0].bpm = event.value; - } - - state.currentSongMetadata.timeChanges = timeChanges; - - Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); }; inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm; diff --git a/source/funkin/util/macro/FlxMacro.hx b/source/funkin/util/macro/FlxMacro.hx index 0c1a5fa78..cec6b72ec 100644 --- a/source/funkin/util/macro/FlxMacro.hx +++ b/source/funkin/util/macro/FlxMacro.hx @@ -1,5 +1,6 @@ package funkin.util.macro; +#if !display #if macro class FlxMacro { @@ -33,3 +34,4 @@ class FlxMacro } } #end +#end diff --git a/source/funkin/util/macro/GitCommit.hx b/source/funkin/util/macro/GitCommit.hx index d0c034828..b3ddd2b7e 100644 --- a/source/funkin/util/macro/GitCommit.hx +++ b/source/funkin/util/macro/GitCommit.hx @@ -1,5 +1,6 @@ package funkin.util.macro; +#if !display #if (debug || FORCE_DEBUG_VERSION) class GitCommit { @@ -65,3 +66,4 @@ class GitCommit } } #end +#end