From 4a0a330eb428c84588ec919876e5679ad68db862 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 4 Jan 2024 08:20:34 -0500 Subject: [PATCH 01/60] Redo logic around note selection, add new buttons for selecting individual sides. --- assets | 2 +- source/funkin/data/song/SongDataUtils.hx | 2 +- .../ui/debug/charting/ChartEditorState.hx | 339 ++++++++++++------ .../charting/commands/AddEventsCommand.hx | 6 + .../charting/commands/AddNotesCommand.hx | 6 + .../commands/ChangeStartingBPMCommand.hx | 6 + .../charting/commands/ChartEditorCommand.hx | 11 + .../charting/commands/CopyItemsCommand.hx | 144 ++++++++ .../charting/commands/CutItemsCommand.hx | 6 + .../commands/DeselectAllItemsCommand.hx | 19 +- .../charting/commands/DeselectItemsCommand.hx | 21 +- .../commands/ExtendNoteLengthCommand.hx | 6 + .../charting/commands/FlipNotesCommand.hx | 6 + .../commands/InvertSelectedItemsCommand.hx | 20 +- .../charting/commands/MoveEventsCommand.hx | 6 + .../charting/commands/MoveItemsCommand.hx | 6 + .../charting/commands/MoveNotesCommand.hx | 6 + .../charting/commands/PasteItemsCommand.hx | 6 + .../charting/commands/RemoveEventsCommand.hx | 6 + .../charting/commands/RemoveItemsCommand.hx | 6 + .../charting/commands/RemoveNotesCommand.hx | 6 + .../commands/SelectAllItemsCommand.hx | 43 ++- .../charting/commands/SelectItemsCommand.hx | 12 +- .../commands/SetItemSelectionCommand.hx | 20 +- .../commands/SwitchDifficultyCommand.hx | 6 + .../components/ChartEditorEventSprite.hx | 4 +- .../components/ChartEditorNotePreview.hx | 17 +- .../handlers/ChartEditorAudioHandler.hx | 11 +- .../handlers/ChartEditorShortcutHandler.hx | 7 +- 29 files changed, 600 insertions(+), 156 deletions(-) create mode 100644 source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx diff --git a/assets b/assets index 9ecc4d26f..198c3ab87 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9ecc4d26fe6b26f31782cccfcd7331bd8a318ce1 +Subproject commit 198c3ab87e401e595be50814af0f667eb1be25e7 diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index 309676884..275106f3a 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -273,7 +273,7 @@ class SongDataUtils } /** - * Filter a list of notes to only include notes whose data is within the given range. + * Filter a list of notes to only include notes whose data is within the given range, inclusive. */ public static function getNotesInDataRange(notes:Array, start:Int, end:Int):Array { diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 4f96fad69..16f83275b 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -61,6 +61,7 @@ import funkin.ui.debug.charting.commands.AddNotesCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.commands.CutItemsCommand; +import funkin.ui.debug.charting.commands.CopyItemsCommand; import funkin.ui.debug.charting.commands.DeselectAllItemsCommand; import funkin.ui.debug.charting.commands.DeselectItemsCommand; import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand; @@ -103,6 +104,7 @@ import haxe.ui.components.Slider; import haxe.ui.components.TextField; import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.Frame; +import haxe.ui.containers.Box; import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuItem; @@ -190,10 +192,40 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ public static final PLAYBAR_HEIGHT:Int = 48; + /** + * The height of the note selection buttons above the grid. + */ + public static final NOTE_SELECT_BUTTON_HEIGHT:Int = 24; + /** * The amount of padding between the menu bar and the chart grid when fully scrolled up. */ - public static final GRID_TOP_PAD:Int = 8; + public static final GRID_TOP_PAD:Int = NOTE_SELECT_BUTTON_HEIGHT + 12; + + /** + * The initial vertical position of the chart grid. + */ + public static final GRID_INITIAL_Y_POS:Int = MENU_BAR_HEIGHT + GRID_TOP_PAD; + + /** + * The X position of the note preview area. + */ + public static final NOTE_PREVIEW_X_POS:Int = 350; + + /** + * The Y position of the note preview area. + */ + public static final NOTE_PREVIEW_Y_POS:Int = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 4; + + /** + * The X position of the note grid. + */ + public static var GRID_X_POS(get, never):Float; + + static function get_GRID_X_POS():Float + { + return FlxG.width / 2 - GRID_SIZE * STRUMLINE_SIZE; + } // Colors // Background color tint. @@ -338,21 +370,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (isViewDownscroll) { - gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS); gridPlayheadScrollArea.y = gridTiledSprite.y; } else { - gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS); gridPlayheadScrollArea.y = gridTiledSprite.y; if (audioVisGroup != null && audioVisGroup.playerVis != null) { - audioVisGroup.playerVis.y = Math.max(gridTiledSprite.y, MENU_BAR_HEIGHT); + audioVisGroup.playerVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS); } if (audioVisGroup != null && audioVisGroup.opponentVis != null) { - audioVisGroup.opponentVis.y = Math.max(gridTiledSprite.y, MENU_BAR_HEIGHT); + audioVisGroup.opponentVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS); } } } @@ -422,7 +454,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState this.playheadPositionInPixels = value; // Move the playhead sprite to the correct position. - gridPlayhead.y = this.playheadPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + gridPlayhead.y = this.playheadPositionInPixels + GRID_INITIAL_Y_POS; return this.playheadPositionInPixels; } @@ -1602,6 +1634,24 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var playbarEnd:Button; + /** + * The button above the grid that selects all notes on the opponent's side. + * Constructed manually and added to the layout so we can control its position. + */ + var buttonSelectOpponent:Button; + + /** + * The button above the grid that selects all notes on the player's side. + * Constructed manually and added to the layout so we can control its position. + */ + var buttonSelectPlayer:Button; + + /** + * The button above the grid that selects all song events. + * Constructed manually and added to the layout so we can control its position. + */ + var buttonSelectEvent:Button; + /** * RENDER OBJECTS */ @@ -2094,8 +2144,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (gridBitmap == null) throw 'ERROR: Tried to build grid, but gridBitmap is null! Check ChartEditorThemeHandler.updateTheme().'; gridTiledSprite = new FlxTiledSprite(gridBitmap, gridBitmap.width, 1000, false, true); - gridTiledSprite.x = FlxG.width / 2 - GRID_SIZE * STRUMLINE_SIZE; // Center the grid. - gridTiledSprite.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; // Push down to account for the menu bar. + gridTiledSprite.x = GRID_X_POS; // Center the grid. + gridTiledSprite.y = GRID_INITIAL_Y_POS; // Push down to account for the menu bar. add(gridTiledSprite); gridTiledSprite.zIndex = 10; @@ -2127,8 +2177,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(gridPlayheadScrollArea); gridPlayheadScrollArea.setGraphicSize(PLAYHEAD_SCROLL_AREA_WIDTH, 3000); gridPlayheadScrollArea.updateHitbox(); - gridPlayheadScrollArea.x = gridTiledSprite.x - PLAYHEAD_SCROLL_AREA_WIDTH; - gridPlayheadScrollArea.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; + gridPlayheadScrollArea.x = GRID_X_POS - PLAYHEAD_SCROLL_AREA_WIDTH; + gridPlayheadScrollArea.y = GRID_INITIAL_Y_POS; gridPlayheadScrollArea.zIndex = 25; // The playhead that show the current position in the song. @@ -2136,8 +2186,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState gridPlayhead.zIndex = 30; 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 playheadBaseYPos:Float = GRID_INITIAL_Y_POS; + gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos); var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadSprite.y = 0; @@ -2168,10 +2218,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function buildNotePreview():Void { - var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - PLAYBAR_HEIGHT - GRID_TOP_PAD - GRID_TOP_PAD; - notePreview = new ChartEditorNotePreview(height); - notePreview.x = 350; - notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; + var playbarHeightWithPad = PLAYBAR_HEIGHT + 10; + var notePreviewHeight:Int = FlxG.height - NOTE_PREVIEW_Y_POS - playbarHeightWithPad; + notePreview = new ChartEditorNotePreview(notePreviewHeight); + notePreview.x = NOTE_PREVIEW_X_POS; + notePreview.y = NOTE_PREVIEW_Y_POS; add(notePreview); if (notePreviewViewport == null) throw 'ERROR: Tried to build note preview, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().'; @@ -2355,6 +2406,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(playbarHeadLayout); + // Little text that shows up when you copy something. txtCopyNotif = new FlxText(0, 0, 0, '', 24); txtCopyNotif.setBorderStyle(OUTLINE, 0xFF074809, 1); txtCopyNotif.color = 0xFF52FF77; @@ -2379,6 +2431,77 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState this.openCharacterDropdown(CharacterType.BF, true); } }); + + buttonSelectOpponent = new Button(); + buttonSelectOpponent.allowFocus = false; + buttonSelectOpponent.text = "Opponent"; // Default text. + buttonSelectOpponent.x = GRID_X_POS; + buttonSelectOpponent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 8; + buttonSelectOpponent.width = GRID_SIZE * 4; + buttonSelectOpponent.height = NOTE_SELECT_BUTTON_HEIGHT; + buttonSelectOpponent.tooltip = "Click to set selection to all notes on this side.\nShift-click to add all notes on this side to selection."; + buttonSelectOpponent.zIndex = 110; + add(buttonSelectOpponent); + + buttonSelectOpponent.onClick = (_) -> { + var notesToSelect:Array = currentSongChartNoteData; + notesToSelect = SongDataUtils.getNotesInDataRange(notesToSelect, STRUMLINE_SIZE, STRUMLINE_SIZE * 2 - 1); + if (FlxG.keys.pressed.SHIFT) + { + performCommand(new SelectItemsCommand(notesToSelect, [])); + } + else + { + performCommand(new SetItemSelectionCommand(notesToSelect, [])); + } + } + + buttonSelectPlayer = new Button(); + buttonSelectPlayer.allowFocus = false; + buttonSelectPlayer.text = "Player"; // Default text. + buttonSelectPlayer.x = buttonSelectOpponent.x + buttonSelectOpponent.width; + buttonSelectPlayer.y = buttonSelectOpponent.y; + buttonSelectPlayer.width = GRID_SIZE * 4; + buttonSelectPlayer.height = NOTE_SELECT_BUTTON_HEIGHT; + buttonSelectPlayer.tooltip = "Click to set selection to all notes on this side.\nShift-click to add all notes on this side to selection."; + buttonSelectPlayer.zIndex = 110; + add(buttonSelectPlayer); + + buttonSelectPlayer.onClick = (_) -> { + var notesToSelect:Array = currentSongChartNoteData; + notesToSelect = SongDataUtils.getNotesInDataRange(notesToSelect, 0, STRUMLINE_SIZE - 1); + if (FlxG.keys.pressed.SHIFT) + { + performCommand(new SelectItemsCommand(notesToSelect, [])); + } + else + { + performCommand(new SetItemSelectionCommand(notesToSelect, [])); + } + } + + buttonSelectEvent = new Button(); + buttonSelectEvent.allowFocus = false; + buttonSelectEvent.icon = Paths.image('ui/chart-editor/events/Default'); + buttonSelectEvent.iconPosition = "top"; + buttonSelectEvent.x = buttonSelectPlayer.x + buttonSelectPlayer.width; + buttonSelectEvent.y = buttonSelectPlayer.y; + buttonSelectEvent.width = GRID_SIZE; + buttonSelectEvent.height = NOTE_SELECT_BUTTON_HEIGHT; + buttonSelectEvent.tooltip = "Click to set selection to all events.\nShift-click to add all events to selection."; + buttonSelectEvent.zIndex = 110; + add(buttonSelectEvent); + + buttonSelectEvent.onClick = (_) -> { + if (FlxG.keys.pressed.SHIFT) + { + performCommand(new SelectItemsCommand([], currentSongChartEventData)); + } + else + { + performCommand(new SetItemSelectionCommand([], currentSongChartEventData)); + } + } } /** @@ -2467,23 +2590,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemUndo.onClick = _ -> undoLastCommand(); menubarItemRedo.onClick = _ -> redoLastCommand(); menubarItemCopy.onClick = 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, timeOffset), - events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset), - }); }; menubarItemCut.onClick = _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection)); @@ -2521,11 +2627,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemFlipNotes.onClick = _ -> performCommand(new FlipNotesCommand(currentNoteSelection)); - menubarItemSelectAll.onClick = _ -> performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection)); + menubarItemSelectAllNotes.onClick = _ -> performCommand(new SelectAllItemsCommand(true, false)); - menubarItemSelectInverse.onClick = _ -> performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection)); + menubarItemSelectAllEvents.onClick = _ -> performCommand(new SelectAllItemsCommand(false, true)); - menubarItemSelectNone.onClick = _ -> performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + menubarItemSelectInverse.onClick = _ -> performCommand(new InvertSelectedItemsCommand()); + + menubarItemSelectNone.onClick = _ -> performCommand(new DeselectAllItemsCommand()); menubarItemPlaytestFull.onClick = _ -> testSongInPlayState(false); menubarItemPlaytestMinimal.onClick = _ -> testSongInPlayState(true); @@ -2989,7 +3097,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (holdNoteSprite == null || holdNoteSprite.noteData == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; - if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - PLAYBAR_HEIGHT, MENU_BAR_HEIGHT)) { holdNoteSprite.kill(); } @@ -3025,7 +3133,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Resolve an issue where dragging an event too far would cause it to be hidden. var isSelectedAndDragged = currentEventSelection.fastContains(eventSprite.eventData) && (dragTargetCurrentStep != 0); - if ((eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD) + if ((eventSprite.isEventVisible(FlxG.height - PLAYBAR_HEIGHT, MENU_BAR_HEIGHT) && currentSongChartEventData.fastContains(eventSprite.eventData)) || isSelectedAndDragged) { @@ -3569,7 +3677,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // Clicked on the playhead scroll area. // Move the playhead to the cursor position. - this.playheadPositionInPixels = FlxG.mouse.screenY - MENU_BAR_HEIGHT - GRID_TOP_PAD; + this.playheadPositionInPixels = FlxG.mouse.screenY - (GRID_INITIAL_Y_POS); moveSongToScrollPosition(); // Cursor should be a grabby hand. @@ -3662,7 +3770,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { // Set the selection. - performCommand(new SetItemSelectionCommand(notesToSelect, eventsToSelect, currentNoteSelection, currentEventSelection)); + performCommand(new SetItemSelectionCommand(notesToSelect, eventsToSelect)); } } else @@ -3675,7 +3783,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0); if (shouldDeselect) { - performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + performCommand(new DeselectAllItemsCommand()); } } } @@ -3780,12 +3888,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (highlightedNote != null && highlightedNote.noteData != null) { // Click a note to select it. - performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection)); + performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [])); } else if (highlightedEvent != null && highlightedEvent.eventData != null) { // Click an event to select it. - performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); + performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData])); } else { @@ -3793,7 +3901,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0); if (shouldDeselect) { - performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + performCommand(new DeselectAllItemsCommand()); } } } @@ -3808,7 +3916,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var shouldDeselect:Bool = !wasCursorOverHaxeUI && (currentNoteSelection.length > 0 || currentEventSelection.length > 0); if (shouldDeselect) { - performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + performCommand(new DeselectAllItemsCommand()); } } } @@ -4049,7 +4157,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { // If you click an unselected note, and aren't holding Control, deselect everything else. - performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection)); + performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [])); } } else if (highlightedEvent != null && highlightedEvent.eventData != null) @@ -4062,7 +4170,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { // If you click an unselected event, and aren't holding Control, deselect everything else. - performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); + performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData])); } } else @@ -4324,7 +4432,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (currentSongMetadata.playData.characters.player != charPlayer.charId) { - if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player; + if (healthIconBF != null) + { + healthIconBF.characterId = currentSongMetadata.playData.characters.player; + } charPlayer.loadCharacter(currentSongMetadata.playData.characters.player); charPlayer.characterType = CharacterType.BF; @@ -4360,7 +4471,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (currentSongMetadata.playData.characters.opponent != charPlayer.charId) { - if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent; + if (healthIconDad != null) + { + healthIconDad.characterId = currentSongMetadata.playData.characters.opponent; + } charPlayer.loadCharacter(currentSongMetadata.playData.characters.opponent); charPlayer.characterType = CharacterType.DAD; @@ -4378,6 +4492,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } + function handleSelectionButtons():Void + { + // Make sure buttons are never nudged out of the correct spot. + // TODO: Why do these even move in the first place? The camera never moves, LOL. + buttonSelectOpponent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2; + buttonSelectPlayer.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2; + buttonSelectEvent.y = GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT - 2; + } + /** * Handles display elements for the playbar at the bottom. */ @@ -4486,11 +4609,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState healthIconBF.size *= 0.5; // Make the icon smaller in Chart Editor. healthIconBF.flipX = !healthIconBF.flipX; // BF faces the other way. } + if (buttonSelectPlayer != null) + { + buttonSelectPlayer.text = charDataBF?.name ?? 'Player'; + } if (healthIconDad != null) { healthIconDad.configure(charDataDad?.healthIcon); healthIconDad.size *= 0.5; // Make the icon smaller in Chart Editor. } + if (buttonSelectOpponent != null) + { + buttonSelectOpponent.text = charDataDad?.name ?? 'Opponent'; + } healthIconsDirty = false; } @@ -4498,15 +4629,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (healthIconBF != null) { // Base X position to the right of the grid. - healthIconBF.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 45 - (healthIconBF.width / 2)); - healthIconBF.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconBF.height / 2)); + var xOffset = 45 - (healthIconBF.width / 2); + healthIconBF.x = (gridTiledSprite == null) ? (0) : (GRID_X_POS + gridTiledSprite.width + xOffset); + var yOffset = 30 - (healthIconBF.height / 2); + healthIconBF.y = (gridTiledSprite == null) ? (0) : (GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT) + yOffset; } // Visibly center the Dad health icon. if (healthIconDad != null) { - healthIconDad.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x - 45 - (healthIconDad.width / 2)); - healthIconDad.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconDad.height / 2)); + var xOffset = 45 + (healthIconDad.width / 2); + healthIconDad.x = (gridTiledSprite == null) ? (0) : (GRID_X_POS - xOffset); + var yOffset = 30 - (healthIconDad.height / 2); + healthIconDad.y = (gridTiledSprite == null) ? (0) : (GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT) + yOffset; } } @@ -4586,54 +4721,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // CTRL + C = Copy if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.C) { - if (currentNoteSelection.length > 0) - { - txtCopyNotif.visible = true; - txtCopyNotif.text = "Copied " + currentNoteSelection.length + " notes to clipboard"; - txtCopyNotif.x = FlxG.mouse.x - (txtCopyNotif.width / 2); - txtCopyNotif.y = FlxG.mouse.y - 16; - FlxTween.tween(txtCopyNotif, {y: txtCopyNotif.y - 32}, 0.5, - { - type: FlxTween.ONESHOT, - ease: FlxEase.quadOut, - onComplete: function(_) { - txtCopyNotif.visible = false; - } - }); - - for (note in renderedNotes.members) - { - if (isNoteSelected(note.noteData)) - { - FlxTween.globalManager.cancelTweensOf(note); - FlxTween.globalManager.cancelTweensOf(note.scale); - note.playNoteAnimation(); - var prevX:Float = note.scale.x; - var prevY:Float = note.scale.y; - - note.scale.x *= 1.2; - note.scale.y *= 1.2; - - note.angle = FlxG.random.bool() ? -10 : 10; - FlxTween.tween(note, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut}); - - FlxTween.tween(note.scale, {"y": prevX, "x": prevY}, 0.7, - { - ease: FlxEase.elasticOut, - onComplete: function(_) { - note.playNoteAnimation(); - } - }); - } - } - } - - // We don't need a command for this since we can't undo it. - SongDataUtils.writeItemsToClipboard( - { - notes: SongDataUtils.buildNoteClipboard(currentNoteSelection), - events: SongDataUtils.buildEventClipboard(currentEventSelection), - }); + performCommand(new CopyItemsCommand(currentNoteSelection, currentEventSelection)); } // CTRL + X = Cut @@ -4694,25 +4782,50 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new FlipNotesCommand(currentNoteSelection)); } - // CTRL + A = Select All + // CTRL + A = Select All Notes if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.A) { // Select all items. - performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection)); + if (FlxG.keys.pressed.ALT) + { + if (FlxG.keys.pressed.SHIFT) + { + // CTRL + ALT + SHIFT + A = Append All Events to Selection + performCommand(new SelectItemsCommand([], currentSongChartEventData)); + } + else + { + // CTRL + ALT + A = Set Selection to All Events + performCommand(new SelectAllItemsCommand(false, true)); + } + } + else + { + if (FlxG.keys.pressed.SHIFT) + { + // CTRL + SHIFT + A = Append All Notes to Selection + performCommand(new SelectItemsCommand(currentSongChartNoteData, [])); + } + else + { + // CTRL + A = Set Selection to All Notes + performCommand(new SelectAllItemsCommand(true, false)); + } + } } // CTRL + I = Select Inverse if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.I) { // Select unselected items and deselect selected items. - performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection)); + performCommand(new InvertSelectedItemsCommand()); } // CTRL + D = Select None if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.D) { // Deselect all items. - performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + performCommand(new DeselectAllItemsCommand()); } } @@ -4878,13 +4991,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState * 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. + * @param purgeRedoStack If `true`, the redo stack will be cleared after performing the command. */ function performCommand(command:ChartEditorCommand, purgeRedoStack:Bool = true):Void { command.execute(this); - undoHistory.push(command); - commandHistoryDirty = true; + if (command.shouldAddToHistory(this)) + { + undoHistory.push(command); + commandHistoryDirty = true; + } if (purgeRedoStack) redoHistory = []; } @@ -4895,6 +5011,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function undoCommand(command:ChartEditorCommand):Void { command.undo(this); + // Note, if we are undoing a command, it should already be in the history, + // therefore we don't need to check `shouldAddToHistory(state)` redoHistory.push(command); commandHistoryDirty = true; } @@ -5515,6 +5633,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState @:privateAccess ChartEditorNoteSprite.noteFrameCollection = null; + + // Stop the music. + welcomeMusic.destroy(); + audioInstTrack.destroy(); + audioVocalTrackGroup.destroy(); } function applyCanQuickSave():Void diff --git a/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx b/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx index 9bf8ec3db..a878ee687 100644 --- a/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/AddEventsCommand.hx @@ -59,6 +59,12 @@ class AddEventsCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (events.length > 0); + } + public function toString():String { var len:Int = events.length; diff --git a/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx b/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx index ce4e73ea2..ea984c82d 100644 --- a/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx +++ b/source/funkin/ui/debug/charting/commands/AddNotesCommand.hx @@ -59,6 +59,12 @@ class AddNotesCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (notes.length > 0); + } + public function toString():String { if (notes.length == 1) diff --git a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx index 3c45c1168..5264623f6 100644 --- a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx +++ b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx @@ -54,6 +54,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (targetBPM != previousBPM); + } + public function toString():String { return 'Change Starting BPM to ${targetBPM}'; diff --git a/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx index cfa169908..1fa86ad94 100644 --- a/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/commands/ChartEditorCommand.hx @@ -6,6 +6,8 @@ package funkin.ui.debug.charting.commands; * * To make a functionality compatible with the undo/redo history, create a new class * that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())` + * + * NOTE: Make the constructor very simple, as it may be called without executing by the command palette. */ interface ChartEditorCommand { @@ -22,6 +24,15 @@ interface ChartEditorCommand */ public function undo(state:ChartEditorState):Void; + /** + * Return whether or not this command should be appended to the in the undo/redo history. + * Generally this should be true, it should only be false if the command is minor and non-destructive, + * like copying to the clipboard. + * + * Called after `execute()` is performed. + */ + public function shouldAddToHistory(state:ChartEditorState):Bool; + /** * Get a short description of the action (for the UI). * For example, return `Add Left Note` to display `Undo Add Left Note` in the menu. diff --git a/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx new file mode 100644 index 000000000..4361f867f --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx @@ -0,0 +1,144 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongDataUtils; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; + +/** + * Command that copies a given set of notes and song events to the clipboard, + * without deleting them from the chart editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class CopyItemsCommand implements ChartEditorCommand +{ + var notes:Array; + var events:Array; + + public function new(notes:Array, events:Array) + { + this.notes = notes; + this.events = events; + } + + public function execute(state:ChartEditorState):Void + { + // Calculate a single time offset for all the notes and events. + var timeOffset:Null = state.currentNoteSelection.length > 0 ? Std.int(state.currentNoteSelection[0].time) : null; + if (state.currentEventSelection.length > 0) + { + if (timeOffset == null || state.currentEventSelection[0].time < timeOffset) + { + timeOffset = Std.int(state.currentEventSelection[0].time); + } + } + + SongDataUtils.writeItemsToClipboard( + { + notes: SongDataUtils.buildNoteClipboard(state.currentNoteSelection, timeOffset), + events: SongDataUtils.buildEventClipboard(state.currentEventSelection, timeOffset), + }); + + performVisuals(state); + } + + function performVisuals(state:ChartEditorState):Void + { + if (state.currentNoteSelection.length > 0) + { + // Display the "Copied Notes" text. + if (state.txtCopyNotif != null) + { + state.txtCopyNotif.visible = true; + state.txtCopyNotif.text = "Copied " + state.currentNoteSelection.length + " notes to clipboard"; + state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2); + state.txtCopyNotif.y = FlxG.mouse.y - 16; + FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5, + { + type: FlxTween.ONESHOT, + ease: FlxEase.quadOut, + onComplete: function(_) { + state.txtCopyNotif.visible = false; + } + }); + } + + // Wiggle the notes. + for (note in state.renderedNotes.members) + { + if (state.isNoteSelected(note.noteData)) + { + FlxTween.globalManager.cancelTweensOf(note); + FlxTween.globalManager.cancelTweensOf(note.scale); + note.playNoteAnimation(); + var prevX:Float = note.scale.x; + var prevY:Float = note.scale.y; + + note.scale.x *= 1.2; + note.scale.y *= 1.2; + + note.angle = FlxG.random.bool() ? -10 : 10; + FlxTween.tween(note, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut}); + + FlxTween.tween(note.scale, {"y": prevX, "x": prevY}, 0.7, + { + ease: FlxEase.elasticOut, + onComplete: function(_) { + note.playNoteAnimation(); + } + }); + } + } + + // Wiggle the events. + for (event in state.renderedEvents.members) + { + if (state.isEventSelected(event.eventData)) + { + FlxTween.globalManager.cancelTweensOf(event); + FlxTween.globalManager.cancelTweensOf(event.scale); + event.playAnimation(); + var prevX:Float = event.scale.x; + var prevY:Float = event.scale.y; + + event.scale.x *= 1.2; + event.scale.y *= 1.2; + + event.angle = FlxG.random.bool() ? -10 : 10; + FlxTween.tween(event, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut}); + + FlxTween.tween(event.scale, {"y": prevX, "x": prevY}, 0.7, + { + ease: FlxEase.elasticOut, + onComplete: function(_) { + event.playAnimation(); + } + }); + } + } + } + } + + public function undo(state:ChartEditorState):Void + { + // This command is not undoable. Do nothing. + } + + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is not undoable. Don't add it to the history. + return false; + } + + public function toString():String + { + var len:Int = notes.length + events.length; + + if (notes.length == 0) return 'Copy $len Events to Clipboard'; + else if (events.length == 0) return 'Copy $len Notes to Clipboard'; + else + return 'Copy $len Items to Clipboard'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx b/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx index d0301b1ec..6cf674f80 100644 --- a/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/CutItemsCommand.hx @@ -56,6 +56,12 @@ class CutItemsCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Always add it to the history. + return (notes.length > 0 || events.length > 0); + } + public function toString():String { var len:Int = notes.length + events.length; diff --git a/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx b/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx index cbde0ab3d..5bfef76cc 100644 --- a/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/DeselectAllItemsCommand.hx @@ -10,17 +10,16 @@ import funkin.data.song.SongData.SongEventData; @:access(funkin.ui.debug.charting.ChartEditorState) class DeselectAllItemsCommand implements ChartEditorCommand { - var previousNoteSelection:Array; - var previousEventSelection:Array; + var previousNoteSelection:Array = []; + var previousEventSelection:Array = []; - public function new(?previousNoteSelection:Array, ?previousEventSelection:Array) - { - this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; - this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; - } + public function new() {} public function execute(state:ChartEditorState):Void { + this.previousNoteSelection = state.currentNoteSelection; + this.previousEventSelection = state.currentEventSelection; + state.currentNoteSelection = []; state.currentEventSelection = []; @@ -35,6 +34,12 @@ class DeselectAllItemsCommand implements ChartEditorCommand state.noteDisplayDirty = true; } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (previousNoteSelection.length > 0 || previousEventSelection.length > 0); + } + public function toString():String { return 'Deselect All Items'; diff --git a/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx index d679b5363..6a115a26a 100644 --- a/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/DeselectItemsCommand.hx @@ -45,16 +45,27 @@ class DeselectItemsCommand implements ChartEditorCommand state.notePreviewDirty = true; } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (notes.length > 0 || events.length > 0); + } + public function toString():String { - var noteCount = notes.length + events.length; + var isPlural = (notes.length + events.length) > 1; + var notesOnly = (notes.length > 0 && events.length == 0); + var eventsOnly = (notes.length == 0 && events.length > 0); - if (noteCount == 1) + if (notesOnly) { - var dir:String = notes[0].getDirectionName(); - return 'Deselect $dir Items'; + return 'Deselect ${notes.length} ${isPlural ? 'Notes' : 'Note'}'; + } + else if (eventsOnly) + { + return 'Deselect ${events.length} ${isPlural ? 'Events' : 'Event'}'; } - return 'Deselect ${noteCount} Items'; + return 'Deselect ${notes.length + events.length} Items'; } } diff --git a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx index 47da0dde5..2479feb0b 100644 --- a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx +++ b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx @@ -45,6 +45,12 @@ class ExtendNoteLengthCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (oldLength != newLength); + } + public function toString():String { return 'Extend Note Length'; diff --git a/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx b/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx index da8ec7fbc..f54ffed15 100644 --- a/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx +++ b/source/funkin/ui/debug/charting/commands/FlipNotesCommand.hx @@ -51,6 +51,12 @@ class FlipNotesCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (notes.length > 0); + } + public function toString():String { var len:Int = notes.length; diff --git a/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx b/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx index 6e37bcc03..d9a28f463 100644 --- a/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/InvertSelectedItemsCommand.hx @@ -12,19 +12,19 @@ import funkin.data.song.SongDataUtils; @:access(funkin.ui.debug.charting.ChartEditorState) class InvertSelectedItemsCommand implements ChartEditorCommand { - var previousNoteSelection:Array; - var previousEventSelection:Array; + var previousNoteSelection:Array = []; + var previousEventSelection:Array = []; - public function new(?previousNoteSelection:Array, ?previousEventSelection:Array) - { - this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; - this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; - } + public function new() {} public function execute(state:ChartEditorState):Void { + this.previousNoteSelection = state.currentNoteSelection; + this.previousEventSelection = state.currentEventSelection; + state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection); state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection); + state.noteDisplayDirty = true; } @@ -36,6 +36,12 @@ class InvertSelectedItemsCommand implements ChartEditorCommand state.noteDisplayDirty = true; } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (previousNoteSelection.length > 0 || previousEventSelection.length > 0); + } + public function toString():String { return 'Invert Selected Items'; diff --git a/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx index efe9c25d5..a0368f908 100644 --- a/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx @@ -65,6 +65,12 @@ class MoveEventsCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (events.length > 0); + } + public function toString():String { var len:Int = events.length; diff --git a/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx index 2eedbbf03..4fa1e2f87 100644 --- a/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx @@ -88,6 +88,12 @@ class MoveItemsCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (notes.length > 0 || events.length > 0); + } + public function toString():String { var len:Int = notes.length + events.length; diff --git a/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx index 8bce747a1..37ed61d72 100644 --- a/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx +++ b/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx @@ -67,6 +67,12 @@ class MoveNotesCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (notes.length > 0); + } + public function toString():String { var len:Int = notes.length; diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 75382da41..bba6ae866 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -71,6 +71,12 @@ class PasteItemsCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (addedNotes.length > 0 || addedEvents.length > 0); + } + public function toString():String { var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard(); diff --git a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx index 7e620c210..b4d913607 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveEventsCommand.hx @@ -48,6 +48,12 @@ class RemoveEventsCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (events.length > 0); + } + public function toString():String { if (events.length == 1 && events[0] != null) diff --git a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx index 77184209e..69317aff4 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveItemsCommand.hx @@ -62,6 +62,12 @@ class RemoveItemsCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (notes.length > 0 || events.length > 0); + } + public function toString():String { return 'Remove ${notes.length + events.length} Items'; diff --git a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx index e189be83e..4811f831d 100644 --- a/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx +++ b/source/funkin/ui/debug/charting/commands/RemoveNotesCommand.hx @@ -50,6 +50,12 @@ class RemoveNotesCommand implements ChartEditorCommand state.sortChartData(); } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (notes.length > 0); + } + public function toString():String { if (notes.length == 1 && notes[0] != null) diff --git a/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx index e1a4dceaa..f550e044b 100644 --- a/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SelectAllItemsCommand.hx @@ -10,19 +10,25 @@ import funkin.data.song.SongData.SongEventData; @:access(funkin.ui.debug.charting.ChartEditorState) class SelectAllItemsCommand implements ChartEditorCommand { - var previousNoteSelection:Array; - var previousEventSelection:Array; + var shouldSelectNotes:Bool; + var shouldSelectEvents:Bool; - public function new(?previousNoteSelection:Array, ?previousEventSelection:Array) + var previousNoteSelection:Array = []; + var previousEventSelection:Array = []; + + public function new(shouldSelectNotes:Bool, shouldSelectEvents:Bool) { - this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; - this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; + this.shouldSelectNotes = shouldSelectNotes; + this.shouldSelectEvents = shouldSelectEvents; } public function execute(state:ChartEditorState):Void { - state.currentNoteSelection = state.currentSongChartNoteData; - state.currentEventSelection = state.currentSongChartEventData; + this.previousNoteSelection = state.currentNoteSelection; + this.previousEventSelection = state.currentEventSelection; + + state.currentNoteSelection = shouldSelectNotes ? state.currentSongChartNoteData : []; + state.currentEventSelection = shouldSelectEvents ? state.currentSongChartEventData : []; state.noteDisplayDirty = true; } @@ -35,8 +41,29 @@ class SelectAllItemsCommand implements ChartEditorCommand state.noteDisplayDirty = true; } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (state.currentNoteSelection.length > 0 || state.currentEventSelection.length > 0); + } + public function toString():String { - return 'Select All Items'; + if (shouldSelectNotes && !shouldSelectEvents) + { + return 'Select All Notes'; + } + else if (shouldSelectEvents && !shouldSelectNotes) + { + return 'Select All Events'; + } + else if (shouldSelectNotes && shouldSelectEvents) + { + return 'Select All Notes and Events'; + } + else + { + return 'Select Nothing (Huh?)'; + } } } diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx index abe8b9e35..17f45f946 100644 --- a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx @@ -15,10 +15,10 @@ class SelectItemsCommand implements ChartEditorCommand var notes:Array; var events:Array; - public function new(notes:Array, events:Array) + public function new(?notes:Array, ?events:Array) { - this.notes = notes; - this.events = events; + this.notes = notes ?? []; + this.events = events ?? []; } public function execute(state:ChartEditorState):Void @@ -46,6 +46,12 @@ class SelectItemsCommand implements ChartEditorCommand state.notePreviewDirty = true; } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // This command is undoable. Add to the history if we actually performed an action. + return (notes.length > 0 || events.length > 0); + } + public function toString():String { var len:Int = notes.length + events.length; diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx index a06aefabc..f9b4fb6d7 100644 --- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx @@ -13,20 +13,20 @@ class SetItemSelectionCommand implements ChartEditorCommand { var notes:Array; var events:Array; - var previousNoteSelection:Array; - var previousEventSelection:Array; + var previousNoteSelection:Array = []; + var previousEventSelection:Array = []; - public function new(notes:Array, events:Array, previousNoteSelection:Array, - previousEventSelection:Array) + public function new(notes:Array, events:Array) { this.notes = notes; this.events = events; - this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection; - this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection; } public function execute(state:ChartEditorState):Void { + this.previousNoteSelection = state.currentNoteSelection; + this.previousEventSelection = state.currentEventSelection; + state.currentNoteSelection = notes; state.currentEventSelection = events; @@ -41,8 +41,14 @@ class SetItemSelectionCommand implements ChartEditorCommand state.noteDisplayDirty = true; } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // Add to the history if we actually performed an action. + return (state.currentNoteSelection != previousNoteSelection && state.currentEventSelection != previousEventSelection); + } + public function toString():String { - return 'Select ${notes.length} Items'; + return 'Select ${notes.length + events.length} Items'; } } diff --git a/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx b/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx index 75e7e5afe..30c2edb61 100644 --- a/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SwitchDifficultyCommand.hx @@ -38,6 +38,12 @@ class SwitchDifficultyCommand implements ChartEditorCommand state.notePreviewDirty = true; } + public function shouldAddToHistory(state:ChartEditorState):Bool + { + // Add to the history if we actually performed an action. + return (prevVariation != newVariation || prevDifficulty != newDifficulty); + } + public function toString():String { return 'Switch Difficulty'; diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx index 4c9d91407..e0070cc7b 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx @@ -119,8 +119,10 @@ class ChartEditorEventSprite extends FlxSprite return DEFAULT_EVENT; } - public function playAnimation(name:String):Void + public function playAnimation(?name:String):Void { + if (name == null) name = eventData?.event ?? DEFAULT_EVENT; + var correctedName = correctAnimationName(name); this.animation.play(correctedName); refresh(); diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx index 598cbb544..8d9ec6743 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx @@ -70,9 +70,9 @@ class ChartEditorNotePreview extends FlxSprite * @param event The data for the event. * @param songLengthInMs The total length of the song in milliseconds. */ - public function addEvent(event:SongEventData, songLengthInMs:Int):Void + public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void { - drawNote(-1, false, Std.int(event.time), songLengthInMs); + drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection); } /** @@ -114,6 +114,19 @@ class ChartEditorNotePreview extends FlxSprite } } + /** + * Add an array of selected events to the preview. + * @param events The data for the events. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addSelectedEvents(events:Array, songLengthInMs:Int):Void + { + for (event in events) + { + addEvent(event, songLengthInMs, true); + } + } + /** * Draws a note on the preview. * @param dir Note data. diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 990ab41ae..d4fcc4638 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -185,9 +185,10 @@ class ChartEditorAudioHandler state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); state.audioVisGroup.addPlayerVis(vocalTrack); state.audioVisGroup.playerVis.x = 885; - state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195; - state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; + state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195; // The height of the visualizer, in time. + state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; // The height of the visualizer, in pixels. state.audioVisGroup.playerVis.detail = 1; + state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS); state.audioVocalTrackGroup.playerVoicesOffset = state.currentSongOffsets.getVocalOffset(charId); return true; @@ -195,10 +196,10 @@ class ChartEditorAudioHandler state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); state.audioVisGroup.addOpponentVis(vocalTrack); state.audioVisGroup.opponentVis.x = 435; - - state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195; - state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; + state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195; // The height of the visualizer, in time. + state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; // The height of the visualizer, in pixels. state.audioVisGroup.opponentVis.detail = 1; + state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS); state.audioVocalTrackGroup.opponentVoicesOffset = state.currentSongOffsets.getVocalOffset(charId); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx index f7105d2f7..62f1f4cbc 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx @@ -2,6 +2,10 @@ package funkin.ui.debug.charting.handlers; import funkin.util.PlatformUtil; +/** + * Handles modifying the shortcut text of menu items based on the current platform. + * On MacOS, `Ctrl`, `Alt`, and `Shift` are replaced with `⌘` (or `^`), `⌥`, and `⇧`, respectively. + */ @:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorShortcutHandler { @@ -18,7 +22,8 @@ class ChartEditorShortcutHandler state.menubarItemCopy.shortcutText = ctrlOrCmd('C'); state.menubarItemPaste.shortcutText = ctrlOrCmd('V'); - state.menubarItemSelectAll.shortcutText = ctrlOrCmd('A'); + state.menubarItemSelectAllNotes.shortcutText = ctrlOrCmd('A'); + state.menubarItemSelectAllEvents.shortcutText = ctrlOrCmd(alt('A')); state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I'); state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D'); state.menubarItemSelectBeforeCursor.shortcutText = shift('Home'); From 336810b628631b0e1dcb67b25774bac1ce815395 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 4 Jan 2024 10:00:39 -0500 Subject: [PATCH 02/60] Tooltips when hovering over chart events --- hmm.json | 4 +- source/funkin/data/event/SongEventData.hx | 109 ++++++++++++++---- source/funkin/data/song/SongData.hx | 61 ++++++++++ .../funkin/play/event/FocusCameraSongEvent.hx | 4 +- .../play/event/PlayAnimationSongEvent.hx | 4 +- .../play/event/SetCameraBopSongEvent.hx | 4 +- .../funkin/play/event/ZoomCameraSongEvent.hx | 4 +- .../ui/debug/charting/ChartEditorState.hx | 3 +- .../components/ChartEditorEventSprite.hx | 42 ++++++- source/funkin/util/HaxeUIUtil.hx | 17 +++ 10 files changed, 217 insertions(+), 35 deletions(-) create mode 100644 source/funkin/util/HaxeUIUtil.hx diff --git a/hmm.json b/hmm.json index 57fbbb555..be9e2dd26 100644 --- a/hmm.json +++ b/hmm.json @@ -54,14 +54,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "e765a3e0b7a653823e8dec765e04623f27f573f8", + "ref": "67c5700e253ff8892589a95945a7799f34ae4df0", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "7a517d561eff49d8123c128bf9f5c1123b84d014", + "ref": "2b9cff727999b53ed292b1675ac1c9089ac77600", "url": "https://github.com/haxeui/haxeui-flixel" }, { diff --git a/source/funkin/data/event/SongEventData.hx b/source/funkin/data/event/SongEventData.hx index 7a167b031..a4a41e3a0 100644 --- a/source/funkin/data/event/SongEventData.hx +++ b/source/funkin/data/event/SongEventData.hx @@ -161,35 +161,71 @@ class SongEventParser } } -enum abstract SongEventFieldType(String) from String to String +@:forward(name, title, type, keys, min, max, step, defaultValue, iterator) +abstract SongEventSchema(SongEventSchemaRaw) { - /** - * The STRING type will display as a text field. - */ - var STRING = "string"; + public function new(?fields:Array) + { + this = fields; + } - /** - * The INTEGER type will display as a text field that only accepts numbers. - */ - var INTEGER = "integer"; + @:arrayAccess + public function getByName(name:String):SongEventSchemaField + { + for (field in this) + { + if (field.name == name) return field; + } - /** - * The FLOAT type will display as a text field that only accepts numbers. - */ - var FLOAT = "float"; + return null; + } - /** - * The BOOL type will display as a checkbox. - */ - var BOOL = "bool"; + public function getFirstField():SongEventSchemaField + { + return this[0]; + } - /** - * The ENUM type will display as a dropdown. - * Make sure to specify the `keys` field in the schema. - */ - var ENUM = "enum"; + public function stringifyFieldValue(name:String, value:Dynamic):String + { + var field:SongEventSchemaField = getByName(name); + if (field == null) return 'Unknown'; + + switch (field.type) + { + case SongEventFieldType.STRING: + return Std.string(value); + case SongEventFieldType.INTEGER: + return Std.string(value); + case SongEventFieldType.FLOAT: + return Std.string(value); + case SongEventFieldType.BOOL: + return Std.string(value); + case SongEventFieldType.ENUM: + for (key in field.keys.keys()) + { + if (field.keys.get(key) == value) return key; + } + return Std.string(value); + default: + return 'Unknown'; + } + } + + @:arrayAccess + public inline function get(key:Int) + { + return this[key]; + } + + @:arrayAccess + public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField + { + return this[k] = v; + } } +typedef SongEventSchemaRaw = Array; + typedef SongEventSchemaField = { /** @@ -240,4 +276,31 @@ typedef SongEventSchemaField = ?defaultValue:Dynamic, } -typedef SongEventSchema = Array; +enum abstract SongEventFieldType(String) from String to String +{ + /** + * The STRING type will display as a text field. + */ + var STRING = "string"; + + /** + * The INTEGER type will display as a text field that only accepts numbers. + */ + var INTEGER = "integer"; + + /** + * The FLOAT type will display as a text field that only accepts numbers. + */ + var FLOAT = "float"; + + /** + * The BOOL type will display as a checkbox. + */ + var BOOL = "bool"; + + /** + * The ENUM type will display as a dropdown. + * Make sure to specify the `keys` field in the schema. + */ + var ENUM = "enum"; +} diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 600871e2f..de73cd957 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -1,5 +1,8 @@ package funkin.data.song; +import funkin.play.event.SongEvent; +import funkin.data.event.SongEventData.SongEventParser; +import funkin.data.event.SongEventData.SongEventSchema; import funkin.data.song.SongRegistry; import thx.semver.Version; @@ -617,6 +620,38 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR this = new SongEventDataRaw(time, event, value); } + public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic + { + if (this.value == null) return {}; + if (Std.isOfType(this.value, Array)) + { + var result:haxe.DynamicAccess = {}; + result.set(defaultKey, this.value); + return cast result; + } + else if (Reflect.isObject(this.value)) + { + // We enter this case if the value is a struct. + return cast this.value; + } + else + { + var result:haxe.DynamicAccess = {}; + result.set(defaultKey, this.value); + return cast result; + } + } + + public inline function getHandler():Null + { + return SongEventParser.getEvent(this.event); + } + + public inline function getSchema():Null + { + return SongEventParser.getEventSchema(this.event); + } + public inline function getDynamic(key:String):Null { return this.value == null ? null : Reflect.field(this.value, key); @@ -662,6 +697,32 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR return this.value == null ? null : cast Reflect.field(this.value, key); } + public function buildTooltip():String + { + var eventHandler = getHandler(); + var eventSchema = getSchema(); + + if (eventSchema == null) return 'Unknown Event: ${this.event}'; + + var result = '${eventHandler.getTitle()}'; + + var defaultKey = eventSchema.getFirstField()?.name; + var valueStruct:haxe.DynamicAccess = valueAsStruct(defaultKey); + + for (pair in valueStruct.keyValueIterator()) + { + var key = pair.key; + var value = pair.value; + + var title = eventSchema.getByName(key)?.title ?? 'UnknownField'; + var valueStr = eventSchema.stringifyFieldValue(key, value); + + result += '\n- ${title}: ${valueStr}'; + } + + return result; + } + public function clone():SongEventData { return new SongEventData(this.time, this.event, this.value); diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 5f63254b0..c91769eb5 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -132,7 +132,7 @@ class FocusCameraSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: "char", title: "Character", @@ -154,6 +154,6 @@ class FocusCameraSongEvent extends SongEvent step: 10.0, type: SongEventFieldType.FLOAT, } - ]; + ]); } } diff --git a/source/funkin/play/event/PlayAnimationSongEvent.hx b/source/funkin/play/event/PlayAnimationSongEvent.hx index 6bc625517..0f611874b 100644 --- a/source/funkin/play/event/PlayAnimationSongEvent.hx +++ b/source/funkin/play/event/PlayAnimationSongEvent.hx @@ -89,7 +89,7 @@ class PlayAnimationSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'target', title: 'Target', @@ -108,6 +108,6 @@ class PlayAnimationSongEvent extends SongEvent type: SongEventFieldType.BOOL, defaultValue: false } - ]; + ]); } } diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index 3cdeb9a67..7d5fd4699 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -72,7 +72,7 @@ class SetCameraBopSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'intensity', title: 'Intensity', @@ -87,6 +87,6 @@ class SetCameraBopSongEvent extends SongEvent step: 1, type: SongEventFieldType.INTEGER, } - ]; + ]); } } diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index 1ae76039e..9a361f71b 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -99,7 +99,7 @@ class ZoomCameraSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'zoom', title: 'Zoom Level', @@ -145,6 +145,6 @@ class ZoomCameraSongEvent extends SongEvent 'Elastic In/Out' => 'elasticInOut', ] } - ]; + ]); } } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 4f96fad69..5c12e3408 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -2113,7 +2113,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(gridGhostHoldNote); gridGhostHoldNote.zIndex = 11; - gridGhostEvent = new ChartEditorEventSprite(this); + gridGhostEvent = new ChartEditorEventSprite(this, true); gridGhostEvent.alpha = 0.6; gridGhostEvent.eventData = new SongEventData(-1, '', {}); gridGhostEvent.visible = false; @@ -3127,6 +3127,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Setting event data resets position relative to the grid so we fix that. eventSprite.x += renderedEvents.x; eventSprite.y += renderedEvents.y; + eventSprite.updateTooltipPosition(); } // Add hold notes that have been made visible (but not their parents) diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx index 4c9d91407..cc9acf344 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx @@ -11,6 +11,9 @@ import flixel.graphics.frames.FlxFramesCollection; import flixel.graphics.frames.FlxTileFrames; import flixel.math.FlxPoint; import funkin.data.song.SongData.SongEventData; +import haxe.ui.tooltips.ToolTipRegionOptions; +import funkin.util.HaxeUIUtil; +import haxe.ui.tooltips.ToolTipManager; /** * A sprite that can be used to display a song event in a chart. @@ -36,6 +39,13 @@ class ChartEditorEventSprite extends FlxSprite public var overrideStepTime(default, set):Null = null; + public var tooltip:ToolTipRegionOptions; + + /** + * Whether this sprite is a "ghost" sprite used when hovering to place a new event. + */ + public var isGhost:Bool = false; + function set_overrideStepTime(value:Null):Null { if (overrideStepTime == value) return overrideStepTime; @@ -45,12 +55,14 @@ class ChartEditorEventSprite extends FlxSprite return overrideStepTime; } - public function new(parent:ChartEditorState) + public function new(parent:ChartEditorState, isGhost:Bool = false) { super(); this.parentState = parent; + this.isGhost = isGhost; + this.tooltip = HaxeUIUtil.buildTooltip('N/A'); this.frames = buildFrames(); buildAnimations(); @@ -140,6 +152,7 @@ class ChartEditorEventSprite extends FlxSprite // Disown parent. MAKE SURE TO REVIVE BEFORE REUSING this.kill(); this.visible = false; + updateTooltipPosition(); return null; } else @@ -151,6 +164,8 @@ class ChartEditorEventSprite extends FlxSprite this.eventData = value; // Update the position to match the note data. updateEventPosition(); + // Update the tooltip text. + this.tooltip.tipData = {text: this.eventData.buildTooltip()}; return this.eventData; } } @@ -169,6 +184,31 @@ class ChartEditorEventSprite extends FlxSprite this.x += origin.x; this.y += origin.y; } + + this.updateTooltipPosition(); + } + + public function updateTooltipPosition():Void + { + // No tooltip for ghost sprites. + if (this.isGhost) return; + + if (this.eventData == null) + { + // Disable the tooltip. + ToolTipManager.instance.unregisterTooltipRegion(this.tooltip); + } + else + { + // Update the position. + this.tooltip.left = this.x; + this.tooltip.top = this.y; + this.tooltip.width = this.width; + this.tooltip.height = this.height; + + // Enable the tooltip. + ToolTipManager.instance.registerTooltipRegion(this.tooltip); + } } /** diff --git a/source/funkin/util/HaxeUIUtil.hx b/source/funkin/util/HaxeUIUtil.hx new file mode 100644 index 000000000..1ffd9cd40 --- /dev/null +++ b/source/funkin/util/HaxeUIUtil.hx @@ -0,0 +1,17 @@ +package funkin.util; + +import haxe.ui.tooltips.ToolTipRegionOptions; + +class HaxeUIUtil +{ + public static function buildTooltip(text:String, ?left:Float, ?top:Float, ?width:Float, ?height:Float):ToolTipRegionOptions + { + return { + tipData: {text: text}, + left: left ?? 0.0, + top: top ?? 0.0, + width: width ?? 0.0, + height: height ?? 0.0 + } + } +} From 14df32d90860ad218dd4104d167a9b0cca3fc505 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 9 Jan 2024 14:48:20 -0500 Subject: [PATCH 03/60] Implement haxelib versions into crash logs --- Project.xml | 1 + source/funkin/util/Constants.hx | 7 ++- source/funkin/util/logging/CrashHandler.hx | 11 ++++ source/funkin/util/macro/HaxelibVersions.hx | 67 +++++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 source/funkin/util/macro/HaxelibVersions.hx diff --git a/Project.xml b/Project.xml index e0677b026..a298c919f 100644 --- a/Project.xml +++ b/Project.xml @@ -111,6 +111,7 @@ + diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 123267a49..197fa28e8 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -70,7 +70,7 @@ class Constants public static final URL_KICKSTARTER:String = 'https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/'; /** - * GIT REPO DATA + * REPOSITORY DATA */ // ============================== @@ -86,6 +86,11 @@ class Constants public static final GIT_HASH:String = funkin.util.macro.GitCommit.getGitCommitHash(); #end + /** + * The current library versions, as provided by hmm. + */ + public static final LIBRARY_VERSIONS:Array = funkin.util.macro.HaxelibVersions.getLibraryVersions(); + /** * COLORS */ diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index a21732048..e32ba2c42 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -123,6 +123,17 @@ class CrashHandler fullContents += '=====================\n'; + fullContents += 'Haxelibs: \n'; + + for (lib in Constants.LIBRARY_VERSIONS) + { + fullContents += '- ${lib}\n'; + } + + fullContents += '\n'; + + fullContents += '=====================\n'; + fullContents += '\n'; fullContents += message; diff --git a/source/funkin/util/macro/HaxelibVersions.hx b/source/funkin/util/macro/HaxelibVersions.hx new file mode 100644 index 000000000..f0317c397 --- /dev/null +++ b/source/funkin/util/macro/HaxelibVersions.hx @@ -0,0 +1,67 @@ +package funkin.util.macro; + +import haxe.io.Path; + +class HaxelibVersions +{ + public static macro function getLibraryVersions():haxe.macro.Expr.ExprOf> + { + #if !display + return macro $v{formatHmmData(readHmmData())}; + #else + // `#if display` is used for code completion. In this case returning an + // empty string is good enough; We don't want to call functions on every hint. + var commitHash:String = ""; + return macro $v{commitHashSplice}; + #end + } + + #if (debug && macro) + static function readHmmData():hmm.HmmConfig + { + return hmm.HmmConfig.HmmConfigs.readHmmJsonOrThrow(); + } + + static function formatHmmData(hmmData:hmm.HmmConfig):Array + { + var result:Array = []; + + for (library in hmmData.dependencies) + { + switch (library) + { + case Haxelib(name, version): + result.push('${name} haxelib(${o(version)})'); + case Git(name, url, ref, dir): + result.push('${name} git(${url}/${o(dir, '')}:${o(ref)})'); + case Mercurial(name, url, ref, dir): + result.push('${name} mercurial(${url}/${o(dir, '')}:${o(ref)})'); + case Dev(name, path): + result.push('${name} dev(${path})'); + } + } + + return result; + } + + static function o(option:haxe.ds.Option, defaultValue:String = 'None'):String + { + switch (option) + { + case Some(value): + return value; + case None: + return defaultValue; + } + } + + static function readLibraryCurrentVersion(libraryName:String):String + { + var path = Path.join([Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']); + // This is compile time so we should always have Sys available. + var result = sys.io.File.getContent(path); + + return result; + } + #end +} From 27d5a6d5a5975d87e52d968969a18e76523aa819 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Tue, 9 Jan 2024 19:31:39 -0500 Subject: [PATCH 04/60] Update Project.xml --- Project.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.xml b/Project.xml index a298c919f..74da3b749 100644 --- a/Project.xml +++ b/Project.xml @@ -128,7 +128,7 @@ - + From 59999aa8fd6fa8367e326f1d29a6d1721ebc31a6 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 10 Jan 2024 00:16:51 -0500 Subject: [PATCH 05/60] Fix an issue with release builds --- source/funkin/util/macro/HaxelibVersions.hx | 67 +++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 source/funkin/util/macro/HaxelibVersions.hx diff --git a/source/funkin/util/macro/HaxelibVersions.hx b/source/funkin/util/macro/HaxelibVersions.hx new file mode 100644 index 000000000..1a4699bba --- /dev/null +++ b/source/funkin/util/macro/HaxelibVersions.hx @@ -0,0 +1,67 @@ +package funkin.util.macro; + +import haxe.io.Path; + +class HaxelibVersions +{ + public static macro function getLibraryVersions():haxe.macro.Expr.ExprOf> + { + #if !display + return macro $v{formatHmmData(readHmmData())}; + #else + // `#if display` is used for code completion. In this case returning an + // empty string is good enough; We don't want to call functions on every hint. + var commitHash:Array = []; + return macro $v{commitHash}; + #end + } + + #if (macro) + static function readHmmData():hmm.HmmConfig + { + return hmm.HmmConfig.HmmConfigs.readHmmJsonOrThrow(); + } + + static function formatHmmData(hmmData:hmm.HmmConfig):Array + { + var result:Array = []; + + for (library in hmmData.dependencies) + { + switch (library) + { + case Haxelib(name, version): + result.push('${name} haxelib(${o(version)})'); + case Git(name, url, ref, dir): + result.push('${name} git(${url}/${o(dir, '')}:${o(ref)})'); + case Mercurial(name, url, ref, dir): + result.push('${name} mercurial(${url}/${o(dir, '')}:${o(ref)})'); + case Dev(name, path): + result.push('${name} dev(${path})'); + } + } + + return result; + } + + static function o(option:haxe.ds.Option, defaultValue:String = 'None'):String + { + switch (option) + { + case Some(value): + return value; + case None: + return defaultValue; + } + } + + static function readLibraryCurrentVersion(libraryName:String):String + { + var path = Path.join([Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']); + // This is compile time so we should always have Sys available. + var result = sys.io.File.getContent(path); + + return result; + } + #end +} From 043fb553f6b46fa3e4700777888cc3031cc82554 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 10 Jan 2024 00:20:00 -0500 Subject: [PATCH 06/60] Fix an issue causing an overflow error when using gamepad (WINDOWS ONLY) --- Project.xml | 5 +-- hmm.json | 2 +- source/funkin/play/PlayState.hx | 6 ++-- source/funkin/util/tools/Int64Tools.hx | 46 ++++++++++++++++---------- 4 files changed, 36 insertions(+), 23 deletions(-) diff --git a/Project.xml b/Project.xml index e0677b026..16af32868 100644 --- a/Project.xml +++ b/Project.xml @@ -111,6 +111,7 @@ + @@ -130,8 +131,8 @@ - - + diff --git a/hmm.json b/hmm.json index d461edd24..bc07b944d 100644 --- a/hmm.json +++ b/hmm.json @@ -107,7 +107,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "737b86f121cdc90358d59e2e527934f267c94a2c", + "ref": "17086ec7fa4535ddb54d01fdc00a318bb8ce6413", "url": "https://github.com/FunkinCrew/lime" }, { diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 995797dd1..9f98d3b04 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2270,8 +2270,10 @@ class PlayState extends MusicBeatSubState vocals.playerVolume = 1; // Calculate the input latency (do this as late as possible). - var inputLatencyMs:Float = haxe.Int64.toInt(PreciseInputManager.getCurrentTimestamp() - input.timestamp) / 1000.0 / 1000.0; - trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!'); + // trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}'); + var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp; + var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS; + // trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!'); // Get the offset and compensate for input latency. // Round inward (trim remainder) for consistency. diff --git a/source/funkin/util/tools/Int64Tools.hx b/source/funkin/util/tools/Int64Tools.hx index 75448b36f..d53fa315d 100644 --- a/source/funkin/util/tools/Int64Tools.hx +++ b/source/funkin/util/tools/Int64Tools.hx @@ -1,32 +1,42 @@ package funkin.util.tools; +import haxe.Int64; + /** - * @see https://github.com/fponticelli/thx.core/blob/master/src/thx/Int64s.hx + * Why `haxe.Int64` doesn't have a built-in `toFloat` function is beyond me. */ class Int64Tools { - static var min = haxe.Int64.make(0x80000000, 0); - static var one = haxe.Int64.make(0, 1); - static var two = haxe.Int64.ofInt(2); - static var zero = haxe.Int64.make(0, 0); - static var ten = haxe.Int64.ofInt(10); + private inline static var MAX_32_PRECISION:Float = 4294967296.0; - public static function toFloat(i:haxe.Int64):Float + public static function fromFloat(f:Float):Int64 { - var isNegative = false; - if (i < 0) + var h = Std.int(f / MAX_32_PRECISION); + var l = Std.int(f); + return Int64.make(h, l); + } + + public static function toFloat(i:Int64):Float + { + var f:Float = Int64.getLow(i); + if (f < 0) f += MAX_32_PRECISION; + return (Int64.getHigh(i) * MAX_32_PRECISION + f); + } + + public static function isToIntSafe(i:Int64):Bool + { + return i.high != i.low >> 31; + } + + public static function toIntSafe(i:Int64):Int + { + try { - if (i < min) return -9223372036854775808.0; // most -ve value can't be made +ve - isNegative = true; - i = -i; + return Int64.toInt(i); } - var multiplier = 1.0, ret = 0.0; - for (_ in 0...64) + catch (e:Dynamic) { - if (haxe.Int64.and(i, one) != zero) ret += multiplier; - multiplier *= 2.0; - i = haxe.Int64.shr(i, 1); + throw 'Could not represent value "${Int64.toStr(i)}" as an integer.'; } - return (isNegative ? -1 : 1) * ret; } } From eea9ac1883360dbd934e1935534379f59f3abd05 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 10 Jan 2024 20:27:40 -0500 Subject: [PATCH 07/60] Update to include linux fix (???) --- hmm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hmm.json b/hmm.json index bc07b944d..cced726e2 100644 --- a/hmm.json +++ b/hmm.json @@ -107,7 +107,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "17086ec7fa4535ddb54d01fdc00a318bb8ce6413", + "ref": "fff39ba6fc64969cd51987ef7491d9345043dc5d", "url": "https://github.com/FunkinCrew/lime" }, { From 06964ce1e3ee82fa6041a9b27245146c0f5812af Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 10 Jan 2024 20:58:06 -0500 Subject: [PATCH 08/60] Fix HTML5 build issue --- source/funkin/ui/debug/charting/ChartEditorState.hx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 1773a84fe..851e3b2fa 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -5333,6 +5333,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (displayAutosavePopup) { displayAutosavePopup = false; + #if sys Toolkit.callLater(() -> { var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]); this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [ @@ -5342,6 +5343,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } ]); }); + #else + // TODO: No auto-save on HTML5? + #end } moveSongToScrollPosition(); From fc12f956f60609c7c0dc8d17f979b0e6d8497918 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 10 Jan 2024 20:58:29 -0500 Subject: [PATCH 09/60] Fix alignment of character pixel icons in freeplay menu --- source/funkin/ui/freeplay/SongMenuItem.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 4e0772dfe..833187acb 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -94,7 +94,7 @@ class SongMenuItem extends FlxSpriteGroup add(songText); grpHide.add(songText); - pixelIcon = new FlxSprite(155, 15); + pixelIcon = new FlxSprite(160, 35); pixelIcon.makeGraphic(32, 32, 0x00000000); pixelIcon.antialiasing = false; pixelIcon.active = false; From 462eece09a2a30d495e038264ea0b39d6a3a997f Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 11 Jan 2024 00:30:00 -0500 Subject: [PATCH 10/60] Use proper difficulty rating data, proper clear percentage --- source/funkin/play/song/Song.hx | 6 + source/funkin/ui/AtlasText.hx | 1 + source/funkin/ui/freeplay/FreeplayState.hx | 144 +++++++++++++++++---- source/funkin/ui/freeplay/SongMenuItem.hx | 46 ++++--- 4 files changed, 150 insertions(+), 47 deletions(-) diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 9e5de6143..cde068f42 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -176,6 +176,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = null; + public function new(song:Song, diffId:String, variation:String) { this.song = song; diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx index fea09de54..186d87c2a 100644 --- a/source/funkin/ui/AtlasText.hx +++ b/source/funkin/ui/AtlasText.hx @@ -274,4 +274,5 @@ enum abstract AtlasFont(String) from String to String { var DEFAULT = "default"; var BOLD = "bold"; + var FREEPLAY_CLEAR = "freeplay-clear"; } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index f17c3d91e..cfe7f802a 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1,6 +1,5 @@ package funkin.ui.freeplay; -import funkin.input.Controls; import flash.text.TextField; import flixel.addons.display.FlxGridOverlay; import flixel.addons.transition.FlxTransitionableState; @@ -23,33 +22,35 @@ import flixel.tweens.FlxTween; import flixel.util.FlxColor; import flixel.util.FlxSpriteUtil; import flixel.util.FlxTimer; -import funkin.input.Controls.Control; import funkin.data.level.LevelRegistry; import funkin.data.song.SongRegistry; import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.graphics.shaders.AngleMask; import funkin.graphics.shaders.HSVShader; import funkin.graphics.shaders.PureColor; -import funkin.util.MathUtil; import funkin.graphics.shaders.StrokeShader; +import funkin.input.Controls; +import funkin.input.Controls.Control; import funkin.play.components.HealthIcon; import funkin.play.PlayState; import funkin.play.PlayStatePlaylist; import funkin.play.song.Song; import funkin.save.Save; import funkin.save.Save.SaveScoreData; +import funkin.ui.AtlasText; import funkin.ui.freeplay.BGScrollingText; import funkin.ui.freeplay.DifficultyStars; import funkin.ui.freeplay.DJBoyfriend; import funkin.ui.freeplay.FreeplayScore; import funkin.ui.freeplay.LetterSort; import funkin.ui.freeplay.SongMenuItem; +import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatState; import funkin.ui.MusicBeatSubState; -import funkin.ui.mainmenu.MainMenuState; import funkin.ui.transition.LoadingState; import funkin.ui.transition.StickerSubState; import funkin.util.MathUtil; +import funkin.util.MathUtil; import lime.app.Future; import lime.utils.Assets; @@ -64,7 +65,7 @@ class FreeplayState extends MusicBeatSubState var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY; var fp:FreeplayScore; - var txtCompletion:FlxText; + var txtCompletion:AtlasText; var lerpCompletion:Float = 0; var intendedCompletion:Float = 0; var lerpScore:Float = 0; @@ -87,6 +88,8 @@ class FreeplayState extends MusicBeatSubState var grpCapsules:FlxTypedGroup; var curCapsule:SongMenuItem; var curPlaying:Bool = false; + var ostName:FlxText; + var difficultyStars:DifficultyStars; var dj:DJBoyfriend; @@ -150,15 +153,10 @@ class FreeplayState extends MusicBeatSubState for (songId in LevelRegistry.instance.parseEntryData(levelId).songs) { var song:Song = SongRegistry.instance.fetchEntry(songId); - var songBaseDifficulty:SongDifficulty = song.getDifficulty(Constants.DEFAULT_DIFFICULTY); - var songName = songBaseDifficulty.songName; - var songOpponent = songBaseDifficulty.characters.opponent; - var songDifficulties = song.listDifficulties(); + songs.push(new FreeplaySongData(levelId, songId, song)); - songs.push(new FreeplaySongData(songId, songName, levelId, songOpponent, songDifficulties)); - - for (difficulty in songDifficulties) + for (difficulty in song.listDifficulties()) { diffIdsTotal.pushUnique(difficulty); } @@ -334,6 +332,8 @@ class FreeplayState extends MusicBeatSubState if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true; } + // NOTE: This is an AtlasSprite because we use an animation to bring it into view. + // TODO: Add the ability to select the album graphic. var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll")); albumArt.visible = false; add(albumArt); @@ -347,7 +347,7 @@ class FreeplayState extends MusicBeatSubState var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1')); var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite')); - var difficultyStars:DifficultyStars = new DifficultyStars(140, 39); + difficultyStars = new DifficultyStars(140, 39); difficultyStars.stars.visible = false; albumTitle.visible = false; @@ -382,10 +382,15 @@ class FreeplayState extends MusicBeatSubState add(overhangStuff); FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut}); - var fnfFreeplay:FlxText = new FlxText(0, 12, 0, "FREEPLAY", 48); + var fnfFreeplay:FlxText = new FlxText(4, 10, 0, "FREEPLAY", 48); fnfFreeplay.font = "VCR OSD Mono"; fnfFreeplay.visible = false; + ostName = new FlxText(4, 10, FlxG.width - 4 - 4, "OFFICIAL OST", 48); + ostName.font = "VCR OSD Mono"; + ostName.alignment = RIGHT; + ostName.visible = false; + exitMovers.set([overhangStuff, fnfFreeplay], { y: -overhangStuff.height, @@ -398,7 +403,7 @@ class FreeplayState extends MusicBeatSubState fnfFreeplay.shader = sillyStroke; add(fnfFreeplay); - var fnfHighscoreSpr:FlxSprite = new FlxSprite(890, 70); + var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70); fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore'); fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false); fnfHighscoreSpr.visible = false; @@ -415,8 +420,10 @@ class FreeplayState extends MusicBeatSubState fp.visible = false; add(fp); - txtCompletion = new FlxText(1200, 77, 0, "0", 32); - txtCompletion.font = "VCR OSD Mono"; + var clearBoxSprite:FlxSprite = new FlxSprite(1165, 65).loadGraphic(Paths.image('freeplay/clearBox')); + add(clearBoxSprite); + + txtCompletion = new AtlasText(1185, 87, "69", AtlasFont.FREEPLAY_CLEAR); txtCompletion.visible = false; add(txtCompletion); @@ -674,9 +681,32 @@ class FreeplayState extends MusicBeatSubState lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2); lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9); + if (Math.isNaN(lerpScore)) + { + lerpScore = intendedScore; + } + + if (Math.isNaN(lerpCompletion)) + { + lerpCompletion = intendedCompletion; + } + fp.updateScore(Std.int(lerpScore)); - txtCompletion.text = Math.floor(lerpCompletion * 100) + "%"; + txtCompletion.text = '${Math.floor(lerpCompletion * 100)}'; + + // Right align the completion percentage + switch (txtCompletion.text.length) + { + case 3: + txtCompletion.x = 1185 - 10; + case 2: + txtCompletion.x = 1185; + case 1: + txtCompletion.x = 1185 + 24; + default: + txtCompletion.x = 1185; + } handleInputs(elapsed); } @@ -913,6 +943,11 @@ class FreeplayState extends MusicBeatSubState intendedCompletion = 0.0; } + if (intendedCompletion == Math.POSITIVE_INFINITY || intendedCompletion == Math.NEGATIVE_INFINITY || Math.isNaN(intendedCompletion)) + { + intendedCompletion = 0; + } + grpDifficulties.group.forEach(function(diffSprite) { diffSprite.visible = false; }); @@ -938,6 +973,27 @@ class FreeplayState extends MusicBeatSubState } } } + + if (change != 0) + { + // Update the song capsules to reflect the new difficulty info. + for (songCapsule in grpCapsules.members) + { + if (songCapsule == null) continue; + if (songCapsule.songData != null) + { + songCapsule.songData.currentDifficulty = currentDifficulty; + songCapsule.init(null, null, songCapsule.songData); + } + else + { + songCapsule.init(null, null, null); + } + } + } + + // Set the difficulty star count on the right. + difficultyStars.difficulty = daSong.songRating; } // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String) @@ -1046,6 +1102,10 @@ class FreeplayState extends MusicBeatSubState { currentDifficulty = rememberedDifficulty; } + + // Set the difficulty star count on the right. + var daSong = songs[curSelected]; + difficultyStars.difficulty = daSong?.songRating ?? 0; } function changeSelection(change:Int = 0) @@ -1176,19 +1236,47 @@ class FreeplaySongData { public var isFav:Bool = false; - public var songId:String = ""; - public var songName:String = ""; - public var levelId:String = ""; - public var songCharacter:String = ""; - public var songDifficulties:Array = []; + var song:Song; - public function new(songId:String, songName:String, levelId:String, songCharacter:String, songDifficulties:Array) + public var levelId(default, null):String = ""; + public var songId(default, null):String = ""; + + public var songDifficulties(default, null):Array = []; + + public var songName(default, null):String = ""; + public var songCharacter(default, null):String = ""; + public var songRating(default, null):Int = 0; + + public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; + + function set_currentDifficulty(value:String):String + { + if (currentDifficulty == value) return value; + + currentDifficulty = value; + updateValues(); + return value; + } + + public function new(levelId:String, songId:String, song:Song) { - this.songId = songId; - this.songName = songName; this.levelId = levelId; - this.songCharacter = songCharacter; - this.songDifficulties = songDifficulties; + this.songId = songId; + this.song = song; + + updateValues(); + } + + function updateValues():Void + { + this.songDifficulties = song.listDifficulties(); + if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY; + + var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty); + if (songDifficulty == null) return; + this.songName = songDifficulty.songName; + this.songCharacter = songDifficulty.characters.opponent; + this.songRating = songDifficulty.difficultyRating; } } diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 833187acb..88b6aef3c 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -35,11 +35,6 @@ class SongMenuItem extends FlxSpriteGroup var ranks:Array = ["fail", "average", "great", "excellent", "perfect"]; - // lol... - var diffRanks:Array = [ - "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "14", "15" - ]; - public var targetPos:FlxPoint = new FlxPoint(); public var doLerp:Bool = false; public var doJumpIn:Bool = false; @@ -47,10 +42,12 @@ class SongMenuItem extends FlxSpriteGroup public var doJumpOut:Bool = false; public var onConfirm:Void->Void; - public var diffGrayscale:Grayscale; + public var grayscaleShader:Grayscale; public var hsvShader(default, set):HSVShader; + var diffRatingSprite:FlxSprite; + public function new(x:Float, y:Float) { super(x, y); @@ -75,26 +72,30 @@ class SongMenuItem extends FlxSpriteGroup add(ranking); grpHide.add(ranking); - diffGrayscale = new Grayscale(1); - - var diffRank = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRankings/diff" + FlxG.random.getObject(diffRanks))); - diffRank.shader = diffGrayscale; - diffRank.visible = false; - add(diffRank); - diffRank.origin.set(capsule.origin.x - diffRank.x, capsule.origin.y - diffRank.y); - grpHide.add(diffRank); - switch (rank) { case "perfect": ranking.x -= 10; } + grayscaleShader = new Grayscale(1); + + diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRatings/diff00")); + diffRatingSprite.shader = grayscaleShader; + diffRatingSprite.visible = false; + add(diffRatingSprite); + diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y); + grpHide.add(diffRatingSprite); + songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled)); add(songText); grpHide.add(songText); + // TODO: Use value from metadata instead of random. + updateDifficultyRating(FlxG.random.int(0, 15)); + pixelIcon = new FlxSprite(160, 35); + pixelIcon.makeGraphic(32, 32, 0x00000000); pixelIcon.antialiasing = false; pixelIcon.active = false; @@ -113,6 +114,12 @@ class SongMenuItem extends FlxSpriteGroup setVisibleGrp(false); } + function updateDifficultyRating(newRating:Int) + { + var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating'; + diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}')); + } + function set_hsvShader(value:HSVShader):HSVShader { this.hsvShader = value; @@ -149,16 +156,17 @@ class SongMenuItem extends FlxSpriteGroup updateSelected(); } - public function init(x:Float, y:Float, songData:Null) + public function init(?x:Float, ?y:Float, songData:Null) { - this.x = x; - this.y = y; + if (x != null) this.x = x; + if (y != null) this.y = y; this.songData = songData; // Update capsule text. songText.text = songData?.songName ?? 'Random'; // Update capsule character. if (songData?.songCharacter != null) setCharacter(songData.songCharacter); + updateDifficultyRating(songData?.songRating ?? 0); // Update opacity, offsets, etc. updateSelected(); } @@ -336,7 +344,7 @@ class SongMenuItem extends FlxSpriteGroup function updateSelected():Void { - diffGrayscale.setAmount(this.selected ? 0 : 0.8); + grayscaleShader.setAmount(this.selected ? 0 : 0.8); songText.alpha = this.selected ? 1 : 0.6; songText.blurredText.visible = this.selected ? true : false; capsule.offset.x = this.selected ? 0 : -5; From 315aeea998eb8945755f2d795bf10f1c5e3ab9ee Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 11 Jan 2024 00:31:18 -0500 Subject: [PATCH 11/60] Update assets submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index b282f3431..a92dc15de 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit b282f3431c15b719222196813da98ab70839d3e5 +Subproject commit a92dc15de3a755d1ec7ec092dd057b2ff3dea0b4 From c05131e6a9192835e6e879bd6f2a42a6e1df1ad2 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 11 Jan 2024 00:52:42 -0500 Subject: [PATCH 12/60] Additional freeplay cleanup --- source/funkin/ui/freeplay/FreeplayState.hx | 8 +++++--- source/funkin/ui/freeplay/SongMenuItem.hx | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index cfe7f802a..dec199e98 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -382,16 +382,16 @@ class FreeplayState extends MusicBeatSubState add(overhangStuff); FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut}); - var fnfFreeplay:FlxText = new FlxText(4, 10, 0, "FREEPLAY", 48); + var fnfFreeplay:FlxText = new FlxText(8, 8, 0, "FREEPLAY", 48); fnfFreeplay.font = "VCR OSD Mono"; fnfFreeplay.visible = false; - ostName = new FlxText(4, 10, FlxG.width - 4 - 4, "OFFICIAL OST", 48); + ostName = new FlxText(8, 8, FlxG.width - 8 - 8, "OFFICIAL OST", 48); ostName.font = "VCR OSD Mono"; ostName.alignment = RIGHT; ostName.visible = false; - exitMovers.set([overhangStuff, fnfFreeplay], + exitMovers.set([overhangStuff, fnfFreeplay, ostName], { y: -overhangStuff.height, x: 0, @@ -402,6 +402,7 @@ class FreeplayState extends MusicBeatSubState var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2); fnfFreeplay.shader = sillyStroke; add(fnfFreeplay); + add(ostName); var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70); fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore'); @@ -492,6 +493,7 @@ class FreeplayState extends MusicBeatSubState new FlxTimer().start(1 / 24, function(handShit) { fnfHighscoreSpr.visible = true; fnfFreeplay.visible = true; + ostName.visible = true; fp.visible = true; fp.updateScore(0); diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 88b6aef3c..06d113468 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -208,7 +208,14 @@ class SongMenuItem extends FlxSpriteGroup pixelIcon.loadGraphic(Paths.image(charPath)); pixelIcon.scale.x = pixelIcon.scale.y = 2; - pixelIcon.origin.x = 100; + + switch (char) + { + case "parents-christmas": + pixelIcon.origin.x = 140; + default: + pixelIcon.origin.x = 100; + } // pixelIcon.origin.x = capsule.origin.x; // pixelIcon.offset.x -= pixelIcon.origin.x; } From d19924ae4148d07b59a6b30284b9ac963d95fb98 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 12 Jan 2024 04:18:04 -0500 Subject: [PATCH 13/60] assets submod update --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index b282f3431..a3e2277e6 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit b282f3431c15b719222196813da98ab70839d3e5 +Subproject commit a3e2277e6f12208f9a976b80883db67c54a2a897 From 025fd326bd5eb3378e390eb682875bd319891fc3 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 12 Jan 2024 06:13:34 -0500 Subject: [PATCH 14/60] Click and drag on a sustain to edit it. --- assets | 2 +- hmm.json | 4 +- source/funkin/data/song/SongData.hx | 36 +++- source/funkin/play/notes/SustainTrail.hx | 32 +++- .../ui/debug/charting/ChartEditorState.hx | 162 ++++++++++++++++-- .../commands/ExtendNoteLengthCommand.hx | 34 +++- .../components/ChartEditorHoldNoteSprite.hx | 37 +++- .../ChartEditorSelectionSquareSprite.hx | 21 ++- .../ChartEditorHoldNoteContextMenu.hx | 43 +++++ .../ChartEditorNoteContextMenu.hx | 5 + .../handlers/ChartEditorContextMenuHandler.hx | 18 ++ .../handlers/ChartEditorThemeHandler.hx | 2 +- 12 files changed, 358 insertions(+), 38 deletions(-) create mode 100644 source/funkin/ui/debug/charting/contextmenus/ChartEditorHoldNoteContextMenu.hx diff --git a/assets b/assets index b282f3431..7e31e86db 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit b282f3431c15b719222196813da98ab70839d3e5 +Subproject commit 7e31e86dbeec3df5076895dedc62a45cc14d66e1 diff --git a/hmm.json b/hmm.json index d461edd24..8d05a7a2e 100644 --- a/hmm.json +++ b/hmm.json @@ -54,14 +54,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "5086e59e7551d775ed4d1fb0188e31de22d1312b", + "ref": "2561076c5abeee0a60f3a2a65a8ecb7832a6a62a", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "2b9cff727999b53ed292b1675ac1c9089ac77600", + "ref": "9c8ab039524086f5a8c8f35b9fb14538b5bfba5d", "url": "https://github.com/haxeui/haxeui-flixel" }, { diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 1a726254f..708881429 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -826,7 +826,13 @@ class SongNoteDataRaw implements ICloneable @:alias("l") @:default(0) @:optional - public var length:Float; + public var length(default, set):Float; + + function set_length(value:Float):Float + { + _stepLength = null; + return length = value; + } /** * The kind of the note. @@ -883,6 +889,11 @@ class SongNoteDataRaw implements ICloneable return _stepTime = Conductor.instance.getTimeInSteps(this.time); } + /** + * The length of the note, if applicable, in steps. + * Calculated from the length and the BPM. + * Cached for performance. Set to `null` to recalculate. + */ @:jignored var _stepLength:Null = null; @@ -907,9 +918,14 @@ class SongNoteDataRaw implements ICloneable } else { - var lengthMs:Float = Conductor.instance.getStepTimeInMs(value) - this.time; + var endStep:Float = getStepTime() + value; + var endMs:Float = Conductor.instance.getStepTimeInMs(endStep); + var lengthMs:Float = endMs - this.time; + this.length = lengthMs; } + + // Recalculate the step length next time it's requested. _stepLength = null; } @@ -980,6 +996,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw @:op(A == B) public function op_equals(other:SongNoteData):Bool { + // Handle the case where one value is null. + if (this == null) return other == null; + if (other == null) return false; + if (this.kind == '') { if (other.kind != '' && other.kind != 'normal') return false; @@ -995,6 +1015,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw @:op(A != B) public function op_notEquals(other:SongNoteData):Bool { + // Handle the case where one value is null. + if (this == null) return other == null; + if (other == null) return false; + if (this.kind == '') { if (other.kind != '' && other.kind != 'normal') return true; @@ -1010,24 +1034,32 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw @:op(A > B) public function op_greaterThan(other:SongNoteData):Bool { + if (other == null) return false; + return this.time > other.time; } @:op(A < B) public function op_lessThan(other:SongNoteData):Bool { + if (other == null) return false; + return this.time < other.time; } @:op(A >= B) public function op_greaterThanOrEquals(other:SongNoteData):Bool { + if (other == null) return false; + return this.time >= other.time; } @:op(A <= B) public function op_lessThanOrEquals(other:SongNoteData):Bool { + if (other == null) return false; + return this.time <= other.time; } diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index 7367b97af..4902afd49 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -82,6 +82,9 @@ class SustainTrail extends FlxSprite public var isPixel:Bool; + var graphicWidth:Float = 0; + var graphicHeight:Float = 0; + /** * Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?) * @param NoteData @@ -110,8 +113,8 @@ class SustainTrail extends FlxSprite zoom *= 0.7; // CALCULATE SIZE - width = graphic.width / 8 * zoom; // amount of notes * 2 - height = sustainHeight(sustainLength, getScrollSpeed()); + graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2 + graphicHeight = sustainHeight(sustainLength, getScrollSpeed()); // instead of scrollSpeed, PlayState.SONG.speed flipY = Preferences.downscroll; @@ -148,12 +151,21 @@ class SustainTrail extends FlxSprite if (sustainLength == s) return s; - height = sustainHeight(s, getScrollSpeed()); + graphicHeight = sustainHeight(s, getScrollSpeed()); this.sustainLength = s; updateClipping(); + updateHitbox(); return this.sustainLength; } + public override function updateHitbox():Void + { + width = graphicWidth; + height = graphicHeight; + offset.set(0, 0); + origin.set(width * 0.5, height * 0.5); + } + /** * Sets up new vertex and UV data to clip the trail. * If flipY is true, top and bottom bounds swap places. @@ -161,7 +173,7 @@ class SustainTrail extends FlxSprite */ public function updateClipping(songTime:Float = 0):Void { - var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height); + var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight); if (clipHeight <= 0.1) { visible = false; @@ -178,10 +190,10 @@ class SustainTrail extends FlxSprite // ===HOLD VERTICES== // Top left vertices[0 * 2] = 0.0; // Inline with left side - vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight; + vertices[0 * 2 + 1] = flipY ? clipHeight : graphicHeight - clipHeight; // Top right - vertices[1 * 2] = width; + vertices[1 * 2] = graphicWidth; vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex // Bottom left @@ -197,7 +209,7 @@ class SustainTrail extends FlxSprite } // Bottom right - vertices[3 * 2] = width; + vertices[3 * 2] = graphicWidth; vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex // ===HOLD UVs=== @@ -233,7 +245,7 @@ class SustainTrail extends FlxSprite // Bottom left vertices[6 * 2] = vertices[2 * 2]; // Inline with left side - vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (height + graphic.height * (bottomClip - endOffset) * zoom); + vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (graphicHeight + graphic.height * (bottomClip - endOffset) * zoom); // Bottom right vertices[7 * 2] = vertices[3 * 2]; // Inline with right side @@ -277,6 +289,10 @@ class SustainTrail extends FlxSprite getScreenPosition(_point, camera).subtractPoint(offset); camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing); } + + #if FLX_DEBUG + if (FlxG.debugger.drawDebug) drawDebug(); + #end } public override function kill():Void diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 1773a84fe..33bba450f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -718,7 +718,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState * `null` if the user isn't currently placing a note. * As the user drags, we will update this note's sustain length, and finalize the note when they release. */ - var currentPlaceNoteData:Null = null; + var currentPlaceNoteData(default, set):Null = null; + + function set_currentPlaceNoteData(value:Null):Null + { + noteDisplayDirty = true; + + return currentPlaceNoteData = value; + } // Note Movement @@ -2270,7 +2277,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState bounds.height = MIN_HEIGHT; } - trace('Note preview viewport bounds: ' + bounds.toString()); + // trace('Note preview viewport bounds: ' + bounds.toString()); return bounds; } @@ -3047,8 +3054,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (holdNoteSprite == null || holdNoteSprite.noteData == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; - if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + if (holdNoteSprite.noteData == currentPlaceNoteData) { + // This hold note is for the note we are currently dragging. + // It will be displayed by gridGhostHoldNoteSprite instead. + holdNoteSprite.kill(); + } + else if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + { + // This hold note is off-screen. + // Kill the hold note sprite and recycle it. holdNoteSprite.kill(); } else if (!currentSongChartNoteData.fastContains(holdNoteSprite.noteData) || holdNoteSprite.noteData.length == 0) @@ -3066,7 +3081,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { displayedHoldNoteData.push(holdNoteSprite.noteData); - // Update the event sprite's position. + // Update the event sprite's height and position. + // var holdNoteHeight = holdNoteSprite.noteData.getStepLength() * GRID_SIZE; + // holdNoteSprite.setHeightDirectly(holdNoteHeight); holdNoteSprite.updateHoldNotePosition(renderedNotes); } } @@ -3144,7 +3161,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState noteSprite.updateNotePosition(renderedNotes); // Add hold notes that are now visible (and not already displayed). - if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1) + if (noteSprite.noteData != null + && noteSprite.noteData.length > 0 + && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1 + && noteSprite.noteData != currentPlaceNoteData) { var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this)); // trace('Creating new HoldNote... (${renderedHoldNotes.members.length})'); @@ -3157,6 +3177,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState holdNoteSprite.setHeightDirectly(noteLengthPixels); holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + + trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height); } } @@ -3195,6 +3217,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Is the note a hold note? if (noteData == null || noteData.length <= 0) continue; + // Is the note the one we are dragging? If so, ghostHoldNoteSprite will handle it. + if (noteData == currentPlaceNoteData) continue; + // Is the hold note rendered already? if (displayedHoldNoteData.indexOf(noteData) != -1) continue; @@ -3284,7 +3309,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState selectionSquare.x = noteSprite.x; selectionSquare.y = noteSprite.y; selectionSquare.width = GRID_SIZE; - selectionSquare.height = GRID_SIZE; + + var stepLength = noteSprite.noteData.getStepLength(); + selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE); } } @@ -3563,6 +3590,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite); + var overlapsRenderedNotes:Bool = FlxG.mouse.overlaps(renderedNotes); + var overlapsRenderedHoldNotes:Bool = FlxG.mouse.overlaps(renderedHoldNotes); + var overlapsRenderedEvents:Bool = FlxG.mouse.overlaps(renderedEvents); + // Cursor position relative to the grid. var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x; var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y; @@ -3804,12 +3835,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return event.alive && FlxG.mouse.overlaps(event); }); } + var highlightedHoldNote:Null = null; + if (highlightedNote == null && highlightedEvent == null) + { + highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool { + return holdNote.alive && FlxG.mouse.overlaps(holdNote); + }); + } if (FlxG.keys.pressed.CONTROL) { if (highlightedNote != null && highlightedNote.noteData != null) { - // TODO: Handle the case of clicking on a sustain piece. // Control click to select/deselect an individual note. if (isNoteSelected(highlightedNote.noteData)) { @@ -3832,6 +3869,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new SelectItemsCommand([], [highlightedEvent.eventData])); } } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + // Control click to select/deselect an individual note. + if (isNoteSelected(highlightedNote.noteData)) + { + performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], [])); + } + else + { + performCommand(new SelectItemsCommand([highlightedHoldNote.noteData], [])); + } + } else { // Do nothing if you control-clicked on an empty space. @@ -3849,6 +3898,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Click an event to select it. performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + // Click a hold note to select it. + performCommand(new SetItemSelectionCommand([highlightedHoldNote.noteData], [], currentNoteSelection, currentEventSelection)); + } else { // Click on an empty space to deselect everything. @@ -4001,7 +4055,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var dragLengthMs:Float = dragLengthSteps * Conductor.instance.stepLengthMs; var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE; - if (gridGhostNote != null && gridGhostNote.noteData != null && gridGhostHoldNote != null) + if (gridGhostHoldNote != null) { if (dragLengthSteps > 0) { @@ -4014,8 +4068,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } gridGhostHoldNote.visible = true; - gridGhostHoldNote.noteData = gridGhostNote.noteData; - gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection(); + gridGhostHoldNote.noteData = currentPlaceNoteData; + gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); @@ -4036,6 +4090,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Apply the new length. performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs)); } + else + { + // Apply the new (zero) length if we are changing the length. + if (currentPlaceNoteData.length > 0) + { + this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI')); + performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, 0)); + } + } // Finished dragging. Release the note. currentPlaceNoteData = null; @@ -4068,6 +4131,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return event.alive && FlxG.mouse.overlaps(event); }); } + var highlightedHoldNote:Null = null; + if (highlightedNote == null && highlightedEvent == null) + { + highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool { + // If holdNote.alive is false, the holdNote is dead and awaiting recycling. + return holdNote.alive && FlxG.mouse.overlaps(holdNote); + }); + } if (FlxG.keys.pressed.CONTROL) { @@ -4094,6 +4165,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new SelectItemsCommand([], [highlightedEvent.eventData])); } } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + if (isNoteSelected(highlightedNote.noteData)) + { + performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], [])); + } + else + { + performCommand(new SelectItemsCommand([highlightedHoldNote.noteData], [])); + } + } else { // Do nothing when control clicking nothing. @@ -4127,6 +4209,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); } } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + // Clicked a hold note, start dragging TO EXTEND NOTE LENGTH. + currentPlaceNoteData = highlightedHoldNote.noteData; + } else { // Click a blank space to place a note and select it. @@ -4176,6 +4263,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return event.alive && FlxG.mouse.overlaps(event); }); } + var highlightedHoldNote:Null = null; + if (highlightedNote == null && highlightedEvent == null) + { + highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool { + // If holdNote.alive is false, the holdNote is dead and awaiting recycling. + return holdNote.alive && FlxG.mouse.overlaps(holdNote); + }); + } if (highlightedNote != null && highlightedNote.noteData != null) { @@ -4227,13 +4322,40 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new RemoveEventsCommand([highlightedEvent.eventData])); } } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + if (FlxG.keys.pressed.SHIFT) + { + // Shift + Right click opens the context menu. + // If we are clicking a large selection, open the Selection context menu, otherwise open the Note context menu. + var isHighlightedNoteSelected:Bool = isNoteSelected(highlightedHoldNote.noteData); + var useSingleNoteContextMenu:Bool = (!isHighlightedNoteSelected) + || (isHighlightedNoteSelected && currentNoteSelection.length == 1); + // Show the context menu connected to the note. + if (useSingleNoteContextMenu) + { + this.openHoldNoteContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedHoldNote.noteData); + } + else + { + this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY); + } + } + else + { + // Right click removes hold from the note. + this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI')); + performCommand(new ExtendNoteLengthCommand(highlightedHoldNote.noteData, 0)); + } + } else { // Right clicked on nothing. } } - var isOrWillSelect = overlapsSelection || dragTargetNote != null || dragTargetEvent != null; + var isOrWillSelect = overlapsSelection || dragTargetNote != null || dragTargetEvent != null || overlapsRenderedNotes || overlapsRenderedHoldNotes + || overlapsRenderedEvents; // Handle grid cursor. if (!isCursorOverHaxeUI && overlapsGrid && !isOrWillSelect && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed) { @@ -4324,6 +4446,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { targetCursorMode = Crosshair; } + else if (overlapsRenderedNotes) + { + targetCursorMode = Pointer; + } + else if (overlapsRenderedHoldNotes) + { + targetCursorMode = Pointer; + } + else if (overlapsRenderedEvents) + { + targetCursorMode = Pointer; + } else if (overlapsGrid) { targetCursorMode = Cell; @@ -5042,7 +5176,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()"; // FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap"); - var result = new ChartEditorSelectionSquareSprite(); + var result = new ChartEditorSelectionSquareSprite(this); result.loadGraphic(selectionSquareBitmap); return result; } @@ -5371,7 +5505,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * HAXEUI FUNCTIONS */ - // ==================== + // ================== /** * Set the currently selected item in the Difficulty tree view to the node representing the current difficulty. @@ -5462,7 +5596,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * STATIC FUNCTIONS */ - // ==================== + // ================== function handleNotePreview():Void { diff --git a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx index 47da0dde5..62ffe63b9 100644 --- a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx +++ b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx @@ -13,17 +13,25 @@ class ExtendNoteLengthCommand implements ChartEditorCommand var note:SongNoteData; var oldLength:Float; var newLength:Float; + var unit:Unit; - public function new(note:SongNoteData, newLength:Float) + public function new(note:SongNoteData, newLength:Float, unit:Unit = MILLISECONDS) { this.note = note; this.oldLength = note.length; this.newLength = newLength; + this.unit = unit; } public function execute(state:ChartEditorState):Void { - note.length = newLength; + switch (unit) + { + case MILLISECONDS: + this.note.length = newLength; + case STEPS: + this.note.setStepLength(newLength); + } state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -36,7 +44,8 @@ class ExtendNoteLengthCommand implements ChartEditorCommand { state.playSound(Paths.sound('chartingSounds/undo')); - note.length = oldLength; + // Always use milliseconds for undoing + this.note.length = oldLength; state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -47,6 +56,23 @@ class ExtendNoteLengthCommand implements ChartEditorCommand public function toString():String { - return 'Extend Note Length'; + if (oldLength == 0) + { + return 'Add Hold to Note'; + } + else if (newLength == 0) + { + return 'Remove Hold from Note'; + } + else + { + return 'Extend Hold Note Length'; + } } } + +enum Unit +{ + MILLISECONDS; + STEPS; +} diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index e5971db08..a7764907c 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -39,6 +39,17 @@ class ChartEditorHoldNoteSprite extends SustainTrail setup(); } + public override function updateHitbox():Void + { + // Expand the clickable hitbox to the full column width, then nudge to the left to re-center it. + width = ChartEditorState.GRID_SIZE; + height = graphicHeight; + + var xOffset = (ChartEditorState.GRID_SIZE - graphicWidth) / 2; + offset.set(-xOffset, 0); + origin.set(width * 0.5, height * 0.5); + } + /** * Set the height directly, to a value in pixels. * @param h The desired height in pixels. @@ -52,6 +63,23 @@ class ChartEditorHoldNoteSprite extends SustainTrail fullSustainLength = sustainLength; } + /** + * Call this to override how debug bounding boxes are drawn for this sprite. + */ + public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void + { + if (!camera.visible || !camera.exists || !isOnScreen(camera)) return; + + var rect = getBoundingBox(camera); + trace('hold note bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height); + + var gfx = beginDrawDebug(camera); + debugBoundingBoxColor = 0xffFF66FF; + gfx.lineStyle(2, color, 0.5); // thickness, color, alpha + gfx.drawRect(rect.x, rect.y, rect.width, rect.height); + endDrawDebug(camera); + } + function setup():Void { strumTime = 999999999; @@ -60,7 +88,9 @@ class ChartEditorHoldNoteSprite extends SustainTrail active = true; visible = true; alpha = 1.0; - width = graphic.width / 8 * zoom; // amount of notes * 2 + graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2 + + updateHitbox(); } public override function revive():Void @@ -154,7 +184,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail } this.x += ChartEditorState.GRID_SIZE / 2; - this.x -= this.width / 2; + this.x -= this.graphicWidth / 2; this.y += ChartEditorState.GRID_SIZE / 2; @@ -163,5 +193,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail this.x += origin.x; this.y += origin.y; } + + // Account for expanded clickable hitbox. + this.x += this.offset.x; } } diff --git a/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx index 8f7c4aaec..14266b71a 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx @@ -1,20 +1,33 @@ package funkin.ui.debug.charting.components; +import flixel.addons.display.FlxSliceSprite; import flixel.FlxSprite; -import funkin.data.song.SongData.SongNoteData; +import flixel.math.FlxRect; import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.ui.debug.charting.handlers.ChartEditorThemeHandler; /** * A sprite that can be used to display a square over a selected note or event in the chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ -class ChartEditorSelectionSquareSprite extends FlxSprite +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorSelectionSquareSprite extends FlxSliceSprite { public var noteData:Null; public var eventData:Null; - public function new() + public function new(chartEditorState:ChartEditorState) { - super(); + super(chartEditorState.selectionSquareBitmap, + new FlxRect(ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + + 4, ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + + 4, + ChartEditorState.GRID_SIZE + - (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8), + ChartEditorState.GRID_SIZE + - (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8)), + 32, 32); } } diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorHoldNoteContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorHoldNoteContextMenu.hx new file mode 100644 index 000000000..9f58d2f03 --- /dev/null +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorHoldNoteContextMenu.hx @@ -0,0 +1,43 @@ +package funkin.ui.debug.charting.contextmenus; + +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.menus.MenuItem; +import haxe.ui.core.Screen; +import funkin.data.song.SongData.SongNoteData; +import funkin.ui.debug.charting.commands.FlipNotesCommand; +import funkin.ui.debug.charting.commands.RemoveNotesCommand; +import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand; + +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/hold-note.xml")) +class ChartEditorHoldNoteContextMenu extends ChartEditorBaseContextMenu +{ + var contextmenuFlip:MenuItem; + var contextmenuDelete:MenuItem; + + var data:SongNoteData; + + public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData) + { + super(chartEditorState2, xPos2, yPos2); + this.data = data; + + initialize(); + } + + function initialize():Void + { + // NOTE: Remember to use commands here to ensure undo/redo works properly + contextmenuFlip.onClick = function(_) { + chartEditorState.performCommand(new FlipNotesCommand([data])); + } + + contextmenuRemoveHold.onClick = function(_) { + chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 0)); + } + + contextmenuDelete.onClick = function(_) { + chartEditorState.performCommand(new RemoveNotesCommand([data])); + } + } +} diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx index 4bfab27e8..66bf6f3ee 100644 --- a/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx @@ -6,6 +6,7 @@ import haxe.ui.core.Screen; import funkin.data.song.SongData.SongNoteData; import funkin.ui.debug.charting.commands.FlipNotesCommand; import funkin.ui.debug.charting.commands.RemoveNotesCommand; +import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand; @:access(funkin.ui.debug.charting.ChartEditorState) @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml")) @@ -31,6 +32,10 @@ class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu chartEditorState.performCommand(new FlipNotesCommand([data])); } + contextmenuAddHold.onClick = function(_) { + chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 4, STEPS)); + } + contextmenuDelete.onClick = function(_) { chartEditorState.performCommand(new RemoveNotesCommand([data])); } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx index b914f4149..c1eea5379 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx @@ -2,6 +2,7 @@ package funkin.ui.debug.charting.handlers; import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu; +import funkin.ui.debug.charting.contextmenus.ChartEditorHoldNoteContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu; import haxe.ui.containers.menus.Menu; @@ -23,16 +24,33 @@ class ChartEditorContextMenuHandler displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos)); } + /** + * Opened when shift+right-clicking a selection of multiple items. + */ public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float) { displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos)); } + /** + * Opened when shift+right-clicking a single note. + */ public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData) { displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data)); } + /** + * Opened when shift+right-clicking a single hold note. + */ + public static function openHoldNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData) + { + displayMenu(state, new ChartEditorHoldNoteContextMenu(state, xPos, yPos, data)); + } + + /** + * Opened when shift+right-clicking a single event. + */ public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData) { displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data)); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index d3aef4bfd..98bb5c2c8 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -52,7 +52,7 @@ class ChartEditorThemeHandler // Border on the square highlighting selected notes. static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933; static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933; - static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1; + public static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1; // Fill on the square highlighting selected notes. // Make sure this is transparent so you can see the notes underneath. From 1d1bab3b14933945876149fc509aa6950be2816c Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 12 Jan 2024 06:14:22 -0500 Subject: [PATCH 15/60] assets merging --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index a92dc15de..e56184c08 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit a92dc15de3a755d1ec7ec092dd057b2ff3dea0b4 +Subproject commit e56184c0851e822136e3d254a51c89d54022938d From 25c10d205dae3727218b8f2e62fac7f3c352ab30 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 12 Jan 2024 06:35:41 -0500 Subject: [PATCH 16/60] Some positioning fixes --- source/funkin/ui/debug/charting/ChartEditorState.hx | 8 ++++---- .../ui/debug/charting/handlers/ChartEditorAudioHandler.hx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 64a2f19ed..dda779917 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -213,7 +213,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * The X position of the note preview area. */ - public static final NOTE_PREVIEW_X_POS:Int = 350; + public static final NOTE_PREVIEW_X_POS:Int = 320; /** * The Y position of the note preview area. @@ -383,11 +383,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (audioVisGroup != null && audioVisGroup.playerVis != null) { - audioVisGroup.playerVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS); + audioVisGroup.playerVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS - GRID_TOP_PAD); } if (audioVisGroup != null && audioVisGroup.opponentVis != null) { - audioVisGroup.opponentVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS); + audioVisGroup.opponentVis.y = Math.max(gridTiledSprite.y, GRID_INITIAL_Y_POS - GRID_TOP_PAD); } } } @@ -4756,7 +4756,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Visibly center the Dad health icon. if (healthIconDad != null) { - var xOffset = 45 + (healthIconDad.width / 2); + var xOffset = 75 + (healthIconDad.width / 2); healthIconDad.x = (gridTiledSprite == null) ? (0) : (GRID_X_POS - xOffset); var yOffset = 30 - (healthIconDad.height / 2); healthIconDad.y = (gridTiledSprite == null) ? (0) : (GRID_INITIAL_Y_POS - NOTE_SELECT_BUTTON_HEIGHT) + yOffset; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 0edba7357..d7fbd42c2 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -188,19 +188,19 @@ class ChartEditorAudioHandler state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195; state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; state.audioVisGroup.playerVis.detail = 1; - state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS); + state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD); state.audioVocalTrackGroup.playerVoicesOffset = state.currentSongOffsets.getVocalOffset(charId); return true; case DAD: state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); state.audioVisGroup.addOpponentVis(vocalTrack); - state.audioVisGroup.opponentVis.x = 435; + state.audioVisGroup.opponentVis.x = 405; state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195; state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; state.audioVisGroup.opponentVis.detail = 1; - state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS); + state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD); state.audioVocalTrackGroup.opponentVoicesOffset = state.currentSongOffsets.getVocalOffset(charId); From 7f83daf23bf10098ebe47ffc7f73a6ef16bf885f Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 12 Jan 2024 08:04:28 -0500 Subject: [PATCH 17/60] null fixy --- assets | 2 +- source/funkin/data/song/SongData.hx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets b/assets index e56184c08..f1e42601b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e56184c0851e822136e3d254a51c89d54022938d +Subproject commit f1e42601b6ea2026c6e2f4627c5738bfb8b7b524 diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 1a726254f..270195200 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -93,7 +93,7 @@ class SongMetadata implements ICloneable result.version = this.version; result.timeFormat = this.timeFormat; result.divisions = this.divisions; - result.offsets = this.offsets.clone(); + result.offsets = this.offsets != null ? this.offsets.clone() : new SongOffsets(); // if no song offsets found (aka null), so just create new ones result.timeChanges = this.timeChanges.deepClone(); result.looped = this.looped; result.playData = this.playData.clone(); From 9fe0c68256a33a5045e0070c245e17fe01aa1bf9 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 12 Jan 2024 18:29:02 -0500 Subject: [PATCH 18/60] Add theme music toggle and extend fade in --- assets | 2 +- source/funkin/save/Save.hx | 28 +++++++++- .../ui/debug/charting/ChartEditorState.hx | 52 +++++++++++++++---- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/assets b/assets index f1e42601b..9d305889f 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit f1e42601b6ea2026c6e2f4627c5738bfb8b7b524 +Subproject commit 9d305889f2e310afeddcda6a4be775eb630fb9ac diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 810d0fd93..1fbcc6c20 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -14,8 +14,8 @@ import thx.semver.Version; @:forward(volume, mute) abstract Save(RawSaveData) { - // Version 2.0.1 adds attributes to `optionsChartEditor`, that should return default values if they are null. - public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.1"; + // Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null. + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. @@ -108,6 +108,7 @@ abstract Save(RawSaveData) metronomeVolume: 1.0, hitsoundsEnabledPlayer: true, hitsoundsEnabledOpponent: true, + themeMusic: true, instVolume: 1.0, voicesVolume: 1.0, playbackSpeed: 1.0, @@ -347,6 +348,23 @@ abstract Save(RawSaveData) return this.optionsChartEditor.hitsoundsEnabledOpponent; } + public var chartEditorThemeMusic(get, set):Bool; + + function get_chartEditorThemeMusic():Bool + { + if (this.optionsChartEditor.themeMusic == null) this.optionsChartEditor.themeMusic = true; + + return this.optionsChartEditor.themeMusic; + } + + function set_chartEditorThemeMusic(value:Bool):Bool + { + // Set and apply. + this.optionsChartEditor.themeMusic = value; + flush(); + return this.optionsChartEditor.themeMusic; + } + public var chartEditorInstVolume(get, set):Float; function get_chartEditorInstVolume():Float @@ -1027,6 +1045,12 @@ typedef SaveDataChartEditorOptions = */ var ?hitsoundsEnabledOpponent:Bool; + /** + * Theme music in the Chart Editor. + * @default `true` + */ + var ?themeMusic:Bool; + /** * Instrumental volume in the Chart Editor. * @default `1.0` diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 452e966d9..0853bf390 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -272,6 +272,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ public static final BASE_QUANT_INDEX:Int = 3; + /** + * The duration before the welcome music starts to fade back in after the user stops playing music in the chart editor. + */ + public static final WELCOME_MUSIC_FADE_IN_DELAY:Float = 30.0; + + /** + * The duration of the welcome music fade in. + */ + public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0; + /** * INSTANCE DATA */ @@ -1636,6 +1646,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var menubarItemOpponentHitsounds:MenuCheckBox; + /** + * The `Audio -> Play Theme Music` menu checkbox. + */ + var menubarItemThemeMusic:MenuCheckBox; + /** * The `Audio -> Hitsound Volume` label. */ @@ -1921,6 +1936,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Set the z-index of the HaxeUI. this.root.zIndex = 100; + // Get rid of any music from the previous state. + if (FlxG.sound.music != null) FlxG.sound.music.stop(); + + // Play the welcome music. + setupWelcomeMusic(); + // Show the mouse cursor. Cursor.show(); @@ -1928,12 +1949,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState fixCamera(); - // Get rid of any music from the previous state. - if (FlxG.sound.music != null) FlxG.sound.music.stop(); - - // Play the welcome music. - setupWelcomeMusic(); - buildDefaultSongData(); buildBackground(); @@ -2027,6 +2042,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState hitsoundVolume = save.chartEditorHitsoundVolume; hitsoundsEnabledPlayer = save.chartEditorHitsoundsEnabledPlayer; hitsoundsEnabledOpponent = save.chartEditorHitsoundsEnabledOpponent; + this.welcomeMusic.active = save.chartEditorThemeMusic; // audioInstTrack.volume = save.chartEditorInstVolume; // audioInstTrack.pitch = save.chartEditorPlaybackSpeed; @@ -2056,6 +2072,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState save.chartEditorHitsoundVolume = hitsoundVolume; save.chartEditorHitsoundsEnabledPlayer = hitsoundsEnabledPlayer; save.chartEditorHitsoundsEnabledOpponent = hitsoundsEnabledOpponent; + save.chartEditorThemeMusic = this.welcomeMusic.active; // save.chartEditorInstVolume = audioInstTrack.volume; // save.chartEditorVoicesVolume = audioVocalTrackGroup.volume; @@ -2116,10 +2133,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function fadeInWelcomeMusic(?extraWait:Float = 0, ?fadeInTime:Float = 5):Void { + if (!this.welcomeMusic.active) + { + stopWelcomeMusic(); + return; + } + bgMusicTimer = new FlxTimer().start(extraWait, (_) -> { this.welcomeMusic.volume = 0; - this.welcomeMusic.play(); - this.welcomeMusic.fadeIn(fadeInTime, 0, 1.0); + if (this.welcomeMusic.active) + { + this.welcomeMusic.play(); + this.welcomeMusic.fadeIn(fadeInTime, 0, 1.0); + } }); } @@ -2750,6 +2776,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemOpponentHitsounds.onChange = event -> hitsoundsEnabledOpponent = event.value; menubarItemOpponentHitsounds.selected = hitsoundsEnabledOpponent; + menubarItemThemeMusic.onChange = event -> { + this.welcomeMusic.active = event.value; + fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION); + }; + menubarItemThemeMusic.selected = this.welcomeMusic.active; + menubarItemVolumeHitsound.onChange = event -> { var volume:Float = event.value.toFloat() / 100.0; hitsoundVolume = volume; @@ -5619,7 +5651,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState moveSongToScrollPosition(); - fadeInWelcomeMusic(7, 10); + fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION); // Reapply the volume. var instTargetVolume:Float = menubarItemVolumeInstrumental.value ?? 1.0; @@ -5855,7 +5887,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // Pause stopAudioPlayback(); - fadeInWelcomeMusic(7, 10); + fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION); } else { From 75dfed8229312185eb3d648dd2e4b8d41134344a Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 12 Jan 2024 19:50:13 -0500 Subject: [PATCH 19/60] -release fix --- .../ui/debug/charting/components/ChartEditorHoldNoteSprite.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index a7764907c..c7f7747c0 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -63,6 +63,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail fullSustainLength = sustainLength; } + #if FLX_DEBUG /** * Call this to override how debug bounding boxes are drawn for this sprite. */ @@ -79,6 +80,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail gfx.drawRect(rect.x, rect.y, rect.width, rect.height); endDrawDebug(camera); } + #end function setup():Void { From 549d46117254b3c53d4f0798c6a4d7c0e501aafd Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sun, 14 Jan 2024 06:46:58 -0500 Subject: [PATCH 20/60] Fix an issue where Song offsets would be null rather than zero. --- source/funkin/data/song/SongData.hx | 5 +++-- source/funkin/data/song/SongRegistry.hx | 9 +++++---- source/funkin/play/song/Song.hx | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 07ea38741..703ab406c 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -38,10 +38,11 @@ class SongMetadata implements ICloneable public var looped:Bool; /** - * Instrumental and vocal offsets. Optional, defaults to 0. + * Instrumental and vocal offsets. + * Defaults to an empty SongOffsets object. */ @:optional - public var offsets:SongOffsets; + public var offsets:Null; /** * Data relating to the song's gameplay. diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 5a0835f57..98b46c782 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -12,6 +12,7 @@ import funkin.util.VersionUtil; using funkin.data.song.migrator.SongDataMigrator; +@:nullSafety class SongRegistry extends BaseRegistry { /** @@ -31,7 +32,7 @@ class SongRegistry extends BaseRegistry public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; - public static var DEFAULT_GENERATEDBY(get, null):String; + public static var DEFAULT_GENERATEDBY(get, never):String; static function get_DEFAULT_GENERATEDBY():String { @@ -88,7 +89,7 @@ class SongRegistry extends BaseRegistry { try { - var entry:Song = createEntry(entryId); + var entry:Null = createEntry(entryId); if (entry != null) { trace(' Loaded entry data: ${entry}'); @@ -455,7 +456,7 @@ class SongRegistry extends BaseRegistry { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryStr:Null = loadEntryMetadataFile(id, variation)?.contents; - var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr); + var entryVersion:Null = VersionUtil.getVersionFromJSON(entryStr); return entryVersion; } @@ -463,7 +464,7 @@ class SongRegistry extends BaseRegistry { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryStr:Null = loadEntryChartFile(id, variation)?.contents; - var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr); + var entryVersion:Null = VersionUtil.getVersionFromJSON(entryStr); return entryVersion; } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index cde068f42..089eb6c92 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -174,7 +174,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry Date: Sun, 14 Jan 2024 06:47:17 -0500 Subject: [PATCH 21/60] Fix an issue with parsing enums in HScript. --- hmm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hmm.json b/hmm.json index 67ba4d18d..b5db4b717 100644 --- a/hmm.json +++ b/hmm.json @@ -149,7 +149,7 @@ "name": "polymod", "type": "git", "dir": null, - "ref": "80d1d309803c1b111866524f9769325e3b8b0b1b", + "ref": "cb11a95d0159271eb3587428cf4b9602e46dc469", "url": "https://github.com/larsiusprime/polymod" }, { From bf1107134924c6aaaf1b1ae1e69f394afed227c4 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sun, 14 Jan 2024 08:48:50 -0500 Subject: [PATCH 22/60] null check on daSong for difficultyStars --- source/funkin/ui/freeplay/FreeplayState.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index dec199e98..de6484fd3 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -995,7 +995,7 @@ class FreeplayState extends MusicBeatSubState } // Set the difficulty star count on the right. - difficultyStars.difficulty = daSong.songRating; + difficultyStars.difficulty = daSong?.songRating ?? difficultyStars.difficulty; // yay haxe 4.3 } // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String) From 0f03021c209b9945c1046d4330d86a6c091de6a0 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sun, 14 Jan 2024 08:52:43 -0500 Subject: [PATCH 23/60] tooltip fix --- hmm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hmm.json b/hmm.json index b5db4b717..9a74ae54a 100644 --- a/hmm.json +++ b/hmm.json @@ -61,7 +61,7 @@ "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "9c8ab039524086f5a8c8f35b9fb14538b5bfba5d", + "ref": "4f1842e55a410014dd4a188b576b31019631493a", "url": "https://github.com/haxeui/haxeui-flixel" }, { From d53dbd67e3a943242317657e4af4c5c7ca52b35f Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sun, 14 Jan 2024 09:18:32 -0500 Subject: [PATCH 24/60] Rewrite to character animation, boyfriend death animation should be fixed --- assets | 2 +- source/funkin/audio/FunkinSound.hx | 20 +++ source/funkin/audio/SoundGroup.hx | 6 + .../play/character/MultiSparrowCharacter.hx | 149 ++++-------------- 4 files changed, 54 insertions(+), 123 deletions(-) diff --git a/assets b/assets index 9d305889f..d440b5937 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9d305889f2e310afeddcda6a4be775eb630fb9ac +Subproject commit d440b59376760ade87b922874f83dba8297ef22b diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 40293b0ce..278578fb3 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -107,6 +107,26 @@ class FunkinSound extends FlxSound return this; } + /** + * Called when the user clicks to focus on the window. + */ + override function onFocus():Void + { + if (!_alreadyPaused && this._shouldPlay) + { + resume(); + } + } + + /** + * Called when the user tabs away from the window. + */ + override function onFocusLost():Void + { + _alreadyPaused = _paused; + pause(); + } + public override function resume():FunkinSound { if (this._time < 0) diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index 15c2296ca..528aaa80c 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -132,6 +132,12 @@ class SoundGroup extends FlxTypedGroup }); } + public override function destroy() + { + stop(); + super.destroy(); + } + /** * Remove all sounds from the group. */ diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx index 0fc07399c..48c5afb58 100644 --- a/source/funkin/play/character/MultiSparrowCharacter.hx +++ b/source/funkin/play/character/MultiSparrowCharacter.hx @@ -1,5 +1,6 @@ package funkin.play.character; +import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxFramesCollection; import funkin.modding.events.ScriptEvent; import funkin.util.assets.FlxAnimationUtil; @@ -7,35 +8,17 @@ import funkin.play.character.CharacterData.CharacterRenderType; /** * For some characters which use Sparrow atlases, the spritesheets need to be split - * into multiple files. This character renderer handles by showing the appropriate sprite. + * into multiple files. This character renderer concatenates these together into a single sprite. * * Examples in base game include BF Holding GF (most of the sprites are in one file * but the death animation is in a separate file). * Only example I can think of in mods is Tricky (which has a separate file for each animation). * - * BaseCharacter has game logic, SparrowCharacter has only rendering logic. + * BaseCharacter has game logic, MultiSparrowCharacter has only rendering logic. * KEEP THEM SEPARATE! - * - * TODO: Rewrite this to use a single frame collection. - * @see https://github.com/HaxeFlixel/flixel/issues/2587#issuecomment-1179620637 */ class MultiSparrowCharacter extends BaseCharacter { - /** - * The actual group which holds all spritesheets this character uses. - */ - var members:Map = new Map(); - - /** - * A map between animation names and what frame collection the animation should use. - */ - var animAssetPath:Map = new Map(); - - /** - * The current frame collection being used. - */ - var activeMember:String; - public function new(id:String) { super(id, CharacterRenderType.MultiSparrow); @@ -51,7 +34,7 @@ class MultiSparrowCharacter extends BaseCharacter function buildSprites():Void { - buildSpritesheets(); + buildSpritesheet(); buildAnimations(); if (_data.isPixel) @@ -66,95 +49,49 @@ class MultiSparrowCharacter extends BaseCharacter } } - function buildSpritesheets():Void + function buildSpritesheet():Void { - // TODO: This currently works by creating like 5 frame collections and switching between them. - // It would be better to refactor this to simply concatenate the frame collections together. - - // Build the list of asset paths to use. - // Ignore nulls and duplicates. - var assetList = [_data.assetPath]; + var assetList = []; for (anim in _data.animations) { if (anim.assetPath != null && !assetList.contains(anim.assetPath)) { assetList.push(anim.assetPath); } - animAssetPath.set(anim.name, anim.assetPath); } - // Load the Sparrow atlas for each path and store them in the members map. + var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath, 'shared'); + + if (texture == null) + { + trace('Multi-Sparrow atlas could not load PRIMARY texture: ${_data.assetPath}'); + } + else + { + trace('Creating multi-sparrow atlas: ${_data.assetPath}'); + texture.parent.destroyOnNoUse = false; + } + for (asset in assetList) { - var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared'); + var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset, 'shared'); // If we don't do this, the unused textures will be removed as soon as they're loaded. - if (texture == null) + if (subTexture == null) { - trace('Multi-Sparrow atlas could not load texture: ${asset}'); + trace('Multi-Sparrow atlas could not load subtexture: ${asset}'); } else { - trace('Adding multi-sparrow atlas: ${asset}'); - texture.parent.destroyOnNoUse = false; - members.set(asset, texture); + trace('Concatenating multi-sparrow atlas: ${asset}'); + subTexture.parent.destroyOnNoUse = false; } + + texture.addAtlas(subTexture); } - // Use the default frame collection to start. - loadFramesByAssetPath(_data.assetPath); - } - - /** - * Replace this sprite's animation frames with the ones at this asset path. - */ - function loadFramesByAssetPath(assetPath:String):Void - { - if (_data.assetPath == null) - { - trace('[ERROR] Multi-Sparrow character has no default asset path!'); - return; - } - if (assetPath == null) - { - // trace('Asset path is null, falling back to default. This is normal!'); - loadFramesByAssetPath(_data.assetPath); - return; - } - - if (this.activeMember == assetPath) - { - // trace('Already using this asset path: ${assetPath}'); - return; - } - - if (members.exists(assetPath)) - { - // Switch to a new set of sprites. - // trace('Loading frames from asset path: ${assetPath}'); - this.frames = members.get(assetPath); - this.activeMember = assetPath; - this.setScale(_data.scale); - } - else - { - trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}'); - } - } - - /** - * Replace this sprite's animation frames with the ones needed to play this animation. - */ - function loadFramesByAnimName(animName) - { - if (animAssetPath.exists(animName)) - { - loadFramesByAssetPath(animAssetPath.get(animName)); - } - else - { - trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}'); - } + this.frames = texture; + this.setScale(_data.scale); } function buildAnimations() @@ -164,7 +101,6 @@ class MultiSparrowCharacter extends BaseCharacter // We need to swap to the proper frame collection before adding the animations, I think? for (anim in _data.animations) { - loadFramesByAnimName(anim.name); FlxAnimationUtil.addAtlasAnimation(this, anim); if (anim.offsets == null) @@ -187,37 +123,6 @@ class MultiSparrowCharacter extends BaseCharacter // unless we're forcing a new animation. if (!this.canPlayOtherAnims && !ignoreOther) return; - loadFramesByAnimName(name); super.playAnimation(name, restart, ignoreOther, reverse); } - - override function set_frames(value:FlxFramesCollection):FlxFramesCollection - { - // DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK - // WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM - // if (animation != null) - // { - // animation.destroyAnimations(); - // } - - if (value != null) - { - graphic = value.parent; - this.frames = value; - this.frame = value.getByIndex(0); - // this.numFrames = value.numFrames; - resetHelpers(); - this.bakedRotationAngle = 0; - this.animation.frameIndex = 0; - graphicLoaded(); - } - else - { - this.frames = null; - this.frame = null; - this.graphic = null; - } - - return this.frames; - } } From 3c4f5d596b96be71397e281d9b7193acb8b9f330 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sun, 14 Jan 2024 12:07:50 -0500 Subject: [PATCH 25/60] assets subjod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index d440b5937..9b9baa6f2 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d440b59376760ade87b922874f83dba8297ef22b +Subproject commit 9b9baa6f2b38dc9bd3354b350084f548e1e16c0f From 313fd55e985f1b3211d51581f41b03e05e9148cc Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 15 Jan 2024 14:20:44 -0500 Subject: [PATCH 26/60] WIP --- source/funkin/play/PlayState.hx | 6 +++++- source/funkin/play/notes/Strumline.hx | 2 +- source/funkin/play/notes/SustainTrail.hx | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 9f98d3b04..c244fd60f 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1796,6 +1796,7 @@ class PlayState extends MusicBeatSubState { if (note == null) continue; + // TODO: Does this properly account for offsets? var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS; var hitWindowCenter = note.strumTime; var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; @@ -1865,7 +1866,10 @@ class PlayState extends MusicBeatSubState } // TODO: Potential penalty for dropping a hold note? - // if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; } + if (holdNote.missedNote && !holdNote.handledMiss) + { + holdNote.handledMiss = true; + } } // Process notes on the player's side. diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index b312494cf..d56bb33ca 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -343,7 +343,7 @@ class Strumline extends FlxSpriteGroup playStatic(holdNote.noteDirection); holdNote.missedNote = true; holdNote.visible = true; - holdNote.alpha = 0.0; + holdNote.alpha = 0.0; // Completely hide the dropped hold note. } } diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index 4902afd49..f5d444f61 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -47,6 +47,11 @@ class SustainTrail extends FlxSprite */ public var missedNote:Bool = false; + /** + * Set to `true` after handling additional logic for missing notes. + */ + public var handledMiss:Bool = false; + // maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support! /** From 6f0f5ea6102d4646e0321d2f9927355151c623ad Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 15 Jan 2024 15:33:28 -0500 Subject: [PATCH 27/60] Fix a crash when closing chart editor with no music playing --- source/funkin/ui/debug/charting/ChartEditorState.hx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 0853bf390..e98809ce8 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -5951,9 +5951,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState ChartEditorNoteSprite.noteFrameCollection = null; // Stop the music. - welcomeMusic.destroy(); - audioInstTrack.destroy(); - audioVocalTrackGroup.destroy(); + if (welcomeMusic != null) welcomeMusic.destroy(); + if (audioInstTrack != null) audioInstTrack.destroy(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.destroy(); } function applyCanQuickSave():Void From 3366240eb5e02b17195dbeec86df328cd7048e6c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 15 Jan 2024 18:21:49 -0500 Subject: [PATCH 28/60] Fix "Edit Event" menu item when right clicking chart events --- hmm.json | 4 ++-- .../charting/contextmenus/ChartEditorEventContextMenu.hx | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/hmm.json b/hmm.json index 9a74ae54a..d93ea0658 100644 --- a/hmm.json +++ b/hmm.json @@ -54,14 +54,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "2561076c5abeee0a60f3a2a65a8ecb7832a6a62a", + "ref": "bb904f8b4b205755a310c23ff25219f9dcd62711", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "4f1842e55a410014dd4a188b576b31019631493a", + "ref": "1ec470c297afd7758a90dc9399aa1e3a4ea6ca0b", "url": "https://github.com/haxeui/haxeui-flixel" }, { diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx index a79125b21..d848f1435 100644 --- a/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx @@ -25,6 +25,10 @@ class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu function initialize() { + contextmenuEdit.onClick = function(_) { + chartEditorState.showToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT); + } + contextmenuDelete.onClick = function(_) { chartEditorState.performCommand(new RemoveEventsCommand([data])); } From e391c02015221908062401574c5f5e912ecfa4df Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 15 Jan 2024 22:10:42 -0500 Subject: [PATCH 29/60] Polish note display visuals, and scrap Killer judgement. --- assets | 2 +- source/funkin/Highscore.hx | 2 - source/funkin/play/PlayState.hx | 83 +++++++++++++------ source/funkin/play/notes/NoteSprite.hx | 46 ++++++++++ source/funkin/play/notes/Strumline.hx | 23 +++-- source/funkin/play/notes/SustainTrail.hx | 1 + source/funkin/play/scoring/Scoring.hx | 4 +- source/funkin/save/Save.hx | 1 - .../funkin/save/migrator/SaveDataMigrator.hx | 6 -- source/funkin/util/Constants.hx | 6 ++ 10 files changed, 131 insertions(+), 43 deletions(-) diff --git a/assets b/assets index 9b9baa6f2..d094640f7 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9b9baa6f2b38dc9bd3354b350084f548e1e16c0f +Subproject commit d094640f727a670a348b3579d11af5ff6a2ada3a diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx index 2c18ffa2d..24b65832b 100644 --- a/source/funkin/Highscore.hx +++ b/source/funkin/Highscore.hx @@ -21,7 +21,6 @@ abstract Tallies(RawTallies) bad: 0, good: 0, sick: 0, - killer: 0, totalNotes: 0, totalNotesHit: 0, maxCombo: 0, @@ -43,7 +42,6 @@ typedef RawTallies = var bad:Int; var good:Int; var sick:Int; - var killer:Int; var maxCombo:Int; var isNewHighscore:Bool; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index c244fd60f..afb5400a3 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1865,17 +1865,30 @@ class PlayState extends MusicBeatSubState } } - // TODO: Potential penalty for dropping a hold note? if (holdNote.missedNote && !holdNote.handledMiss) { + // When the opponent drops a hold note. holdNote.handledMiss = true; + + // We dropped a hold note. + // Mute vocals and play miss animation, but don't penalize. + vocals.opponentVolume = 0; + currentStage.getOpponent().playSingAnimation(holdNote.noteData.getDirection(), true); } } // Process notes on the player's side. for (note in playerStrumline.notes.members) { - if (note == null || note.hasBeenHit) continue; + if (note == null) continue; + + if (note.hasBeenHit) + { + note.tooEarly = false; + note.mayHit = false; + note.hasMissed = false; + continue; + } var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS; var hitWindowCenter = note.strumTime; @@ -1938,8 +1951,15 @@ class PlayState extends MusicBeatSubState songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed); } - // TODO: Potential penalty for dropping a hold note? - // if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; } + if (holdNote.missedNote && !holdNote.handledMiss) + { + // The player dropped a hold note. + holdNote.handledMiss = true; + + // Mute vocals and play miss animation, but don't penalize. + vocals.playerVolume = 0; + currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true); + } } } @@ -2031,8 +2051,6 @@ class PlayState extends MusicBeatSubState trace('Hit note! ${targetNote.noteData}'); goodNoteHit(targetNote, input); - targetNote.visible = false; - targetNote.kill(); notesInDirection.remove(targetNote); // Play the strumline animation. @@ -2064,15 +2082,8 @@ class PlayState extends MusicBeatSubState // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) return; - Highscore.tallies.combo++; - Highscore.tallies.totalNotesHit++; - - if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; - popUpScore(note, input); - playerStrumline.hitNote(note); - if (note.isHoldNote && note.holdNoteSprite != null) { playerStrumline.playNoteHoldCover(note.holdNoteSprite); @@ -2088,8 +2099,6 @@ class PlayState extends MusicBeatSubState function onNoteMiss(note:NoteSprite):Void { // a MISS is when you let a note scroll past you!! - Highscore.tallies.missed++; - var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2137,8 +2146,11 @@ class PlayState extends MusicBeatSubState } vocals.playerVolume = 0; + Highscore.tallies.missed++; + if (Highscore.tallies.combo != 0) { + // Break the combo. Highscore.tallies.combo = comboPopUps.displayCombo(0); } @@ -2286,29 +2298,54 @@ class PlayState extends MusicBeatSubState var score = Scoring.scoreNote(noteDiff, PBOT1); var daRating = Scoring.judgeNote(noteDiff, PBOT1); + if (daRating == 'miss') + { + // If daRating is 'miss', that means we made a mistake and should not continue. + trace('[WARNING] popUpScore judged a note as a miss!'); + // TODO: Remove this. + comboPopUps.displayRating('miss'); + return; + } + + // TODO: DEBUG OVERRIDE DON'T BE A DUMBASS + daRating = 'bad'; + + var isComboBreak = false; switch (daRating) { - case 'killer': - Highscore.tallies.killer += 1; - health += Constants.HEALTH_KILLER_BONUS; case 'sick': Highscore.tallies.sick += 1; health += Constants.HEALTH_SICK_BONUS; + isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; case 'good': Highscore.tallies.good += 1; health += Constants.HEALTH_GOOD_BONUS; + isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; case 'bad': Highscore.tallies.bad += 1; health += Constants.HEALTH_BAD_BONUS; + isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; case 'shit': Highscore.tallies.shit += 1; health += Constants.HEALTH_SHIT_BONUS; - case 'miss': - Highscore.tallies.missed += 1; - health -= Constants.HEALTH_MISS_PENALTY; + isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; } - if (daRating == "sick" || daRating == "killer") + if (isComboBreak) + { + // Break the combo, but don't increment tallies.misses. + Highscore.tallies.combo = comboPopUps.displayCombo(0); + } + else + { + Highscore.tallies.combo++; + Highscore.tallies.totalNotesHit++; + if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; + } + + playerStrumline.hitNote(daNote, !isComboBreak); + + if (daRating == "sick") { playerStrumline.playNoteSplash(daNote.noteData.getDirection()); } @@ -2444,7 +2481,6 @@ class PlayState extends MusicBeatSubState score: songScore, tallies: { - killer: Highscore.tallies.killer, sick: Highscore.tallies.sick, good: Highscore.tallies.good, bad: Highscore.tallies.bad, @@ -2495,7 +2531,6 @@ class PlayState extends MusicBeatSubState tallies: { // TODO: Sum up the values for the whole level! - killer: 0, sick: 0, good: 0, bad: 0, diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index e5c4786d3..0368b18e9 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -4,6 +4,7 @@ import funkin.data.song.SongData.SongNoteData; import funkin.play.notes.notestyle.NoteStyle; import flixel.graphics.frames.FlxAtlasFrames; import flixel.FlxSprite; +import funkin.graphics.shaders.HSVShader; class NoteSprite extends FlxSprite { @@ -11,6 +12,8 @@ class NoteSprite extends FlxSprite public var holdNoteSprite:SustainTrail; + var hsvShader:HSVShader; + /** * The time at which the note should be hit, in milliseconds. */ @@ -102,6 +105,8 @@ class NoteSprite extends FlxSprite this.strumTime = strumTime; this.direction = direction; + this.hsvShader = new HSVShader(); + if (this.strumTime < 0) this.strumTime = 0; setupNoteGraphic(noteStyle); @@ -116,16 +121,57 @@ class NoteSprite extends FlxSprite setGraphicSize(Strumline.STRUMLINE_SIZE); updateHitbox(); + + this.shader = hsvShader; + } + + #if FLX_DEBUG + /** + * Call this to override how debug bounding boxes are drawn for this sprite. + */ + public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void + { + if (!camera.visible || !camera.exists || !isOnScreen(camera)) return; + + var gfx = beginDrawDebug(camera); + + var rect = getBoundingBox(camera); + trace('note sprite bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height); + + gfx.lineStyle(2, 0xFFFF66FF, 0.5); // thickness, color, alpha + gfx.drawRect(rect.x, rect.y, rect.width, rect.height); + + gfx.lineStyle(2, 0xFFFFFF66, 0.5); // thickness, color, alpha + gfx.drawRect(rect.x, rect.y + rect.height / 2, rect.width, 1); + + endDrawDebug(camera); + } + #end + + public function desaturate():Void + { + this.hsvShader.saturation = 0.2; + } + + public function setHue(hue:Float):Void + { + this.hsvShader.hue = hue; } public override function revive():Void { super.revive(); + this.visible = true; + this.alpha = 1.0; this.active = false; this.tooEarly = false; this.hasBeenHit = false; this.mayHit = false; this.hasMissed = false; + + this.hsvShader.hue = 1.0; + this.hsvShader.saturation = 1.0; + this.hsvShader.value = 1.0; } public override function kill():Void diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index d56bb33ca..5fdd3945f 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -316,7 +316,7 @@ class Strumline extends FlxSpriteGroup // Update rendering of notes. for (note in notes.members) { - if (note == null || !note.alive || note.hasBeenHit) continue; + if (note == null || !note.alive) continue; var vwoosh:Bool = note.holdNoteSprite == null; // Set the note's position. @@ -384,10 +384,6 @@ class Strumline extends FlxSpriteGroup var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Constants.PIXELS_PER_MS; - trace('yOffset: ' + yOffset); - trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength); - trace('holdNote.sustainLength: ' + holdNote.sustainLength); - var vwoosh:Bool = false; if (Preferences.downscroll) @@ -531,11 +527,24 @@ class Strumline extends FlxSpriteGroup this.noteData.insertionSort(compareNoteData.bind(FlxSort.ASCENDING)); } - public function hitNote(note:NoteSprite):Void + /** + * @param note The note to hit. + * @param removeNote True to remove the note immediately, false to make it transparent and let it move offscreen. + */ + public function hitNote(note:NoteSprite, removeNote:Bool = true):Void { playConfirm(note.direction); note.hasBeenHit = true; - killNote(note); + + if (removeNote) + { + killNote(note); + } + else + { + note.alpha = 0.5; + note.desaturate(); + } if (note.holdNoteSprite != null) { diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index f5d444f61..056a6a5a9 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -326,6 +326,7 @@ class SustainTrail extends FlxSprite hitNote = false; missedNote = false; + handledMiss = false; } override public function destroy():Void diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx index 75d002cb5..edfb2cae7 100644 --- a/source/funkin/play/scoring/Scoring.hx +++ b/source/funkin/play/scoring/Scoring.hx @@ -158,8 +158,8 @@ class Scoring return switch (absTiming) { - case(_ < PBOT1_KILLER_THRESHOLD) => true: - 'killer'; + // case(_ < PBOT1_KILLER_THRESHOLD) => true: + // 'killer'; case(_ < PBOT1_SICK_THRESHOLD) => true: 'sick'; case(_ < PBOT1_GOOD_THRESHOLD) => true: diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 1fbcc6c20..c68caad5f 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -792,7 +792,6 @@ typedef SaveScoreData = typedef SaveScoreTallyData = { - var killer:Int; var sick:Int; var good:Int; var bad:Int; diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index d5b23cfd9..92bee4ceb 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -120,7 +120,6 @@ class SaveDataMigrator accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0, tallies: { - killer: 0, sick: 0, good: 0, bad: 0, @@ -140,7 +139,6 @@ class SaveDataMigrator accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0, tallies: { - killer: 0, sick: 0, good: 0, bad: 0, @@ -160,7 +158,6 @@ class SaveDataMigrator accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0, tallies: { - killer: 0, sick: 0, good: 0, bad: 0, @@ -183,7 +180,6 @@ class SaveDataMigrator accuracy: 0, tallies: { - killer: 0, sick: 0, good: 0, bad: 0, @@ -209,7 +205,6 @@ class SaveDataMigrator accuracy: 0, tallies: { - killer: 0, sick: 0, good: 0, bad: 0, @@ -235,7 +230,6 @@ class SaveDataMigrator accuracy: 0, tallies: { - killer: 0, sick: 0, good: 0, bad: 0, diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 197fa28e8..1005b312e 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -363,6 +363,12 @@ class Constants */ public static final SCORE_HOLD_BONUS_PER_SECOND:Float = 250.0; + public static final JUDGEMENT_KILLER_COMBO_BREAK:Bool = false; + public static final JUDGEMENT_SICK_COMBO_BREAK:Bool = false; + public static final JUDGEMENT_GOOD_COMBO_BREAK:Bool = false; + public static final JUDGEMENT_BAD_COMBO_BREAK:Bool = true; + public static final JUDGEMENT_SHIT_COMBO_BREAK:Bool = true; + /** * FILE EXTENSIONS */ From 3d2e50906fc689188e692b65016c289b828b547c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 15 Jan 2024 22:13:01 -0500 Subject: [PATCH 30/60] I was in fact a dumbass. --- source/funkin/play/PlayState.hx | 3 --- 1 file changed, 3 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index afb5400a3..f7fc6e0f4 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2307,9 +2307,6 @@ class PlayState extends MusicBeatSubState return; } - // TODO: DEBUG OVERRIDE DON'T BE A DUMBASS - daRating = 'bad'; - var isComboBreak = false; switch (daRating) { From b4edcc4b3c974d15c787c344c162dfb3d80baba4 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 15 Jan 2024 23:43:05 -0500 Subject: [PATCH 31/60] Reuse LevelProps (this fixes animations when switching weeks) --- source/funkin/ui/story/Level.hx | 23 ++++++--- source/funkin/ui/story/LevelProp.hx | 62 +++++++++++++++--------- source/funkin/ui/story/StoryMenuState.hx | 3 +- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index e86241277..1b9252fde 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -180,9 +180,9 @@ class Level implements IRegistryEntry return difficulties; } - public function buildProps():Array + public function buildProps(?existingProps:Array):Array { - var props:Array = []; + var props:Array = existingProps == null ? [] : [for (x in existingProps) x]; if (_data.props.length == 0) return props; @@ -190,11 +190,22 @@ class Level implements IRegistryEntry { var propData = _data.props[propIndex]; - var propSprite:Null = LevelProp.build(propData); - if (propSprite == null) continue; + // Attempt to reuse the `LevelProp` object. + // This prevents animations from resetting. + var existingProp:Null = props[propIndex]; + if (existingProp != null) + { + existingProp.propData = propData; + existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex; + } + else + { + var propSprite:Null = LevelProp.build(propData); + if (propSprite == null) continue; - propSprite.x += FlxG.width * 0.25 * propIndex; - props.push(propSprite); + propSprite.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex; + props.push(propSprite); + } } return props; diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx index 4dce7bfb3..5af383de9 100644 --- a/source/funkin/ui/story/LevelProp.hx +++ b/source/funkin/ui/story/LevelProp.hx @@ -6,9 +6,26 @@ import funkin.data.level.LevelData; class LevelProp extends Bopper { - public function new(danceEvery:Int) + public var propData(default, set):Null = null; + + function set_propData(value:LevelPropData):LevelPropData { - super(danceEvery); + // Only reset the prop if the asset path has changed. + if (propData == null || value.assetPath != this.propData.assetPath) + { + this.visible = (value != null); + this.propData = value; + danceEvery = this.propData.danceEvery; + applyData(); + } + + return this.propData; + } + + public function new(propData:LevelPropData) + { + super(propData.danceEvery); + this.propData = propData; } public function playConfirm():Void @@ -16,50 +33,51 @@ class LevelProp extends Bopper playAnimation('confirm', true, true); } - public static function build(propData:Null):Null + function applyData():Void { - if (propData == null) return null; - var isAnimated:Bool = propData.animations.length > 0; - var prop:LevelProp = new LevelProp(propData.danceEvery); - if (isAnimated) { // Initalize sprite frames. // Sparrow atlas only LEL. - prop.frames = Paths.getSparrowAtlas(propData.assetPath); + this.frames = Paths.getSparrowAtlas(propData.assetPath); } else { // Initalize static sprite. - prop.loadGraphic(Paths.image(propData.assetPath)); + this.loadGraphic(Paths.image(propData.assetPath)); // Disables calls to update() for a performance boost. - prop.active = false; + this.active = false; } - if (prop.frames == null || prop.frames.numFrames == 0) + if (this.frames == null || this.frames.numFrames == 0) { trace('ERROR: Could not build texture for level prop (${propData.assetPath}).'); - return null; + return; } var scale:Float = propData.scale * (propData.isPixel ? 6 : 1); - prop.scale.set(scale, scale); - prop.antialiasing = !propData.isPixel; - prop.alpha = propData.alpha; - prop.x = propData.offsets[0]; - prop.y = propData.offsets[1]; + this.scale.set(scale, scale); + this.antialiasing = !propData.isPixel; + this.alpha = propData.alpha; + this.x = propData.offsets[0]; + this.y = propData.offsets[1]; - FlxAnimationUtil.addAtlasAnimations(prop, propData.animations); + FlxAnimationUtil.addAtlasAnimations(this, propData.animations); for (propAnim in propData.animations) { - prop.setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]); + this.setAnimationOffsets(propAnim.name, propAnim.offsets[0], propAnim.offsets[1]); } - prop.dance(); - prop.animation.paused = true; + this.dance(); + this.animation.paused = true; + } - return prop; + public static function build(propData:Null):Null + { + if (propData == null) return null; + + return new LevelProp(propData); } } diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index d6dd536f7..356757599 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -636,8 +636,7 @@ class StoryMenuState extends MusicBeatState function updateProps():Void { - levelProps.clear(); - for (prop in currentLevel.buildProps()) + for (prop in currentLevel.buildProps(levelProps.members)) { prop.zIndex = 1000; levelProps.add(prop); From 5ac4b14891fa81c73bfb60bc4e67d16d999be038 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 15 Jan 2024 23:51:37 -0500 Subject: [PATCH 32/60] Fix week 7 death animations. --- source/funkin/play/GameOverSubState.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 137bf3905..732b22568 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -220,6 +220,7 @@ class GameOverSubState extends MusicBeatSubState playJeffQuote(); // Start music at lower volume startDeathMusic(0.2, false); + boyfriend.playAnimation('deathLoop' + animationSuffix); } default: // Start music at normal volume once the initial death animation finishes. From 673f7f57d456b1471fea39d79d0e4763e30901fc Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 16 Jan 2024 00:03:30 -0500 Subject: [PATCH 33/60] Fix pink rim on low zoom levels. --- source/funkin/play/GameOverSubState.hx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 732b22568..443f742fa 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -94,11 +94,12 @@ class GameOverSubState extends MusicBeatSubState // // Add a black background to the screen. - var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); + var bg = new FlxSprite().makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); // We make this transparent so that we can see the stage underneath during debugging, // but it's normally opaque. bg.alpha = transparent ? 0.25 : 1.0; bg.scrollFactor.set(); + bg.screenCenter(); add(bg); // Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState. From 80c7bcdfdfaeec6a37d426c2b8b6ddb2b52b8d04 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 16 Jan 2024 16:49:15 -0500 Subject: [PATCH 34/60] Rewrite Stage data handling to use the Registry pattern, and add support for solid colors. --- source/funkin/InitState.hx | 5 +- source/funkin/data/character/TODO.md | 0 source/funkin/data/conversation/TODO.md | 0 source/funkin/data/dialogue/TODO.md | 0 source/funkin/data/level/LevelRegistry.hx | 4 +- source/funkin/data/song/SongRegistry.hx | 16 +- source/funkin/data/speaker/TODO.md | 0 source/funkin/data/stage/StageData.hx | 199 +++++++ source/funkin/data/stage/StageRegistry.hx | 103 ++++ source/funkin/graphics/FunkinSprite.hx | 53 ++ source/funkin/modding/PolymodHandler.hx | 5 +- source/funkin/play/GameOverSubState.hx | 3 +- source/funkin/play/PlayState.hx | 4 +- .../play/character/AnimateAtlasCharacter.hx | 3 +- source/funkin/play/stage/Stage.hx | 93 +-- source/funkin/play/stage/StageData.hx | 548 ------------------ source/funkin/play/stage/StageProp.hx | 4 +- .../ui/debug/charting/ChartEditorState.hx | 11 +- .../handlers/ChartEditorDialogHandler.hx | 2 +- .../handlers/ChartEditorToolboxHandler.hx | 6 +- .../toolboxes/ChartEditorEventDataToolbox.hx | 2 +- .../toolboxes/ChartEditorMetadataToolbox.hx | 10 +- .../charting/util/ChartEditorDropdowns.hx | 11 +- .../ui/debug/stage/StageOffsetSubState.hx | 14 +- source/funkin/ui/options/ControlsMenu.hx | 5 +- source/funkin/ui/story/StoryMenuState.hx | 3 +- source/funkin/ui/title/OutdatedSubState.hx | 2 +- source/funkin/ui/title/TitleState.hx | 4 +- source/funkin/ui/transition/LoadingState.hx | 5 +- 29 files changed, 484 insertions(+), 631 deletions(-) create mode 100644 source/funkin/data/character/TODO.md create mode 100644 source/funkin/data/conversation/TODO.md create mode 100644 source/funkin/data/dialogue/TODO.md create mode 100644 source/funkin/data/speaker/TODO.md create mode 100644 source/funkin/data/stage/StageData.hx create mode 100644 source/funkin/data/stage/StageRegistry.hx create mode 100644 source/funkin/graphics/FunkinSprite.hx delete mode 100644 source/funkin/play/stage/StageData.hx diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 02b46c88c..c9198c3d4 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -20,11 +20,11 @@ import openfl.display.BitmapData; import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.event.SongEventRegistry; +import funkin.data.stage.StageRegistry; import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.play.cutscene.dialogue.SpeakerDataParser; import funkin.data.song.SongRegistry; -import funkin.play.stage.StageData.StageDataParser; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.modding.module.ModuleHandler; import funkin.ui.title.TitleState; @@ -217,8 +217,9 @@ class InitState extends FlxState ConversationDataParser.loadConversationCache(); DialogueBoxDataParser.loadDialogueBoxCache(); SpeakerDataParser.loadSpeakerCache(); - StageDataParser.loadStageCache(); + StageRegistry.instance.loadEntries(); CharacterDataParser.loadCharacterCache(); + ModuleHandler.buildModuleCallbacks(); ModuleHandler.loadModuleCache(); diff --git a/source/funkin/data/character/TODO.md b/source/funkin/data/character/TODO.md new file mode 100644 index 000000000..e69de29bb diff --git a/source/funkin/data/conversation/TODO.md b/source/funkin/data/conversation/TODO.md new file mode 100644 index 000000000..e69de29bb diff --git a/source/funkin/data/dialogue/TODO.md b/source/funkin/data/dialogue/TODO.md new file mode 100644 index 000000000..e69de29bb diff --git a/source/funkin/data/level/LevelRegistry.hx b/source/funkin/data/level/LevelRegistry.hx index b5c15de0f..96712cba5 100644 --- a/source/funkin/data/level/LevelRegistry.hx +++ b/source/funkin/data/level/LevelRegistry.hx @@ -7,9 +7,9 @@ import funkin.ui.story.ScriptedLevel; class LevelRegistry extends BaseRegistry { /** - * The current version string for the stage data format. + * The current version string for the level data format. * Handle breaking changes by incrementing this value - * and adding migration to the `migrateStageData()` function. + * and adding migration to the `migrateLevelData()` function. */ public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.0"; diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 98b46c782..b772349bc 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -127,7 +127,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { @@ -150,7 +150,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) @@ -210,7 +210,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { @@ -232,7 +232,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { @@ -252,7 +252,7 @@ class SongRegistry extends BaseRegistry function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null { var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) @@ -266,7 +266,7 @@ class SongRegistry extends BaseRegistry function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null { var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) @@ -347,7 +347,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; switch (loadEntryChartFile(id, variation)) { @@ -370,7 +370,7 @@ class SongRegistry extends BaseRegistry variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; + parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) diff --git a/source/funkin/data/speaker/TODO.md b/source/funkin/data/speaker/TODO.md new file mode 100644 index 000000000..e69de29bb diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx new file mode 100644 index 000000000..cb914007f --- /dev/null +++ b/source/funkin/data/stage/StageData.hx @@ -0,0 +1,199 @@ +package funkin.data.stage; + +import funkin.data.animation.AnimationData; + +@:nullSafety +class StageData +{ + /** + * The sematic version number of the stage data JSON format. + * Supports fancy comparisons like NPM does it's neat. + */ + @:default(funkin.data.stage.StageRegistry.STAGE_DATA_VERSION) + public var version:String; + + public var name:String = 'Unknown'; + public var props:Array = []; + public var characters:StageDataCharacters; + + @:default(1.0) + @:optional + public var cameraZoom:Null; + + public function new() + { + this.version = StageRegistry.STAGE_DATA_VERSION; + this.characters = makeDefaultCharacters(); + } + + function makeDefaultCharacters():StageDataCharacters + { + return { + bf: + { + zIndex: 0, + position: [0, 0], + cameraOffsets: [-100, -100] + }, + dad: + { + zIndex: 0, + position: [0, 0], + cameraOffsets: [100, -100] + }, + gf: + { + zIndex: 0, + position: [0, 0], + cameraOffsets: [0, 0] + } + }; + } + + /** + * Convert this StageData into a JSON string. + */ + public function serialize(pretty:Bool = true):String + { + var writer = new json2object.JsonWriter(); + return writer.write(this, pretty ? ' ' : null); + } +} + +typedef StageDataCharacters = +{ + var bf:StageDataCharacter; + var dad:StageDataCharacter; + var gf:StageDataCharacter; +}; + +typedef StageDataProp = +{ + /** + * The name of the prop for later lookup by scripts. + * Optional; if unspecified, the prop can't be referenced by scripts. + */ + @:optional + var name:String; + + /** + * The asset used to display the prop. + * NOTE: As of Stage data v1.0.1, you can also use a color here to create a rectangle, like "#ff0000". + * In this case, the `scale` property will be used to determine the size of the prop. + */ + var assetPath:String; + + /** + * The position of the prop as an [x, y] array of two floats. + */ + var position:Array; + + /** + * A number determining the stack order of the prop, relative to other props and the characters in the stage. + * Props with lower numbers render below those with higher numbers. + * This is just like CSS, it isn't hard. + * @default 0 + */ + @:optional + @:default(0) + var zIndex:Int; + + /** + * If set to true, anti-aliasing will be forcibly disabled on the sprite. + * This prevents blurry images on pixel-art levels. + * @default false + */ + @:optional + @:default(false) + var isPixel:Bool; + + /** + * Either the scale of the prop as a float, or the [w, h] scale as an array of two floats. + * Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory. + */ + @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) + @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) + @:optional + var scale:haxe.ds.Either>; + + /** + * The alpha of the prop, as a float. + * @default 1.0 + */ + @:optional + @:default(1.0) + var alpha:Float; + + /** + * If not zero, this prop will play an animation every X beats of the song. + * This requires animations to be defined. If `danceLeft` and `danceRight` are defined, + * they will alternated between, otherwise the `idle` animation will be used. + * + * @default 0 + */ + @:default(0) + @:optional + var danceEvery:Int; + + /** + * How much the prop scrolls relative to the camera. Used to create a parallax effect. + * Represented as an [x, y] array of two floats. + * [1, 1] means the prop moves 1:1 with the camera. + * [0.5, 0.5] means the prop half as much as the camera. + * [0, 0] means the prop is not moved. + * @default [0, 0] + */ + @:optional + @:default([0, 0]) + var scroll:Array; + + /** + * An optional array of animations which the prop can play. + * @default Prop has no animations. + */ + @:optional + @:default([]) + var animations:Array; + + /** + * If animations are used, this is the name of the animation to play first. + * @default Don't play an animation. + */ + @:optional + var startingAnimation:Null; + + /** + * The animation type to use. + * Options: "sparrow", "packer" + * @default "sparrow" + */ + @:default("sparrow") + @:optional + var animType:String; +}; + +typedef StageDataCharacter = +{ + /** + * A number determining the stack order of the character, relative to props and other characters in the stage. + * Again, just like CSS. + * @default 0 + */ + @:optional + @:default(0) + var zIndex:Int; + + /** + * The position to render the character at. + */ + @:optional + @:default([0, 0]) + var position:Array; + + /** + * The camera offsets to apply when focusing on the character on this stage. + * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF + */ + @:optional + var cameraOffsets:Array; +}; diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx new file mode 100644 index 000000000..b78292e5b --- /dev/null +++ b/source/funkin/data/stage/StageRegistry.hx @@ -0,0 +1,103 @@ +package funkin.data.stage; + +import funkin.data.stage.StageData; +import funkin.play.stage.Stage; +import funkin.play.stage.ScriptedStage; + +class StageRegistry extends BaseRegistry +{ + /** + * The current version string for the stage data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migrateStageData()` function. + */ + public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.1"; + + public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; + + public static final instance:StageRegistry = new StageRegistry(); + + public function new() + { + super('STAGE', 'stages', STAGE_DATA_VERSION_RULE); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + */ + public function parseEntryData(id:String):Null + { + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + + switch (loadEntryFile(id)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return parser.value; + } + + /** + * Parse and validate the JSON data and produce the corresponding data object. + * + * NOTE: Must be implemented on the implementation class. + * @param contents The JSON as a string. + * @param fileName An optional file name for error reporting. + */ + public function parseEntryDataRaw(contents:String, ?fileName:String):Null + { + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):Stage + { + return ScriptedStage.init(clsName, "unknown"); + } + + function getScriptedClassNames():Array + { + return ScriptedStage.listScriptClasses(); + } + + /** + * A list of all the stages from the base game, in order. + * TODO: Should this be hardcoded? + */ + public function listBaseGameStageIds():Array + { + return [ + "mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets", + "phillyBlazin", + ]; + } + + /** + * A list of all installed story weeks that are not from the base game. + */ + public function listModdedStageIds():Array + { + return listEntryIds().filter(function(id:String):Bool { + return listBaseGameStageIds().indexOf(id) == -1; + }); + } +} diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx new file mode 100644 index 000000000..487aaac34 --- /dev/null +++ b/source/funkin/graphics/FunkinSprite.hx @@ -0,0 +1,53 @@ +package funkin.graphics; + +import flixel.FlxSprite; +import flixel.util.FlxColor; +import flixel.graphics.FlxGraphic; + +/** + * An FlxSprite with additional functionality. + */ +class FunkinSprite extends FlxSprite +{ + /** + * @param x Starting X position + * @param y Starting Y position + */ + public function new(?x:Float = 0, ?y:Float = 0) + { + super(x, y); + } + + /** + * Acts similarly to `makeGraphic`, but with improved memory usage, + * at the expense of not being able to paint onto the sprite. + * + * @param width The target width of the sprite. + * @param height The target height of the sprite. + * @param color The color to fill the sprite with. + */ + public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite + { + var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}'); + frames = graphic.imageFrame; + scale.set(width / 2, height / 2); + updateHitbox(); + + return this; + } + + /** + * Ensure scale is applied when cloning a sprite. + * The default `clone()` method acts kinda weird TBH. + * @return A clone of this sprite. + */ + public override function clone():FunkinSprite + { + var result = new FunkinSprite(this.x, this.y); + result.frames = this.frames; + result.scale.set(this.scale.x, this.scale.y); + result.updateHitbox(); + + return result; + } +} diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index b7ef07be5..151e658b4 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -4,11 +4,12 @@ import funkin.util.macro.ClassMacro; import funkin.modding.module.ModuleHandler; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.data.song.SongData; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import polymod.Polymod; import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.format.ParseRules.TextFileFormat; import funkin.data.event.SongEventRegistry; +import funkin.data.stage.StageRegistry; import funkin.util.FileUtil; import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; @@ -275,7 +276,7 @@ class PolymodHandler ConversationDataParser.loadConversationCache(); DialogueBoxDataParser.loadDialogueBoxCache(); SpeakerDataParser.loadSpeakerCache(); - StageDataParser.loadStageCache(); + StageRegistry.instance.loadEntries(); CharacterDataParser.loadCharacterCache(); ModuleHandler.loadModuleCache(); } diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 137bf3905..cb190d23a 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -7,6 +7,7 @@ import flixel.sound.FlxSound; import funkin.ui.story.StoryMenuState; import flixel.util.FlxColor; import flixel.util.FlxTimer; +import funkin.graphics.FunkinSprite; import funkin.ui.MusicBeatSubState; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; @@ -94,7 +95,7 @@ class GameOverSubState extends MusicBeatSubState // // Add a black background to the screen. - var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); + var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); // We make this transparent so that we can see the stage underneath during debugging, // but it's normally opaque. bg.alpha = transparent ? 0.25 : 1.0; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 9f98d3b04..da525d4e0 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -50,11 +50,11 @@ import funkin.play.notes.SustainTrail; import funkin.play.scoring.Scoring; import funkin.play.song.Song; import funkin.data.song.SongRegistry; +import funkin.data.stage.StageRegistry; import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongCharacterData; import funkin.play.stage.Stage; -import funkin.play.stage.StageData.StageDataParser; import funkin.ui.transition.LoadingState; import funkin.play.components.PopUpStuff; import funkin.ui.options.PreferencesMenu; @@ -1353,7 +1353,7 @@ class PlayState extends MusicBeatSubState */ function loadStage(id:String):Void { - currentStage = StageDataParser.fetchStage(id); + currentStage = StageRegistry.instance.fetchEntry(id); if (currentStage != null) { diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index 3523ec994..f9dc18119 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -9,6 +9,7 @@ import flixel.math.FlxMath; import flixel.math.FlxPoint.FlxCallbackPoint; import flixel.math.FlxPoint; import flixel.math.FlxRect; +import funkin.graphics.FunkinSprite; import flixel.system.FlxAssets.FlxGraphicAsset; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; @@ -621,7 +622,7 @@ class AnimateAtlasCharacter extends BaseCharacter * This functionality isn't supported in SpriteGroup * @return this sprite group */ - public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite + public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite { #if FLX_DEBUG throw "This function is not supported in FlxSpriteGroup"; diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index c8cb8ce66..ac6c3705e 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -5,13 +5,16 @@ import flixel.group.FlxSpriteGroup; import flixel.math.FlxPoint; import flixel.system.FlxAssets.FlxShader; import flixel.util.FlxSort; +import flixel.util.FlxColor; import funkin.modding.IScriptedClass; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventType; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.BaseCharacter; -import funkin.play.stage.StageData.StageDataCharacter; -import funkin.play.stage.StageData.StageDataParser; +import funkin.data.IRegistryEntry; +import funkin.data.stage.StageData; +import funkin.data.stage.StageData.StageDataCharacter; +import funkin.data.stage.StageRegistry; import funkin.play.stage.StageProp; import funkin.util.SortUtil; import funkin.util.assets.FlxAnimationUtil; @@ -23,14 +26,25 @@ typedef StagePropGroup = FlxTypedSpriteGroup; * * A Stage is comprised of one or more props, each of which is a FlxSprite. */ -class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass +class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements IRegistryEntry { - public final stageId:String; - public final stageName:String; + public final id:String; - final _data:StageData; + public final _data:StageData; - public var camZoom:Float = 1.0; + public var stageName(get, never):String; + + function get_stageName():String + { + return _data?.name ?? 'Unknown'; + } + + public var camZoom(get, never):Float; + + function get_camZoom():Float + { + return _data?.cameraZoom ?? 1.0; + } var namedProps:Map = new Map(); var characters:Map = new Map(); @@ -41,21 +55,18 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass * They're used to cache the data needed to build the stage, * then accessed and fleshed out when the stage needs to be built. * - * @param stageId + * @param id */ - public function new(stageId:String) + public function new(id:String) { super(); - this.stageId = stageId; - _data = StageDataParser.parseStageData(this.stageId); + this.id = id; + _data = _fetchData(id); + if (_data == null) { - throw 'Could not find stage data for stageId: $stageId'; - } - else - { - this.stageName = _data.name; + throw 'Could not find stage data for stage id: $id'; } } @@ -129,9 +140,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass */ function buildStage():Void { - trace('Building stage for display: ${this.stageId}'); - - this.camZoom = _data.cameraZoom; + trace('Building stage for display: ${this.id}'); this.debugIconGroup = new FlxSpriteGroup(); @@ -139,6 +148,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass { trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})'); + var isSolidColor = dataProp.assetPath.startsWith('#'); var isAnimated = dataProp.animations.length > 0; var propSprite:StageProp; @@ -162,6 +172,22 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath); } } + else if (isSolidColor) + { + var width:Int = 1; + var height:Int = 1; + switch (dataProp.scale) + { + case Left(value): + width = Std.int(value); + height = Std.int(value); + + case Right(values): + width = Std.int(values[0]); + height = Std.int(values[1]); + } + propSprite.makeSolidColor(width, height, FlxColor.fromString(dataProp.assetPath)); + } else { // Initalize static sprite. @@ -177,13 +203,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass continue; } - switch (dataProp.scale) + if (!isSolidColor) { - case Left(value): - propSprite.scale.set(value); + switch (dataProp.scale) + { + case Left(value): + propSprite.scale.set(value); - case Right(values): - propSprite.scale.set(values[0], values[1]); + case Right(values): + propSprite.scale.set(values[0], values[1]); + } } propSprite.updateHitbox(); @@ -195,15 +224,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass // If pixel, disable antialiasing. propSprite.antialiasing = !dataProp.isPixel; - switch (dataProp.scroll) - { - case Left(value): - propSprite.scrollFactor.x = value; - propSprite.scrollFactor.y = value; - case Right(values): - propSprite.scrollFactor.x = values[0]; - propSprite.scrollFactor.y = values[1]; - } + propSprite.scrollFactor.x = dataProp.scroll[0]; + propSprite.scrollFactor.y = dataProp.scroll[1]; propSprite.zIndex = dataProp.zIndex; @@ -731,6 +753,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass return Sprite; } + static function _fetchData(id:String):Null + { + return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id)); + } + public function onScriptEvent(event:ScriptEvent) {} public function onPause(event:PauseScriptEvent) {} diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx deleted file mode 100644 index 2d87dec31..000000000 --- a/source/funkin/play/stage/StageData.hx +++ /dev/null @@ -1,548 +0,0 @@ -package funkin.play.stage; - -import funkin.data.animation.AnimationData; -import funkin.play.stage.ScriptedStage; -import funkin.play.stage.Stage; -import funkin.util.VersionUtil; -import funkin.util.assets.DataAssets; -import haxe.Json; -import openfl.Assets; - -/** - * Contains utilities for loading and parsing stage data. - */ -class StageDataParser -{ - /** - * The current version string for the stage data format. - * Handle breaking changes by incrementing this value - * and adding migration to the `migrateStageData()` function. - */ - public static final STAGE_DATA_VERSION:String = "1.0.0"; - - /** - * The current version rule check for the stage data format. - */ - public static final STAGE_DATA_VERSION_RULE:String = "1.0.x"; - - static final stageCache:Map = new Map(); - - static final DEFAULT_STAGE_ID = 'UNKNOWN'; - - /** - * Parses and preloads the game's stage data and scripts when the game starts. - * - * If you want to force stages to be reloaded, you can just call this function again. - */ - public static function loadStageCache():Void - { - // Clear any stages that are cached if there were any. - clearStageCache(); - trace("Loading stage cache..."); - - // - // SCRIPTED STAGES - // - var scriptedStageClassNames:Array = ScriptedStage.listScriptClasses(); - trace(' Instantiating ${scriptedStageClassNames.length} scripted stages...'); - for (stageCls in scriptedStageClassNames) - { - var stage:Stage = ScriptedStage.init(stageCls, DEFAULT_STAGE_ID); - if (stage != null) - { - trace(' Loaded scripted stage: ${stage.stageName}'); - // Disable the rendering logic for stage until it's loaded. - // Note that kill() =/= destroy() - stage.kill(); - - // Then store it. - stageCache.set(stage.stageId, stage); - } - else - { - trace(' Failed to instantiate scripted stage class: ${stageCls}'); - } - } - - // - // UNSCRIPTED STAGES - // - var stageIdList:Array = DataAssets.listDataFilesInPath('stages/'); - var unscriptedStageIds:Array = stageIdList.filter(function(stageId:String):Bool { - return !stageCache.exists(stageId); - }); - trace(' Instantiating ${unscriptedStageIds.length} non-scripted stages...'); - for (stageId in unscriptedStageIds) - { - var stage:Stage; - try - { - stage = new Stage(stageId); - if (stage != null) - { - trace(' Loaded stage data: ${stage.stageName}'); - stageCache.set(stageId, stage); - } - } - catch (e) - { - trace(' An error occurred while loading stage data: ${stageId}'); - // Assume error was already logged. - continue; - } - } - - trace(' Successfully loaded ${Lambda.count(stageCache)} stages.'); - } - - public static function fetchStage(stageId:String):Null - { - if (stageCache.exists(stageId)) - { - trace('Successfully fetch stage: ${stageId}'); - var stage:Stage = stageCache.get(stageId); - stage.revive(); - return stage; - } - else - { - trace('Failed to fetch stage, not found in cache: ${stageId}'); - return null; - } - } - - static function clearStageCache():Void - { - if (stageCache != null) - { - for (stage in stageCache) - { - stage.destroy(); - } - stageCache.clear(); - } - } - - /** - * Load a stage's JSON file, parse its data, and return it. - * - * @param stageId The stage to load. - * @return The stage data, or null if validation failed. - */ - public static function parseStageData(stageId:String):Null - { - var rawJson:String = loadStageFile(stageId); - - var stageData:StageData = migrateStageData(rawJson, stageId); - - return validateStageData(stageId, stageData); - } - - public static function listStageIds():Array - { - return stageCache.keys().array(); - } - - static function loadStageFile(stagePath:String):String - { - var stageFilePath:String = Paths.json('stages/${stagePath}'); - var rawJson = Assets.getText(stageFilePath).trim(); - - while (!rawJson.endsWith("}")) - { - rawJson = rawJson.substr(0, rawJson.length - 1); - } - - return rawJson; - } - - static function migrateStageData(rawJson:String, stageId:String):Null - { - // If you update the stage data format in a breaking way, - // handle migration here by checking the `version` value. - - try - { - var parser = new json2object.JsonParser(); - parser.ignoreUnknownVariables = false; - parser.fromJson(rawJson, '$stageId.json'); - - if (parser.errors.length > 0) - { - trace('[STAGE] Failed to parse stage data'); - - for (error in parser.errors) - funkin.data.DataError.printError(error); - - return null; - } - return parser.value; - } - catch (e) - { - trace(' Error parsing data for stage: ${stageId}'); - trace(' ${e}'); - return null; - } - } - - static final DEFAULT_ANIMTYPE:String = "sparrow"; - static final DEFAULT_CAMERAZOOM:Float = 1.0; - static final DEFAULT_DANCEEVERY:Int = 0; - static final DEFAULT_ISPIXEL:Bool = false; - static final DEFAULT_NAME:String = "Untitled Stage"; - static final DEFAULT_OFFSETS:Array = [0, 0]; - static final DEFAULT_CAMERA_OFFSETS_BF:Array = [-100, -100]; - static final DEFAULT_CAMERA_OFFSETS_DAD:Array = [150, -100]; - static final DEFAULT_POSITION:Array = [0, 0]; - static final DEFAULT_SCALE:Float = 1.0; - static final DEFAULT_ALPHA:Float = 1.0; - static final DEFAULT_SCROLL:Array = [0, 0]; - static final DEFAULT_ZINDEX:Int = 0; - - static final DEFAULT_CHARACTER_DATA:StageDataCharacter = - { - zIndex: DEFAULT_ZINDEX, - position: DEFAULT_POSITION, - cameraOffsets: DEFAULT_OFFSETS, - } - - /** - * Set unspecified parameters to their defaults. - * If the parameter is mandatory, print an error message. - * @param id - * @param input - * @return The validated stage data - */ - static function validateStageData(id:String, input:StageData):Null - { - if (input == null) - { - trace('ERROR: Could not parse stage data for "${id}".'); - return null; - } - - if (input.version == null) - { - trace('ERROR: Could not load stage data for "$id": missing version'); - return null; - } - - if (!VersionUtil.validateVersionStr(input.version, STAGE_DATA_VERSION_RULE)) - { - trace('ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})'); - return null; - } - - if (input.name == null) - { - trace('WARN: Stage data for "$id" missing name'); - input.name = DEFAULT_NAME; - } - - if (input.cameraZoom == null) - { - input.cameraZoom = DEFAULT_CAMERAZOOM; - } - - if (input.props == null) - { - input.props = []; - } - - for (inputProp in input.props) - { - // It's fine for inputProp.name to be null - - if (inputProp.assetPath == null) - { - trace('ERROR: Could not load stage data for "$id": missing assetPath for prop "${inputProp.name}"'); - return null; - } - - if (inputProp.position == null) - { - inputProp.position = DEFAULT_POSITION; - } - - if (inputProp.zIndex == null) - { - inputProp.zIndex = DEFAULT_ZINDEX; - } - - if (inputProp.isPixel == null) - { - inputProp.isPixel = DEFAULT_ISPIXEL; - } - - if (inputProp.danceEvery == null) - { - inputProp.danceEvery = DEFAULT_DANCEEVERY; - } - - if (inputProp.animType == null) - { - inputProp.animType = DEFAULT_ANIMTYPE; - } - - switch (inputProp.scale) - { - case null: - inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]); - case Left(value): - inputProp.scale = Right([value, value]); - case Right(_): - // Do nothing - } - - switch (inputProp.scroll) - { - case null: - inputProp.scroll = Right(DEFAULT_SCROLL); - case Left(value): - inputProp.scroll = Right([value, value]); - case Right(_): - // Do nothing - } - - if (inputProp.alpha == null) - { - inputProp.alpha = DEFAULT_ALPHA; - } - - if (inputProp.animations == null) - { - inputProp.animations = []; - } - - if (inputProp.animations.length == 0 && inputProp.startingAnimation != null) - { - trace('ERROR: Could not load stage data for "$id": missing animations for prop "${inputProp.name}"'); - return null; - } - - for (inputAnimation in inputProp.animations) - { - if (inputAnimation.name == null) - { - trace('ERROR: Could not load stage data for "$id": missing animation name for prop "${inputProp.name}"'); - return null; - } - - if (inputAnimation.frameRate == null) - { - inputAnimation.frameRate = 24; - } - - if (inputAnimation.offsets == null) - { - inputAnimation.offsets = DEFAULT_OFFSETS; - } - - if (inputAnimation.looped == null) - { - inputAnimation.looped = true; - } - - if (inputAnimation.flipX == null) - { - inputAnimation.flipX = false; - } - - if (inputAnimation.flipY == null) - { - inputAnimation.flipY = false; - } - } - } - - if (input.characters == null) - { - trace('ERROR: Could not load stage data for "$id": missing characters'); - return null; - } - - if (input.characters.bf == null) - { - input.characters.bf = DEFAULT_CHARACTER_DATA; - } - if (input.characters.dad == null) - { - input.characters.dad = DEFAULT_CHARACTER_DATA; - } - if (input.characters.gf == null) - { - input.characters.gf = DEFAULT_CHARACTER_DATA; - } - - for (inputCharacter in [input.characters.bf, input.characters.dad, input.characters.gf]) - { - if (inputCharacter.position == null || inputCharacter.position.length != 2) - { - inputCharacter.position = [0, 0]; - } - } - - // All good! - return input; - } -} - -class StageData -{ - /** - * The sematic version number of the stage data JSON format. - * Supports fancy comparisons like NPM does it's neat. - */ - public var version:String; - - public var name:String; - public var cameraZoom:Null; - public var props:Array; - public var characters:StageDataCharacters; - - public function new() - { - this.version = StageDataParser.STAGE_DATA_VERSION; - } - - /** - * Convert this StageData into a JSON string. - */ - public function serialize(pretty:Bool = true):String - { - var writer = new json2object.JsonWriter(); - return writer.write(this, pretty ? ' ' : null); - } -} - -typedef StageDataCharacters = -{ - var bf:StageDataCharacter; - var dad:StageDataCharacter; - var gf:StageDataCharacter; -}; - -typedef StageDataProp = -{ - /** - * The name of the prop for later lookup by scripts. - * Optional; if unspecified, the prop can't be referenced by scripts. - */ - @:optional - var name:String; - - /** - * The asset used to display the prop. - */ - var assetPath:String; - - /** - * The position of the prop as an [x, y] array of two floats. - */ - var position:Array; - - /** - * A number determining the stack order of the prop, relative to other props and the characters in the stage. - * Props with lower numbers render below those with higher numbers. - * This is just like CSS, it isn't hard. - * @default 0 - */ - @:optional - @:default(0) - var zIndex:Int; - - /** - * If set to true, anti-aliasing will be forcibly disabled on the sprite. - * This prevents blurry images on pixel-art levels. - * @default false - */ - @:optional - @:default(false) - var isPixel:Bool; - - /** - * Either the scale of the prop as a float, or the [w, h] scale as an array of two floats. - * Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory. - */ - @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) - @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) - @:optional - var scale:haxe.ds.Either>; - - /** - * The alpha of the prop, as a float. - * @default 1.0 - */ - @:optional - @:default(1.0) - var alpha:Float; - - /** - * If not zero, this prop will play an animation every X beats of the song. - * This requires animations to be defined. If `danceLeft` and `danceRight` are defined, - * they will alternated between, otherwise the `idle` animation will be used. - * - * @default 0 - */ - @:default(0) - @:optional - var danceEvery:Int; - - /** - * How much the prop scrolls relative to the camera. Used to create a parallax effect. - * Represented as a float or as an [x, y] array of two floats. - * [1, 1] means the prop moves 1:1 with the camera. - * [0.5, 0.5] means the prop half as much as the camera. - * [0, 0] means the prop is not moved. - * @default [0, 0] - */ - @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) - @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) - @:optional - var scroll:haxe.ds.Either>; - - /** - * An optional array of animations which the prop can play. - * @default Prop has no animations. - */ - @:optional - var animations:Array; - - /** - * If animations are used, this is the name of the animation to play first. - * @default Don't play an animation. - */ - @:optional - var startingAnimation:Null; - - /** - * The animation type to use. - * Options: "sparrow", "packer" - * @default "sparrow" - */ - @:default("sparrow") - @:optional - var animType:String; -}; - -typedef StageDataCharacter = -{ - /** - * A number determining the stack order of the character, relative to props and other characters in the stage. - * Again, just like CSS. - * @default 0 - */ - var zIndex:Int; - - /** - * The position to render the character at. - */ - var position:Array; - - /** - * The camera offsets to apply when focusing on the character on this stage. - * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF - */ - var cameraOffsets:Array; -}; diff --git a/source/funkin/play/stage/StageProp.hx b/source/funkin/play/stage/StageProp.hx index 4f67c5e4b..4d846162b 100644 --- a/source/funkin/play/stage/StageProp.hx +++ b/source/funkin/play/stage/StageProp.hx @@ -1,10 +1,10 @@ package funkin.play.stage; import funkin.modding.events.ScriptEvent; -import flixel.FlxSprite; +import funkin.graphics.FunkinSprite; import funkin.modding.IScriptedClass.IStateStageProp; -class StageProp extends FlxSprite implements IStateStageProp +class StageProp extends FunkinSprite implements IStateStageProp { /** * An internal name for this prop. diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 0853bf390..3090abad2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -12,6 +12,7 @@ import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxSubState; import flixel.group.FlxSpriteGroup; +import funkin.graphics.FunkinSprite; import flixel.input.keyboard.FlxKey; import flixel.math.FlxMath; import flixel.math.FlxPoint; @@ -56,7 +57,7 @@ import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongDataUtils; import funkin.ui.debug.charting.commands.ChartEditorCommand; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.save.Save; import funkin.ui.debug.charting.commands.AddEventsCommand; import funkin.ui.debug.charting.commands.AddNotesCommand; @@ -2230,7 +2231,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); var playheadBaseYPos:Float = GRID_INITIAL_Y_POS; gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos); - var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); + var playheadSprite:FunkinSprite = new FunkinSprite().makeSolidColor(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadSprite.y = 0; gridPlayhead.add(playheadSprite); @@ -5951,9 +5952,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState ChartEditorNoteSprite.noteFrameCollection = null; // Stop the music. - welcomeMusic.destroy(); - audioInstTrack.destroy(); - audioVocalTrackGroup.destroy(); + if (welcomeMusic != null) welcomeMusic.destroy(); + if (audioInstTrack != null) audioInstTrack.destroy(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.destroy(); } function applyCanQuickSave():Void diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 1e1a02974..32e9fef53 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -13,7 +13,7 @@ import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.song.Song; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog; import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index ce1997968..1916f92c2 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -1,7 +1,6 @@ package funkin.ui.debug.charting.handlers; -import funkin.play.stage.StageData.StageDataParser; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import haxe.ui.components.HorizontalSlider; @@ -16,8 +15,7 @@ import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.event.SongEvent; import funkin.play.song.SongSerializer; -import funkin.play.stage.StageData; -import funkin.play.stage.StageData.StageDataParser; +import funkin.data.stage.StageData; import haxe.ui.RuntimeComponentBuilder; import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.ui.haxeui.components.CharacterPlayer; diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx index 480873bc5..fbd1562b4 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx @@ -2,7 +2,7 @@ package funkin.ui.debug.charting.toolboxes; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.play.event.SongEvent; import funkin.data.event.SongEventSchema; import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx index 98aa02151..06b20ed7c 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx @@ -2,7 +2,8 @@ package funkin.ui.debug.charting.toolboxes; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; +import funkin.data.stage.StageRegistry; import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; import funkin.ui.debug.charting.util.ChartEditorDropdowns; import haxe.ui.components.Button; @@ -13,6 +14,7 @@ import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; +import funkin.play.stage.Stage; import haxe.ui.containers.Box; import haxe.ui.containers.Frame; import haxe.ui.events.UIEvent; @@ -199,11 +201,11 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature}; var stageId:String = chartEditorState.currentSongMetadata.playData.stage; - var stageData:Null = StageDataParser.parseStageData(stageId); + var stage:Null = StageRegistry.instance.fetchEntry(stageId); if (inputStage != null) { - inputStage.value = (stageData != null) ? - {id: stageId, text: stageData.name} : + inputStage.value = (stage != null) ? + {id: stage.id, text: stage.stageName} : {id: "mainStage", text: "Main Stage"}; } diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx index dfa0408d3..14c07440b 100644 --- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx +++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx @@ -2,10 +2,11 @@ package funkin.ui.debug.charting.util; import funkin.data.notestyle.NoteStyleRegistry; import funkin.play.notes.notestyle.NoteStyle; -import funkin.play.stage.StageData; -import funkin.play.stage.StageData.StageDataParser; +import funkin.data.stage.StageData; +import funkin.data.stage.StageRegistry; import funkin.play.character.CharacterData; import haxe.ui.components.DropDown; +import funkin.play.stage.Stage; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData.CharacterDataParser; @@ -60,16 +61,16 @@ class ChartEditorDropdowns { dropDown.dataSource.clear(); - var stageIds:Array = StageDataParser.listStageIds(); + var stageIds:Array = StageRegistry.instance.listEntryIds(); var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"}; for (stageId in stageIds) { - var stage:Null = StageDataParser.parseStageData(stageId); + var stage:Null = StageRegistry.instance.fetchEntry(stageId); if (stage == null) continue; - var value = {id: stageId, text: stage.name}; + var value = {id: stage.id, text: stage.stageName}; if (startingStageId == stageId) returnValue = value; dropDown.dataSource.add(value); diff --git a/source/funkin/ui/debug/stage/StageOffsetSubState.hx b/source/funkin/ui/debug/stage/StageOffsetSubState.hx index 68546f1c7..e8a5d0a23 100644 --- a/source/funkin/ui/debug/stage/StageOffsetSubState.hx +++ b/source/funkin/ui/debug/stage/StageOffsetSubState.hx @@ -5,15 +5,17 @@ import flixel.input.mouse.FlxMouseEvent; import flixel.math.FlxPoint; import funkin.play.character.BaseCharacter; import funkin.play.PlayState; -import funkin.play.stage.StageData; +import funkin.data.stage.StageData; import funkin.play.stage.StageProp; import funkin.graphics.shaders.StrokeShader; import funkin.ui.haxeui.HaxeUISubState; import funkin.ui.debug.stage.StageEditorCommand; import funkin.util.SerializerUtil; +import funkin.data.stage.StageRegistry; import funkin.util.MouseUtil; import haxe.ui.containers.ListView; import haxe.ui.core.Component; +import funkin.graphics.FunkinSprite; import haxe.ui.events.UIEvent; import haxe.ui.RuntimeComponentBuilder; import openfl.events.Event; @@ -354,7 +356,13 @@ class StageOffsetSubState extends HaxeUISubState function prepStageStuff():String { - var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId); + var stageLol:StageData = StageRegistry.instance.fetchEntry(PlayState.instance.currentStageId)?._data; + + if (stageLol == null) + { + FlxG.log.error("Stage not found in registry!"); + return ""; + } for (prop in stageLol.props) { @@ -378,6 +386,6 @@ class StageOffsetSubState extends HaxeUISubState stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x); stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y); - return SerializerUtil.toJSON(stageLol); + return stageLol.serialize(); } } diff --git a/source/funkin/ui/options/ControlsMenu.hx b/source/funkin/ui/options/ControlsMenu.hx index b83b54152..ea04e1208 100644 --- a/source/funkin/ui/options/ControlsMenu.hx +++ b/source/funkin/ui/options/ControlsMenu.hx @@ -8,6 +8,7 @@ import flixel.group.FlxGroup; import flixel.input.actions.FlxActionInput; import flixel.input.gamepad.FlxGamepadInputID; import flixel.input.keyboard.FlxKey; +import funkin.graphics.FunkinSprite; import funkin.input.Controls; import funkin.ui.AtlasText; import funkin.ui.MenuList; @@ -61,8 +62,8 @@ class ControlsMenu extends funkin.ui.options.OptionsState.Page if (FlxG.gamepads.numActiveGamepads > 0) { - var devicesBg:FlxSprite = new FlxSprite(); - devicesBg.makeGraphic(FlxG.width, 100, 0xFFFAFD6D); + var devicesBg:FunkinSprite = new FunkinSprite(); + devicesBg.makeSolidColor(FlxG.width, 100, 0xFFFAFD6D); add(devicesBg); deviceList = new TextMenuList(Horizontal, None); add(deviceList); diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index d6dd536f7..a616fd46b 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -10,6 +10,7 @@ import flixel.group.FlxGroup.FlxTypedGroup; import flixel.text.FlxText; import flixel.addons.transition.FlxTransitionableState; import flixel.tweens.FlxEase; +import funkin.graphics.FunkinSprite; import funkin.ui.MusicBeatState; import flixel.tweens.FlxTween; import flixel.util.FlxColor; @@ -153,7 +154,7 @@ class StoryMenuState extends MusicBeatState updateBackground(); - var black:FlxSprite = new FlxSprite(levelBackground.x, 0).makeGraphic(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK); + var black:FunkinSprite = new FunkinSprite(levelBackground.x, 0).makeSolidColor(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK); black.zIndex = levelBackground.zIndex - 1; add(black); diff --git a/source/funkin/ui/title/OutdatedSubState.hx b/source/funkin/ui/title/OutdatedSubState.hx index d262fc4e4..012823541 100644 --- a/source/funkin/ui/title/OutdatedSubState.hx +++ b/source/funkin/ui/title/OutdatedSubState.hx @@ -15,7 +15,7 @@ class OutdatedSubState extends MusicBeatState override function create() { super.create(); - var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); + var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK); add(bg); var ver = "v" + Application.current.meta.get('version'); var txt:FlxText = new FlxText(0, 0, FlxG.width, diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index bc44af073..a5dcd6def 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -13,6 +13,7 @@ import funkin.audio.visualize.SpectogramSprite; import funkin.graphics.shaders.ColorSwap; import funkin.graphics.shaders.LeftMaskShader; import funkin.data.song.SongRegistry; +import funkin.graphics.FunkinSprite; import funkin.ui.MusicBeatState; import funkin.data.song.SongData.SongMusicData; import funkin.graphics.shaders.TitleOutline; @@ -118,7 +119,8 @@ class TitleState extends MusicBeatState persistentUpdate = true; - var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); + var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK); + bg.screenCenter(); add(bg); logoBl = new FlxSprite(-150, -100); diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index a223a4123..da9aeb28b 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -13,6 +13,7 @@ import funkin.play.song.Song.SongDifficulty; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatState; import haxe.io.Path; +import funkin.graphics.FunkinSprite; import lime.app.Future; import lime.app.Promise; import lime.utils.AssetLibrary; @@ -42,7 +43,7 @@ class LoadingState extends MusicBeatState override function create():Void { - var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, 0xFFcaff4d); + var bg:FlxSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d); add(bg); funkay = new FlxSprite(); @@ -53,7 +54,7 @@ class LoadingState extends MusicBeatState funkay.scrollFactor.set(); funkay.screenCenter(); - loadBar = new FlxSprite(0, FlxG.height - 20).makeGraphic(FlxG.width, 10, 0xFFff16d2); + loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(FlxG.width, 10, 0xFFff16d2); loadBar.screenCenter(X); add(loadBar); From cd9035da199f3b114b84745785574ffb902e25fd Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 16 Jan 2024 17:08:41 -0500 Subject: [PATCH 35/60] Update assets --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 9b9baa6f2..9e385784b 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9b9baa6f2b38dc9bd3354b350084f548e1e16c0f +Subproject commit 9e385784b1d2f4332de0d696b1df655cfa269da0 From 19cd7da8ee39ec3b80702d154fcfcc87fa178802 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 01:01:13 -0500 Subject: [PATCH 36/60] Fix an FlxDrawItems crash tied to FlxAnimate --- hmm.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hmm.json b/hmm.json index d93ea0658..7e17f105c 100644 --- a/hmm.json +++ b/hmm.json @@ -11,7 +11,7 @@ "name": "flixel", "type": "git", "dir": null, - "ref": "a83738673e7edbf8acba3a1426af284dfe6719fe", + "ref": "07c6018008801972d12275690fc144fcc22e3de6", "url": "https://github.com/FunkinCrew/flixel" }, { @@ -37,7 +37,7 @@ "name": "flxanimate", "type": "git", "dir": null, - "ref": "d7c5621be742e2c98d523dfe5af7528835eaff1e", + "ref": "9bacdd6ea39f5e3a33b0f5dfb7bc583fe76060d4", "url": "https://github.com/FunkinCrew/flxanimate" }, { From e44b028946e5d98c5798ec5aee0502c54fbac968 Mon Sep 17 00:00:00 2001 From: Jenny Crowe Date: Wed, 17 Jan 2024 16:24:03 -0700 Subject: [PATCH 37/60] Added side sliders that alter the volume of vocals and hitsounds on player/opponent sides. --- assets | 2 +- .../ui/debug/charting/ChartEditorState.hx | 88 ++++++++++++++++++- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/assets b/assets index d094640f7..0e9019f0f 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d094640f727a670a348b3579d11af5ff6a2ada3a +Subproject commit 0e9019f0fcb53f3e554604ea9a4e62d381873d1f diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index e98809ce8..5fa5308e2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -104,6 +104,7 @@ import haxe.ui.components.Label; import haxe.ui.components.Button; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; +import haxe.ui.components.VerticalSlider; import haxe.ui.components.TextField; import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.Frame; @@ -720,6 +721,34 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent; } + /** + * Sound multiplier for vocals and hitsounds on the player's side. + */ + var soundMultiplierPlayer(default, set):Float = 1.0; + + function set_soundMultiplierPlayer(value:Float):Float + { + soundMultiplierPlayer = value; + var vocalTargetVolume:Float = (menubarItemVolumeVocals.value ?? 100.0) / 100.0; + if (audioVocalTrackGroup != null) audioVocalTrackGroup.playerVolume = vocalTargetVolume * soundMultiplierPlayer; + + return soundMultiplierPlayer; + } + + /** + * Sound multiplier for vocals and hitsounds on the opponent's side. + */ + var soundMultiplierOpponent(default, set):Float = 1.0; + + function set_soundMultiplierOpponent(value:Float):Float + { + soundMultiplierOpponent = value; + var vocalTargetVolume:Float = (menubarItemVolumeVocals.value ?? 100.0) / 100.0; + if (audioVocalTrackGroup != null) audioVocalTrackGroup.opponentVolume = vocalTargetVolume * soundMultiplierOpponent; + + return soundMultiplierOpponent; + } + // Auto-save /** @@ -1749,6 +1778,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var buttonSelectEvent:Button; + /** + * The slider above the grid that sets the volume of the player's sounds. + * Constructed manually and added to the layout so we can control its position. + */ + var sliderVolumePlayer:Slider; + + /** + * The slider above the grid that sets the volume of the opponent's sounds. + * Constructed manually and added to the layout so we can control its position. + */ + var sliderVolumeOpponent:Slider; + /** * RENDER OBJECTS */ @@ -2557,6 +2598,37 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new SetItemSelectionCommand([], currentSongChartEventData)); } } + + function setupSideSlider(x, y):VerticalSlider + { + var slider = new VerticalSlider(); + slider.allowFocus = false; + slider.x = x; + slider.y = y; + slider.width = NOTE_SELECT_BUTTON_HEIGHT; + slider.height = GRID_SIZE * 4; + slider.pos = slider.max; + slider.tooltip = "Slide to set the volume of sounds on this side."; + slider.zIndex = 110; + slider.styleNames = "sideSlider"; + add(slider); + + return slider; + } + + var sliderY = GRID_INITIAL_Y_POS + 34; + sliderVolumeOpponent = setupSideSlider(GRID_X_POS - 64, sliderY); + sliderVolumePlayer = setupSideSlider(buttonSelectEvent.x + buttonSelectEvent.width, sliderY); + + sliderVolumePlayer.onChange = event -> { + var volume:Float = event.value.toFloat() / 100.0; + soundMultiplierPlayer = volume; + } + + sliderVolumeOpponent.onChange = event -> { + var volume:Float = event.value.toFloat() / 100.0; + soundMultiplierOpponent = volume; + } } /** @@ -2797,7 +2869,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemVolumeVocals.onChange = event -> { var volume:Float = event.value.toFloat() / 100.0; - if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume; + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.playerVolume = volume * soundMultiplierPlayer; + audioVocalTrackGroup.opponentVolume = volume * soundMultiplierOpponent; + } menubarLabelVolumeVocals.text = 'Voices - ${Std.int(event.value)}%'; } @@ -5662,7 +5738,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState audioInstTrack.volume = instTargetVolume; audioInstTrack.onComplete = null; } - if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = vocalTargetVolume; + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.playerVolume = vocalTargetVolume * soundMultiplierPlayer; + audioVocalTrackGroup.opponentVolume = vocalTargetVolume * soundMultiplierOpponent; + } } function updateTimeSignature():Void @@ -5864,9 +5944,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState switch (noteData.getStrumlineIndex()) { case 0: // Player - if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume); + if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume * soundMultiplierPlayer); case 1: // Opponent - if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume); + if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume * soundMultiplierOpponent); } } } From 027c2843f4958158e5ae74deca7499c2160ac91f Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 22:19:40 -0500 Subject: [PATCH 38/60] This bug took me like 4-5 hours of staring at code to fix i am going crazy graaaa --- .../ui/debug/charting/ChartEditorState.hx | 25 ++++++++++--------- .../handlers/ChartEditorThemeHandler.hx | 6 +++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index e98809ce8..5e8f112dd 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1958,7 +1958,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState buildGrid(); buildMeasureTicks(); buildNotePreview(); - buildSelectionBox(); buildAdditionalUI(); populateOpenRecentMenu(); @@ -2287,17 +2286,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); } - function buildSelectionBox():Void - { - if (selectionBoxSprite == null) throw 'ERROR: Tried to build selection box, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().'; - - selectionBoxSprite.scrollFactor.set(0, 0); - add(selectionBoxSprite); - selectionBoxSprite.zIndex = 30; - - setSelectionBoxBounds(); - } - function setSelectionBoxBounds(bounds:FlxRect = null):Void { if (selectionBoxSprite == null) @@ -2319,6 +2307,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } + /** + * Automatically goes through and calls render on everything you added. + */ + override public function draw():Void + { + if (selectionBoxStartPos != null) + { + trace('selectionBoxSprite: ${selectionBoxSprite.visible} ${selectionBoxSprite.exists} ${this.members.contains(selectionBoxSprite)}'); + } + + super.draw(); + } + function calculateNotePreviewViewportBounds():FlxRect { var bounds:FlxRect = new FlxRect(); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index 98bb5c2c8..89fd4d5d3 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -317,6 +317,12 @@ class ChartEditorThemeHandler ChartEditorState.GRID_SIZE - (2 * SELECTION_SQUARE_BORDER_WIDTH + 8)), 32, 32); + + state.selectionBoxSprite.scrollFactor.set(0, 0); + state.selectionBoxSprite.zIndex = 30; + state.add(state.selectionBoxSprite); + + state.setSelectionBoxBounds(); } static function updateNotePreview(state:ChartEditorState):Void From a0c4499b03bb7c14fc2bf9bb0d78313617a54c28 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 23:12:59 -0500 Subject: [PATCH 39/60] Fix a bug where replaying a level makes a pink screen --- source/funkin/play/PlayState.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 20f19f714..cc9debf13 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1354,6 +1354,7 @@ class PlayState extends MusicBeatSubState function loadStage(id:String):Void { currentStage = StageRegistry.instance.fetchEntry(id); + currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory. if (currentStage != null) { From 26b761066303aa0fb1ab2e71689235ce9456baaa Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 23:14:28 -0500 Subject: [PATCH 40/60] Fix an error with playable Pico death --- assets | 2 +- source/funkin/play/GameOverSubState.hx | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/assets b/assets index 9e385784b..d6be0e084 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9e385784b1d2f4332de0d696b1df655cfa269da0 +Subproject commit d6be0e084e4fda0416eca9ec7fe406af9b626e5c diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 88d9be7d4..36f72237e 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -23,6 +23,12 @@ import funkin.play.character.BaseCharacter; */ class GameOverSubState extends MusicBeatSubState { + /** + * The currently active GameOverSubState. + * There should be only one GameOverSubState in existance at a time, we can use a singleton. + */ + public static var instance:GameOverSubState = null; + /** * Which alternate animation on the character to use. * You can set this via script. @@ -88,6 +94,13 @@ class GameOverSubState extends MusicBeatSubState override public function create() { + if (instance != null) + { + // TODO: Do something in this case? IDK. + trace('WARNING: GameOverSubState instance already exists. This should not happen.'); + } + instance = this; + super.create(); // @@ -283,10 +296,10 @@ class GameOverSubState extends MusicBeatSubState */ function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void { - var musicPath = Paths.music('gameOver' + musicSuffix); + var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix); if (isEnding) { - musicPath = Paths.music('gameOverEnd' + musicSuffix); + musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix); } if (!gameOverMusic.playing || force) { @@ -306,7 +319,7 @@ class GameOverSubState extends MusicBeatSubState public static function playBlueBalledSFX() { blueballed = true; - FlxG.sound.play(Paths.sound('fnf_loss_sfx' + blueBallSuffix)); + FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)); } var playingJeffQuote:Bool = false; @@ -329,6 +342,11 @@ class GameOverSubState extends MusicBeatSubState } }); } + + public override function toString():String + { + return "GameOverSubState"; + } } typedef GameOverParams = From 25fe2c0d39ea0c3bf371fee23c5f3b4bee5ab53b Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 17 Jan 2024 23:53:23 -0500 Subject: [PATCH 41/60] Rewrite Upload Vocals dialog. --- assets | 2 +- .../dialogs/ChartEditorUploadChartDialog.hx | 1 + .../dialogs/ChartEditorUploadVocalsDialog.hx | 311 ++++++++++++++++++ .../handlers/ChartEditorDialogHandler.hx | 223 +++---------- 4 files changed, 364 insertions(+), 173 deletions(-) create mode 100644 source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx diff --git a/assets b/assets index 9e385784b..d0e0c9f94 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9e385784b1d2f4332de0d696b1df655cfa269da0 +Subproject commit d0e0c9f94961793a815f66c9a2875c99ef3b4c8c diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx index 5b84148c6..17f047106 100644 --- a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx @@ -13,6 +13,7 @@ import haxe.ui.notifications.NotificationType; // @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml")) +@:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorUploadChartDialog extends ChartEditorBaseDialog { var dropHandlers:Array = []; diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx new file mode 100644 index 000000000..537c7c36e --- /dev/null +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadVocalsDialog.hx @@ -0,0 +1,311 @@ +package funkin.ui.debug.charting.dialogs; + +import funkin.input.Cursor; +import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; +import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams; +import funkin.util.FileUtil; +import funkin.play.character.CharacterData; +import haxe.io.Path; +import haxe.ui.components.Button; +import haxe.ui.components.Label; +import haxe.ui.containers.dialogs.Dialog.DialogButton; +import haxe.ui.containers.dialogs.Dialog.DialogEvent; +import haxe.ui.containers.Box; +import haxe.ui.containers.dialogs.Dialogs; +import haxe.ui.core.Component; +import haxe.ui.notifications.NotificationManager; +import haxe.ui.notifications.NotificationType; + +// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. + +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals.xml")) +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorUploadVocalsDialog extends ChartEditorBaseDialog +{ + var dropHandlers:Array = []; + + var vocalContainer:Component; + var dialogCancel:Button; + var dialogNoVocals:Button; + var dialogContinue:Button; + + var charIds:Array; + var instId:String; + var hasClearedVocals:Bool = false; + + public function new(state2:ChartEditorState, charIds:Array, params2:DialogParams) + { + super(state2, params2); + + this.charIds = charIds; + this.instId = chartEditorState.currentInstrumentalId; + + dialogCancel.onClick = function(_) { + hideDialog(DialogButton.CANCEL); + } + + dialogNoVocals.onClick = function(_) { + // Dismiss + chartEditorState.wipeVocalData(); + hideDialog(DialogButton.APPLY); + }; + + dialogContinue.onClick = function(_) { + // Dismiss + hideDialog(DialogButton.APPLY); + }; + + buildDropHandlers(); + } + + function buildDropHandlers():Void + { + for (charKey in charIds) + { + trace('Adding vocal upload for character ${charKey}'); + + var charMetadata:Null = CharacterDataParser.fetchCharacterData(charKey); + var charName:String = charMetadata?.name ?? charKey; + + var vocalsEntry = new ChartEditorUploadVocalsEntry(charName); + + var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null}; + + var onDropFile:String->Void = function(pathStr:String) { + trace('Selected file: $pathStr'); + var path:Path = new Path(pathStr); + + if (chartEditorState.loadVocalsFromPath(path, charKey, this.instId, !this.hasClearedVocals)) + { + this.hasClearedVocals = true; + // Tell the user the load was successful. + chartEditorState.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${chartEditorState.selectedVariation}'); + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}'; + #end + + dialogNoVocals.hidden = true; + chartEditorState.removeDropHandler(dropHandler); + } + else + { + trace('Failed to load vocal track (${path.file}.${path.ext})'); + + chartEditorState.error('Failed to Load Vocals', + 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${chartEditorState.selectedVariation})'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + } + }; + + vocalsEntry.onClick = function(_event) { + Dialogs.openBinaryFile('Open $charName Vocals', [ + {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) { + if (selectedFile != null && selectedFile.bytes != null) + { + trace('Selected file: ' + selectedFile.name); + + if (chartEditorState.loadVocalsFromBytes(selectedFile.bytes, charKey, this.instId, !this.hasClearedVocals)) + { + hasClearedVocals = true; + // Tell the user the load was successful. + chartEditorState.success('Loaded Vocals', + 'Loaded vocals for $charName (${selectedFile.name}), variation ${chartEditorState.selectedVariation}'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}'; + #end + + dialogNoVocals.hidden = true; + } + else + { + trace('Failed to load vocal track (${selectedFile.fullPath})'); + + chartEditorState.error('Failed to Load Vocals', + 'Failed to load vocal track (${selectedFile.name}) for variation (${chartEditorState.selectedVariation})'); + + #if FILE_DROP_SUPPORTED + vocalsEntry.vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntry.vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + } + } + }); + } + + dropHandler.handler = onDropFile; + + // onDropFile + #if FILE_DROP_SUPPORTED + dropHandlers.push(dropHandler); + #end + + vocalContainer.addComponent(vocalsEntry); + } + } + + public static function build(state:ChartEditorState, charIds:Array, ?closable:Bool, ?modal:Bool):ChartEditorUploadVocalsDialog + { + var dialog = new ChartEditorUploadVocalsDialog(state, charIds, + { + closable: closable ?? false, + modal: modal ?? true + }); + + for (dropTarget in dialog.dropHandlers) + { + state.addDropHandler(dropTarget); + } + + dialog.showDialog(modal ?? true); + + return dialog; + } + + public override function onClose(event:DialogEvent):Void + { + super.onClose(event); + + if (event.button != DialogButton.APPLY && !this.closable) + { + // User cancelled the wizard! Back to the welcome dialog. + chartEditorState.openWelcomeDialog(this.closable); + } + + for (dropTarget in dropHandlers) + { + chartEditorState.removeDropHandler(dropTarget); + } + } + + public override function lock():Void + { + super.lock(); + this.dialogCancel.disabled = true; + } + + public override function unlock():Void + { + super.unlock(); + this.dialogCancel.disabled = false; + } + + /** + * Called when clicking the Upload Chart box. + */ + public function onClickChartBox():Void + { + if (this.locked) return; + + this.lock(); + // TODO / BUG: File filtering not working on mac finder dialog, so we don't use it for now + #if !mac + FileUtil.browseForBinaryFile('Open Chart', [FileUtil.FILE_EXTENSION_INFO_FNFC], onSelectFile, onCancelBrowse); + #else + FileUtil.browseForBinaryFile('Open Chart', null, onSelectFile, onCancelBrowse); + #end + } + + /** + * Called when a file is selected by dropping a file onto the Upload Chart box. + */ + function onDropFileChartBox(pathStr:String):Void + { + var path:Path = new Path(pathStr); + trace('Dropped file (${path})'); + + try + { + var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(chartEditorState, path.toString()); + if (result != null) + { + chartEditorState.success('Loaded Chart', + result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}'); + this.hideDialog(DialogButton.APPLY); + } + else + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()})'); + } + } + catch (err) + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${path.toString()}): ${err}'); + } + } + + /** + * Called when a file is selected by the dialog displayed when clicking the Upload Chart box. + */ + function onSelectFile(selectedFile:SelectedFileInfo):Void + { + this.unlock(); + + if (selectedFile != null && selectedFile.bytes != null) + { + try + { + var result:Null> = ChartEditorImportExportHandler.loadFromFNFC(chartEditorState, selectedFile.bytes); + if (result != null) + { + chartEditorState.success('Loaded Chart', + result.length == 0 ? 'Loaded chart (${selectedFile.name})' : 'Loaded chart (${selectedFile.name})\n${result.join("\n")}'); + + if (selectedFile.fullPath != null) chartEditorState.currentWorkingFilePath = selectedFile.fullPath; + this.hideDialog(DialogButton.APPLY); + } + } + catch (err) + { + chartEditorState.failure('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}'); + } + } + } + + function onCancelBrowse():Void + { + this.unlock(); + } +} + +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-vocals-entry.xml")) +class ChartEditorUploadVocalsEntry extends Box +{ + public var vocalsEntryLabel:Label; + + var charName:String; + + public function new(charName:String) + { + super(); + + this.charName = charName; + + #if FILE_DROP_SUPPORTED + vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end + + this.onMouseOver = function(_event) { + // if (this.locked) return; + this.swapClass('upload-bg', 'upload-bg-hover'); + Cursor.cursorMode = Pointer; + } + + this.onMouseOut = function(_event) { + this.swapClass('upload-bg-hover', 'upload-bg'); + Cursor.cursorMode = Default; + } + } +} diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 32e9fef53..970f021ac 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -19,6 +19,7 @@ import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu; import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog; import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog; +import funkin.ui.debug.charting.dialogs.ChartEditorUploadVocalsDialog; import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.util.Constants; import funkin.util.DateUtil; @@ -59,11 +60,8 @@ using Lambda; class ChartEditorDialogHandler { // Paths to HaxeUI layout files for each dialog. - static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart'); static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata'); - static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals'); - static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry'); static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts'); static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry'); static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart'); @@ -105,6 +103,56 @@ class ChartEditorDialogHandler return dialog; } + /** + * Builds and opens a dialog letting the user browse for a chart file to open. + * @param state The current chart editor state. + * @param closable Whether the dialog can be closed by the user. + * @return The dialog that was opened. + */ + public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null + { + var dialog = ChartEditorUploadChartDialog.build(state, closable); + + dialog.zIndex = 1000; + state.isHaxeUIDialogOpen = true; + + return dialog; + } + + /** + * Builds and opens a dialog where the user uploads vocals for the current song. + * @param state The current chart editor state. + * @param closable Whether the dialog can be closed by the user. + * @return The dialog that was opened. + */ + public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog + { + var charData:SongCharacterData = state.currentSongMetadata.playData.characters; + + var hasClearedVocals:Bool = false; + + var charIdsForVocals:Array = [charData.player, charData.opponent]; + + var dialog = ChartEditorUploadVocalsDialog.build(state, charIdsForVocals, closable); + + dialog.zIndex = 1000; + state.isHaxeUIDialogOpen = true; + + return dialog; + } + + /** + * Builds and opens the dialog for selecting a character. + */ + public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null + { + var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition); + + menu.zIndex = 1000; + + return menu; + } + /** * Builds and opens a dialog letting the user know a backup is available, and prompting them to load it. */ @@ -186,22 +234,6 @@ class ChartEditorDialogHandler return dialog; } - /** - * Builds and opens a dialog letting the user browse for a chart file to open. - * @param state The current chart editor state. - * @param closable Whether the dialog can be closed by the user. - * @return The dialog that was opened. - */ - public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null - { - var dialog = ChartEditorUploadChartDialog.build(state, closable); - - dialog.zIndex = 1000; - state.isHaxeUIDialogOpen = true; - - return dialog; - } - /** * Open the wizard for opening an existing chart from individual files. * @param state @@ -288,15 +320,6 @@ class ChartEditorDialogHandler }; } - public static function openCharacterDropdown(state:ChartEditorState, charType:CharacterType, lockPosition:Bool = false):Null - { - var menu = ChartEditorCharacterIconSelectorMenu.build(state, charType, lockPosition); - - menu.zIndex = 1000; - - return menu; - } - public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata @@ -699,150 +722,6 @@ class ChartEditorDialogHandler return dialog; } - /** - * Builds and opens a dialog where the user uploads vocals for the current song. - * @param state The current chart editor state. - * @param closable Whether the dialog can be closed by the user. - * @return The dialog that was opened. - */ - public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog - { - var instId:String = state.currentInstrumentalId; - var charIdsForVocals:Array = []; - - var charData:SongCharacterData = state.currentSongMetadata.playData.characters; - - var hasClearedVocals:Bool = false; - - charIdsForVocals.push(charData.player); - charIdsForVocals.push(charData.opponent); - - var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable); - if (dialog == null) throw 'Could not locate Upload Vocals dialog'; - - var dialogContainer:Null = dialog.findComponent('vocalContainer'); - if (dialogContainer == null) throw 'Could not locate vocalContainer in Upload Vocals dialog'; - - var buttonCancel:Null