diff --git a/source/funkin/modding/base/ScriptedFlxRuntimeShader.hx b/source/funkin/modding/base/ScriptedFlxRuntimeShader.hx index 50af136ff..4d17638bb 100644 --- a/source/funkin/modding/base/ScriptedFlxRuntimeShader.hx +++ b/source/funkin/modding/base/ScriptedFlxRuntimeShader.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import flixel.addons.display.FlxRuntimeShader; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedFlxRuntimeShader extends FlxRuntimeShader implements HScriptedClass {} +class ScriptedFlxRuntimeShader extends flixel.addons.display.FlxRuntimeShader implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/ScriptedFlxSprite.hx b/source/funkin/modding/base/ScriptedFlxSprite.hx index e4ae69107..036f16c00 100644 --- a/source/funkin/modding/base/ScriptedFlxSprite.hx +++ b/source/funkin/modding/base/ScriptedFlxSprite.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import flixel.FlxSprite; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedFlxSprite extends FlxSprite implements HScriptedClass {} +class ScriptedFlxSprite extends flixel.FlxSprite implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/ScriptedFlxSpriteGroup.hx b/source/funkin/modding/base/ScriptedFlxSpriteGroup.hx index 7d35aab47..049c2b668 100644 --- a/source/funkin/modding/base/ScriptedFlxSpriteGroup.hx +++ b/source/funkin/modding/base/ScriptedFlxSpriteGroup.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import flixel.group.FlxSpriteGroup; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedFlxSpriteGroup extends FlxSpriteGroup implements HScriptedClass {} +class ScriptedFlxSpriteGroup extends flixel.group.FlxSpriteGroup implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/ScriptedFlxState.hx b/source/funkin/modding/base/ScriptedFlxState.hx index 2a498e66b..4069a33eb 100644 --- a/source/funkin/modding/base/ScriptedFlxState.hx +++ b/source/funkin/modding/base/ScriptedFlxState.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import flixel.FlxState; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedFlxState extends FlxState implements HScriptedClass {} +class ScriptedFlxState extends flixel.FlxState implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/ScriptedFlxSubState.hx b/source/funkin/modding/base/ScriptedFlxSubState.hx index 90c2a6474..e5b85b26d 100644 --- a/source/funkin/modding/base/ScriptedFlxSubState.hx +++ b/source/funkin/modding/base/ScriptedFlxSubState.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import flixel.FlxSubState; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedFlxSubState extends FlxSubState implements HScriptedClass {} +class ScriptedFlxSubState extends flixel.FlxSubState implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/ScriptedFlxTransitionableState.hx b/source/funkin/modding/base/ScriptedFlxTransitionableState.hx index 1d2b92b27..4bc310222 100644 --- a/source/funkin/modding/base/ScriptedFlxTransitionableState.hx +++ b/source/funkin/modding/base/ScriptedFlxTransitionableState.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import flixel.addons.transition.FlxTransitionableState; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedFlxTransitionableState extends FlxTransitionableState implements HScriptedClass {} +class ScriptedFlxTransitionableState extends flixel.addons.transition.FlxTransitionableState implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/ScriptedFlxUIState.hx b/source/funkin/modding/base/ScriptedFlxUIState.hx index 1799be32a..4260850ad 100644 --- a/source/funkin/modding/base/ScriptedFlxUIState.hx +++ b/source/funkin/modding/base/ScriptedFlxUIState.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import flixel.addons.ui.FlxUIState; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedFlxUIState extends FlxUIState implements HScriptedClass {} +class ScriptedFlxUIState extends flixel.addons.ui.FlxUIState implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/ScriptedMusicBeatState.hx b/source/funkin/modding/base/ScriptedMusicBeatState.hx index 430022029..236df3911 100644 --- a/source/funkin/modding/base/ScriptedMusicBeatState.hx +++ b/source/funkin/modding/base/ScriptedMusicBeatState.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import funkin.MusicBeatState; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedMusicBeatState extends MusicBeatState implements HScriptedClass {} +class ScriptedMusicBeatState extends funkin.MusicBeatState implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/ScriptedMusicBeatSubstate.hx b/source/funkin/modding/base/ScriptedMusicBeatSubstate.hx index dd399a74c..c56776ea8 100644 --- a/source/funkin/modding/base/ScriptedMusicBeatSubstate.hx +++ b/source/funkin/modding/base/ScriptedMusicBeatSubstate.hx @@ -1,7 +1,6 @@ package funkin.modding.base; -import funkin.MusicBeatSubstate; -import polymod.hscript.HScriptedClass; - @:hscriptClass -class ScriptedMusicBeatSubstate extends MusicBeatSubstate implements HScriptedClass {} +class ScriptedMusicBeatSubstate extends funkin.MusicBeatSubstate implements HScriptedClass +{ +} diff --git a/source/funkin/modding/base/import.hx b/source/funkin/modding/base/import.hx new file mode 100644 index 000000000..957fb894c --- /dev/null +++ b/source/funkin/modding/base/import.hx @@ -0,0 +1 @@ +import polymod.hscript.HScriptedClass; diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index a7008bc5a..3ff247e2b 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -1,34 +1,54 @@ package funkin.ui.debug.charting; -import funkin.input.Cursor; -import haxe.ui.containers.Box; -import haxe.ui.containers.dialogs.Dialogs; -import haxe.ui.components.Link; -import flixel.util.FlxTimer; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.play.character.BaseCharacter; +import haxe.ui.components.Label; +import haxe.ui.events.MouseEvent; +import funkin.play.song.SongData.SongPlayableChar; import flixel.FlxSprite; -import haxe.ui.containers.dialogs.Dialog; -import haxe.ui.containers.VBox; +import flixel.util.FlxTimer; +import funkin.input.Cursor; +import funkin.play.song.SongData.SongTimeChange; +import haxe.ui.components.Button; +import haxe.ui.components.DropDown; import haxe.ui.components.Image; +import haxe.ui.components.Link; +import haxe.ui.components.NumberStepper; +import haxe.ui.components.TextField; +import haxe.ui.containers.Box; +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.Dialogs; +import haxe.ui.containers.properties.Property; +import haxe.ui.containers.properties.PropertyGrid; +import haxe.ui.containers.properties.PropertyGroup; +import haxe.ui.containers.VBox; +import haxe.ui.events.UIEvent; + +using Lambda; class ChartEditorDialogHandler { static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT = Paths.ui('chart-editor/dialogs/about'); static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT = Paths.ui('chart-editor/dialogs/welcome'); static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT = Paths.ui('chart-editor/dialogs/upload-inst'); + static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT = Paths.ui('chart-editor/dialogs/song-metadata'); + static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT = Paths.ui('chart-editor/dialogs/song-metadata-chargroup'); + static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT = Paths.ui('chart-editor/dialogs/upload-vocals'); + static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT = Paths.ui('chart-editor/dialogs/upload-vocals-entry'); static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT = Paths.ui('chart-editor/dialogs/user-guide'); /** * */ - public static inline function openAboutDialog(state:ChartEditorState):Void + public static inline function openAboutDialog(state:ChartEditorState):Dialog { - openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true); + return openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true); } /** * Builds and opens a dialog letting the user create a new chart, open a recent chart, or load from a template. */ - public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Void + public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Dialog { var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); @@ -68,13 +88,32 @@ class ChartEditorDialogHandler linkCreateBasic.onClick = (_event) -> { dialog.hideDialog(DialogButton.CANCEL); - openUploadInstDialog(state, false); - }; - // Get the list of songs and insert them as links into the "Create From Song" section. + // Create song wizard + var uploadInstDialog = openUploadInstDialog(state, false); + uploadInstDialog.onDialogClosed = (_event) -> + { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + var songMetadataDialog = openSongMetadataDialog(state); + songMetadataDialog.onDialogClosed = (_event) -> + { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + var uploadVocalsDialog = openUploadVocalsDialog(state); + } + }; + } + }; + } + + // TODO: Get the list of songs and insert them as links into the "Create From Song" section. + return dialog; } - public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Void + public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Dialog { var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable); @@ -117,6 +156,8 @@ class ChartEditorDialogHandler }; addDropHandler(onDropFile); + + return dialog; } static function addDropHandler(handler:String->Void) @@ -135,12 +176,253 @@ class ChartEditorDialogHandler #end } + public static function openSongMetadataDialog(state:ChartEditorState):Dialog + { + var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false); + + var dialogSongName:TextField = dialog.findComponent('dialogSongName', TextField); + dialogSongName.onChange = (event:UIEvent) -> + { + var valid = event.target.text != null && event.target.text != ""; + + if (valid) + { + dialogSongName.removeClass('invalid-value'); + state.currentSongMetadata.songName = event.target.text; + } + else + { + state.currentSongMetadata.songName = null; + } + }; + state.currentSongMetadata.songName = null; + + var dialogSongArtist:TextField = dialog.findComponent('dialogSongArtist', TextField); + dialogSongArtist.onChange = (event:UIEvent) -> + { + var valid = event.target.text != null && event.target.text != ""; + + if (valid) + { + dialogSongArtist.removeClass('invalid-value'); + state.currentSongMetadata.artist = event.target.text; + } + else + { + state.currentSongMetadata.artist = null; + } + }; + state.currentSongMetadata.artist = null; + + var dialogStage:DropDown = dialog.findComponent('dialogStage', DropDown); + dialogStage.onChange = (event:UIEvent) -> + { + var valid = event.data != null && event.data.id != null; + + if (event.data.id == null) + return; + state.currentSongMetadata.playData.stage = event.data.id; + }; + state.currentSongMetadata.playData.stage = null; + + var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown); + dialogNoteSkin.onChange = (event:UIEvent) -> + { + if (event.data.id == null) + return; + state.currentSongMetadata.playData.noteSkin = event.data.id; + }; + state.currentSongMetadata.playData.noteSkin = null; + + var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper); + dialogBPM.onChange = (event:UIEvent) -> + { + if (event.value == null) + return; + + var timeChanges = state.currentSongMetadata.timeChanges; + if (timeChanges == null || timeChanges.length == 0) + { + timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])]; + } + else + { + timeChanges[0].bpm = event.value; + } + state.currentSongMetadata.timeChanges = timeChanges; + }; + + var dialogCharGrid:PropertyGrid = dialog.findComponent('dialogCharGrid', PropertyGrid); + var dialogCharAdd:Button = dialog.findComponent('dialogCharAdd', Button); + dialogCharAdd.onClick = (_event) -> + { + var charGroup:PropertyGroup; + charGroup = buildCharGroup(state, null, () -> + { + dialogCharGrid.removeComponent(charGroup); + }); + dialogCharGrid.addComponent(charGroup); + }; + + // Empty the character list. + state.currentSongMetadata.playData.playableChars = {}; + // Add at least one character group with no Remove button. + dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null)); + + var dialogContinue:Button = dialog.findComponent('dialogContinue', Button); + dialogContinue.onClick = (_event) -> + { + dialog.hideDialog(DialogButton.APPLY); + }; + + return dialog; + } + + static function buildCharGroup(state:ChartEditorState, ?key:String = null, removeFunc:Void->Void):PropertyGroup + { + var groupKey = key; + + var getCharData = () -> + { + if (groupKey == null) + groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}'; + + var result = state.currentSongMetadata.playData.playableChars.get(groupKey); + if (result == null) + { + result = new SongPlayableChar('', 'dad'); + state.currentSongMetadata.playData.playableChars.set(groupKey, result); + } + return result; + } + + var moveCharGroup = (target:String) -> + { + var charData = getCharData(); + state.currentSongMetadata.playData.playableChars.remove(groupKey); + state.currentSongMetadata.playData.playableChars.set(target, charData); + groupKey = target; + } + + var removeGroup = () -> + { + state.currentSongMetadata.playData.playableChars.remove(groupKey); + removeFunc(); + } + + var charData = getCharData(); + + var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT); + + var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown); + charGroupPlayer.onChange = (event:UIEvent) -> + { + charGroup.text = event.data.text; + moveCharGroup(event.data.id); + }; + + if (key == null) + { + // Find the next available player character. + trace(charGroupPlayer.dataSource.data); + } + + var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown); + charGroupOpponent.onChange = (event:UIEvent) -> + { + charData.opponent = event.data.id; + }; + charGroupOpponent.value = getCharData().opponent; + + var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown); + charGroupGirlfriend.onChange = (event:UIEvent) -> + { + charData.girlfriend = event.data.id; + }; + charGroupGirlfriend.value = getCharData().girlfriend; + + var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button); + charGroupRemove.onClick = (_event:MouseEvent) -> + { + removeGroup(); + }; + + if (removeFunc == null) + charGroupRemove.hidden = true; + + return charGroup; + } + + public static function openUploadVocalsDialog(state:ChartEditorState, ?closable:Bool = true):Dialog + { + var charIdsForVocals = []; + + for (charKey in state.currentSongMetadata.playData.playableChars.keys()) + { + var charData = state.currentSongMetadata.playData.playableChars.get(charKey); + charIdsForVocals.push(charKey); + if (charData.opponent != null) + charIdsForVocals.push(charData.opponent); + } + + var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable); + + var dialogContainer = dialog.findComponent('vocalContainer'); + + var onDropFile:String->Void; + + for (charKey in charIdsForVocals) + { + trace('Adding vocal upload for character ${charKey}'); + var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey); + var charName:String = charMetadata.characterName; + + var vocalsEntry = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT); + + var vocalsEntryLabel:Label = vocalsEntry.findComponent('vocalsEntryLabel', Label); + vocalsEntryLabel.text = 'Click to browse for a vocal track for $charName.'; + + vocalsEntry.onClick = (_event) -> + { + Dialogs.openBinaryFile('Open $charName Vocals', [{label: "Audio File (.ogg)", extension: "ogg"}], function(selectedFile) + { + if (selectedFile != null) + { + trace('Selected file: ' + selectedFile); + vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n$selectedFile'; + // state.loadVocalsFromBytes(selectedFile.bytes); + removeDropHandler(onDropFile); + } + }); + } + + dialogContainer.addComponent(vocalsEntry); + } + + var dialogContinue:Button = dialog.findComponent('dialogContinue', Button); + dialogContinue.onClick = (_event) -> + { + dialog.hideDialog(DialogButton.APPLY); + }; + + // TODO: Redo the logic for file drop handler to be more robust. + // We need to distinguish which component the mouse is over when the file is dropped. + + onDropFile = (path:String) -> + { + trace('Dropped file: ' + path); + }; + addDropHandler(onDropFile); + + return dialog; + } + /** * Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor. */ - public static inline function openUserGuideDialog(state:ChartEditorState):Void + public static inline function openUserGuideDialog(state:ChartEditorState):Dialog { - openDialog(state, CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT, true, true); + return openDialog(state, CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT, true, true); } /** @@ -155,6 +437,12 @@ class ChartEditorDialogHandler dialog.closable = closable; dialog.showDialog(modal); + state.isHaxeUIDialogOpen = true; + dialog.onDialogClosed = (_event) -> + { + state.isHaxeUIDialogOpen = false; + }; + return dialog; } } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index a7256402a..dd6f2d8fd 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -295,6 +295,11 @@ class ChartEditorState extends HaxeUIState || Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY, haxe.ui.components.Link); } + /** + * Set by ChartEditorDialogHandler, used to prevent background interaction while the dialog is open. + */ + public var isHaxeUIDialogOpen:Bool = false; + /** * The variation ID for the difficulty which is currently being edited. */ @@ -753,7 +758,7 @@ class ChartEditorState extends HaxeUIState // TODO: We should be loading the music later when the user requests it. // loadDefaultMusic(); - ChartEditorDialogHandler.openSplashDialog(this, false); + ChartEditorDialogHandler.openWelcomeDialog(this, false); } function buildDefaultSongData() @@ -974,7 +979,7 @@ class ChartEditorState extends HaxeUIState // Add functionality to the menu items. - addUIClickListener('menubarItemNewChart', (event:MouseEvent) -> ChartEditorDialogHandler.openSplashDialog(this, true)); + addUIClickListener('menubarItemNewChart', (event:MouseEvent) -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); addUIClickListener('menubarItemLoadInst', (event:MouseEvent) -> ChartEditorDialogHandler.openUploadInstDialog(this, true)); addUIClickListener('menubarItemUndo', (event:MouseEvent) -> undoLastCommand()); @@ -1168,7 +1173,7 @@ class ChartEditorState extends HaxeUIState if (FlxG.keys.justPressed.Q) { - ChartEditorDialogHandler.openSplashDialog(this, true); + ChartEditorDialogHandler.openWelcomeDialog(this, true); } // Right align the BF health icon. @@ -2266,7 +2271,7 @@ class ChartEditorState extends HaxeUIState } } - if (FlxG.keys.justPressed.SPACE) + if (FlxG.keys.justPressed.SPACE && !isHaxeUIDialogOpen) { toggleAudioPlayback(); } @@ -2422,7 +2427,9 @@ class ChartEditorState extends HaxeUIState */ public function loadInstrumentalFromBytes(bytes:haxe.io.Bytes):Void { - audioInstTrack = FlxG.sound.load(openfl.media.Sound.loadCompressedDataFromByteArray(ByteArray.fromBytes(bytes)), 1.0, false); + var openflSound = new openfl.media.Sound(); + openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); + audioInstTrack = FlxG.sound.load(openflSound, 1.0, false); audioInstTrack.autoDestroy = false; audioInstTrack.pause(); @@ -2438,8 +2445,10 @@ class ChartEditorState extends HaxeUIState // Prevent the time from skipping back to 0 when the song ends. audioInstTrack.onComplete = function() { - audioInstTrack.pause(); - audioVocalTrack.pause(); + if (audioInstTrack != null) + audioInstTrack.pause(); + if (audioVocalTrack != null) + audioVocalTrack.pause(); }; var DAD_BATTLE_BPM = 180;