From 1ff6a1656cebdfe415768ffe38bd43b37c663eda Mon Sep 17 00:00:00 2001 From: Lasercar <64717068+Lasercar@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:25:48 +1000 Subject: [PATCH] Open target song stage with song characters Also, nullable test characters (but it's buggy) --- source/funkin/play/PlayState.hx | 18 ++- .../ui/debug/stageeditor/StageEditorState.hx | 86 +++++++--- .../stageeditor/handlers/StageDataHandler.hx | 1 + .../stageeditor/handlers/UndoRedoHandler.hx | 1 + .../toolboxes/StageEditorCharacterToolbox.hx | 59 +++++-- source/funkin/ui/freeplay/FreeplayState.hx | 147 +++++++++++++----- 6 files changed, 235 insertions(+), 77 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 788eac299..cbeae31a5 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -54,7 +54,12 @@ import funkin.play.scoring.Scoring; import funkin.play.song.Song; import funkin.play.stage.Stage; import funkin.save.Save; +#if FEATURE_CHART_EDITOR import funkin.ui.debug.charting.ChartEditorState; +#end +#if FEATURE_STAGE_EDITOR +import funkin.ui.debug.stageeditor.StageEditorState; +#end import funkin.ui.debug.stage.StageOffsetSubState; import funkin.ui.mainmenu.MainMenuState; import funkin.ui.MusicBeatSubState; @@ -3124,7 +3129,18 @@ class PlayState extends MusicBeatSubState // hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!! disableKeys = true; persistentUpdate = false; - openSubState(new StageOffsetSubState()); + // The strings have to be get like this otherwise it just NORs when setting the params? + // Or, none of the characters show up, in the case of the pico songs? + var bf:String = currentStage?.getBoyfriend()?.characterId ?? ''; + var gf:String = currentStage?.getGirlfriend()?.characterId ?? ''; + var dad:String = currentStage?.getDad()?.characterId ?? ''; + FlxG.switchState(() -> new StageEditorState( + { + targetStageId: currentStageId, + targetBfChar: bf, + targetGfChar: gf, + targetDadChar: dad + })); } #end diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx index 4c3d2f240..697181fa1 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorState.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -277,7 +277,10 @@ class StageEditorState extends UIState this.showChars = value; for (cooldude in getCharacters()) + { + if (cooldude == null) continue; cooldude.visible = showChars; + } return value; } @@ -329,20 +332,20 @@ class StageEditorState extends UIState Screen.instance.addComponent(root); // Characters setup. - var gf = CharacterDataParser.fetchCharacter(Save.instance.stageGirlfriendChar, true); - gf.characterType = CharacterType.GF; - var dad = CharacterDataParser.fetchCharacter(Save.instance.stageDadChar, true); - dad.characterType = CharacterType.DAD; - var bf = CharacterDataParser.fetchCharacter(Save.instance.stageBoyfriendChar, true); - bf.characterType = CharacterType.BF; + var gf = CharacterDataParser.fetchCharacter(params?.targetGfChar ?? Save.instance.stageGirlfriendChar, true); + if (gf != null) gf.characterType = CharacterType.GF; + var dad = CharacterDataParser.fetchCharacter(params?.targetDadChar ?? Save.instance.stageDadChar, true); + if (dad != null) dad.characterType = CharacterType.DAD; + var bf = CharacterDataParser.fetchCharacter(params?.targetBfChar ?? Save.instance.stageBoyfriendChar, true); + if (bf != null) bf.characterType = CharacterType.BF; - bf.flipX = !bf.getDataFlipX(); - gf.flipX = gf.getDataFlipX(); - dad.flipX = dad.getDataFlipX(); + if (bf != null) bf.flipX = !bf.getDataFlipX(); + if (gf != null) gf.flipX = gf.getDataFlipX(); + if (dad != null) dad.flipX = dad.getDataFlipX(); - gf.updateHitbox(); - dad.updateHitbox(); - bf.updateHitbox(); + gf?.updateHitbox(); + dad?.updateHitbox(); + bf?.updateHitbox(); // Only one character per group allowed. charGroups = [ @@ -351,12 +354,21 @@ class StageEditorState extends UIState CharacterType.DAD => new FlxTypedGroup(1) ]; - gf.x = charPos[CharacterType.GF][0] - gf.characterOrigin.x + gf.globalOffsets[0]; - gf.y = charPos[CharacterType.GF][1] - gf.characterOrigin.y + gf.globalOffsets[1]; - dad.x = charPos[CharacterType.DAD][0] - dad.characterOrigin.x + dad.globalOffsets[0]; - dad.y = charPos[CharacterType.DAD][1] - dad.characterOrigin.y + dad.globalOffsets[1]; - bf.x = charPos[CharacterType.BF][0] - bf.characterOrigin.x + bf.globalOffsets[0]; - bf.y = charPos[CharacterType.BF][1] - bf.characterOrigin.y + bf.globalOffsets[1]; + if (gf != null) + { + gf.x = charPos[CharacterType.GF][0] - gf.characterOrigin.x + gf.globalOffsets[0]; + gf.y = charPos[CharacterType.GF][1] - gf.characterOrigin.y + gf.globalOffsets[1]; + } + if (dad != null) + { + dad.x = charPos[CharacterType.DAD][0] - dad.characterOrigin.x + dad.globalOffsets[0]; + dad.y = charPos[CharacterType.DAD][1] - dad.characterOrigin.y + dad.globalOffsets[1]; + } + if (bf != null) + { + bf.x = charPos[CharacterType.BF][0] - bf.characterOrigin.x + bf.globalOffsets[0]; + bf.y = charPos[CharacterType.BF][1] - bf.characterOrigin.y + bf.globalOffsets[1]; + } selectedChar = bf; @@ -541,7 +553,7 @@ class StageEditorState extends UIState if (conductorInUse.currentBeat % 2 == 0) { for (char in getCharacters()) - char.dance(true); + char?.dance(true); } for (asset in spriteArray) @@ -588,7 +600,10 @@ class StageEditorState extends UIState if (testingMode) { for (char in getCharacters()) + { + if (char == null) continue; char.shader = null; + } // spriteMarker.visible = camMarker.visible = false; findObjDialog.hideDialog(DialogButton.CANCEL); @@ -601,11 +616,15 @@ class StageEditorState extends UIState if (curTestChar >= getCharacters().length) curTestChar = 0; - bottomBarSelectText.text = Std.string(getCharacters()[curTestChar].characterType); + var text = Std.string(getCharacters()[curTestChar]?.characterType); + bottomBarSelectText.text = (text == 'null') ? 'None' : text; var char = getCharacters()[curTestChar]; - camFollow.x = char.cameraFocusPoint.x + charCamOffsets.get(char.characterType)[0]; - camFollow.y = char.cameraFocusPoint.y + charCamOffsets.get(char.characterType)[1]; + if (char != null) + { + camFollow.x = char.cameraFocusPoint.x + charCamOffsets.get(char.characterType)[0]; + camFollow.y = char.cameraFocusPoint.y + charCamOffsets.get(char.characterType)[1]; + } // EXIT if (FlxG.keys.justPressed.ENTER) // so we dont accidentally get stuck (happened to me once, terrible experience) @@ -744,14 +763,18 @@ class StageEditorState extends UIState arrowMovement(selectedSprite); for (char in getCharacters()) + { + if (char == null) continue; char.shader = null; + } } else { - selectedChar.shader = null; + if (selectedChar != null) selectedChar.shader = null; for (char in getCharacters()) { + if (char == null) continue; if (char != selectedChar) char.shader = charDeselectShader; if (char != null && checkCharOverlaps(char)) // flxg.mouse.overlaps crashes the game @@ -864,6 +887,7 @@ class StageEditorState extends UIState for (i in 0...getCharacters().length) { var char = getCharacters()[i]; + if (char == null) continue; var type = char.characterType; charPos.set(type, [ @@ -894,6 +918,7 @@ class StageEditorState extends UIState // it comes from some flxobject/polymod error apparently and I have no idea why function checkCharOverlaps(char:BaseCharacter) { + if (char == null) return false; var mouseX = FlxG.mouse.x >= char.x && FlxG.mouse.x <= char.x + char.width; var mouseY = FlxG.mouse.y >= char.y && FlxG.mouse.y <= char.y + char.height; @@ -1307,7 +1332,6 @@ class StageEditorState extends UIState index++; if (index >= chars.length) index = 0; - selectedChar = chars[index]; } else @@ -1623,4 +1647,18 @@ typedef StageEditorParams = * If non-null, load this stage immediately instead of the welcome screen. */ var ?targetStageId:String; + /** + * If non-null, load this character as Boyfriend. + */ + var ?targetBfChar:String; + + /** + * If non-null, load this character as Girlfriend. + */ + var ?targetGfChar:String; + + /** + * If non-null, load this character as Dad. + */ + var ?targetDadChar:String; }; diff --git a/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx index cb2a3d869..306fe1102 100644 --- a/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx +++ b/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx @@ -241,6 +241,7 @@ class StageDataHandler for (char in chars) { var charData:StageDataCharacter = null; + if (char == null) continue; switch (char.characterType) { diff --git a/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx index 6ee9669f1..45acec6cd 100644 --- a/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx +++ b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx @@ -22,6 +22,7 @@ class UndoRedoHandler for (char in state.getCharacters()) { + if (char == null) continue; if (char.characterType == type) state.selectedChar = char; } diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx index 65be7da78..e5b65f93a 100644 --- a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorCharacterToolbox.hx @@ -47,29 +47,35 @@ class StageEditorCharacterToolbox extends StageEditorDefaultToolbox charZIdx.max = StageEditorState.MAX_Z_INDEX; charZIdx.onChange = function(_) { + if (state.selectedChar == null) return; state.charGroups[state.selectedChar.characterType].zIndex = Std.int(charZIdx.pos); state.sortAssets(); } charCamX.onChange = charCamY.onChange = function(_) { + if (state.selectedChar == null) return; state.charCamOffsets[state.selectedChar.characterType] = [charCamX.pos, charCamY.pos]; state.updateMarkerPos(); } charScale.onChange = function(_) { + if (state.selectedChar == null) return; state.selectedChar.setScale(state.selectedChar.getBaseScale() * charScale.pos); repositionCharacter(); } charAlpha.onChange = function(_) { + if (state.selectedChar == null) return; state.selectedChar.alpha = charAlpha.pos; } charAngle.onChange = function(_) { + if (state.selectedChar == null) return; state.selectedChar.angle = charAngle.pos; } charScrollX.onChange = charScrollY.onChange = function(_) { + if (state.selectedChar == null) return; state.selectedChar.scrollFactor.set(charScrollX.pos, charScrollY.pos); } @@ -91,7 +97,7 @@ class StageEditorCharacterToolbox extends StageEditorDefaultToolbox override public function refresh() { - var name = stageEditorState.selectedChar.characterType; + var name = stageEditorState.selectedChar?.characterType; var curChar = stageEditorState.selectedChar; charPosX.step = charPosY.step = stageEditorState.moveStep; @@ -100,20 +106,23 @@ class StageEditorCharacterToolbox extends StageEditorDefaultToolbox // Always update the displays, since selectedChar is never null. - if (charPosX.pos != stageEditorState.charPos[name][0]) charPosX.pos = stageEditorState.charPos[name][0]; - if (charPosY.pos != stageEditorState.charPos[name][1]) charPosY.pos = stageEditorState.charPos[name][1]; - if (charZIdx.pos != stageEditorState.charGroups[name].zIndex) charZIdx.pos = stageEditorState.charGroups[name].zIndex; - if (charCamX.pos != stageEditorState.charCamOffsets[name][0]) charCamX.pos = stageEditorState.charCamOffsets[name][0]; - if (charCamY.pos != stageEditorState.charCamOffsets[name][1]) charCamY.pos = stageEditorState.charCamOffsets[name][1]; - if (charScale.pos != curChar.scale.x / curChar.getBaseScale()) charScale.pos = curChar.scale.x / curChar.getBaseScale(); - if (charAlpha.pos != curChar.alpha) charAlpha.pos = curChar.alpha; - if (charAngle.pos != curChar.angle) charAngle.pos = curChar.angle; - if (charScrollX.pos != curChar.scrollFactor.x) charScrollX.pos = curChar.scrollFactor.x; - if (charScrollY.pos != curChar.scrollFactor.y) charScrollY.pos = curChar.scrollFactor.y; + if (curChar != null) + { + if (charPosX.pos != stageEditorState.charPos[name][0]) charPosX.pos = stageEditorState.charPos[name][0]; + if (charPosY.pos != stageEditorState.charPos[name][1]) charPosY.pos = stageEditorState.charPos[name][1]; + if (charZIdx.pos != stageEditorState.charGroups[name].zIndex) charZIdx.pos = stageEditorState.charGroups[name].zIndex; + if (charCamX.pos != stageEditorState.charCamOffsets[name][0]) charCamX.pos = stageEditorState.charCamOffsets[name][0]; + if (charCamY.pos != stageEditorState.charCamOffsets[name][1]) charCamY.pos = stageEditorState.charCamOffsets[name][1]; + if (charScale.pos != curChar.scale.x / curChar.getBaseScale()) charScale.pos = curChar.scale.x / curChar.getBaseScale(); + if (charAlpha.pos != curChar.alpha) charAlpha.pos = curChar.alpha; + if (charAngle.pos != curChar.angle) charAngle.pos = curChar.angle; + if (charScrollX.pos != curChar.scrollFactor.x) charScrollX.pos = curChar.scrollFactor.x; + if (charScrollY.pos != curChar.scrollFactor.y) charScrollY.pos = curChar.scrollFactor.y; + } var prevText = charType.text; - var charData = CharacterDataParser.fetchCharacterData(curChar.characterId); - charType.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(curChar.characterId)); + var charData = CharacterDataParser.fetchCharacterData(curChar?.characterId); + charType.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(curChar?.characterId)); charType.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}'); if (prevText != charType.text) Screen.instance.removeComponent(charMenu); @@ -121,6 +130,7 @@ class StageEditorCharacterToolbox extends StageEditorDefaultToolbox public function repositionCharacter() { + if (stageEditorState.selectedChar == null) return; stageEditorState.selectedChar.x = charPosX.pos - stageEditorState.selectedChar.characterOrigin.x + stageEditorState.selectedChar.globalOffsets[0]; stageEditorState.selectedChar.y = charPosY.pos - stageEditorState.selectedChar.characterOrigin.y + stageEditorState.selectedChar.globalOffsets[1]; @@ -166,7 +176,7 @@ class StageEditorCharacterMenu extends Menu // copied from chart editor charButton.padding = 8; charButton.iconPosition = "top"; - if (charId == state.selectedChar.characterId) + if (charId == state.selectedChar?.characterId) { // Scroll to the character if it is already selected. charSelectScroll.hscrollPos = Math.floor(charIndex / 5) * 80; @@ -180,8 +190,25 @@ class StageEditorCharacterMenu extends Menu // copied from chart editor charButton.text = charData.name.length > LIMIT ? '${charData.name.substr(0, LIMIT)}.' : '${charData.name}'; charButton.onClick = _ -> { - var type = state.selectedChar.characterType; - if (state.selectedChar.characterId == charId) return; // saves on memory + var type = state.selectedChar?.characterType; + if (type == null) + { + // Hack - choose the type based of the first null character in getCharacters, + // since this is how the click on the char type gets the selected char anyway, we can be sure this is the right type + var chars = state.getCharacters(); + var index = chars.indexOf(state.selectedChar); + + switch (index) + { + case 0: + type = CharacterType.GF; + case 1: + type = CharacterType.DAD; + case 2: + type = CharacterType.BF; + } + } + if (state.selectedChar?.characterId == charId) return; // saves on memory var group = state.charGroups[type]; group.killMembers(); diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 18e9bacc7..6b8fa6f86 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -56,7 +56,13 @@ import funkin.util.MathUtil; import funkin.util.SortUtil; import openfl.display.BlendMode; import funkin.ui.freeplay.DifficultyDot; +import funkin.data.freeplay.style.FreeplayStyleRegistry; +#if FEATURE_CHART_EDITOR import funkin.ui.debug.charting.ChartEditorState; +#end +#if FEATURE_STAGE_EDITOR +import funkin.ui.debug.stageeditor.StageEditorState; +#end #if FEATURE_DISCORD_RPC import funkin.api.discord.DiscordClient; #end @@ -1839,52 +1845,121 @@ class FreeplayState extends MusicBeatSubState { #if FEATURE_CHART_EDITOR if (!controls.active) return; - if (!controls.DEBUG_CHART) return; - - controls.active = false; - - var targetSongID = currentCapsule?.freeplayData?.data.id ?? 'unknown'; - if (targetSongID == 'unknown') + if (controls.DEBUG_CHART) { - letterSort.inputEnabled = false; + controls.active = false; - var availableSongCapsules:Array = grpCapsules.members.filter(function(cap:SongMenuItem) { - // Dead capsules are ones which were removed from the list when changing filters. - return cap.alive && cap.freeplayData != null; - }); - - trace('Available songs: ${availableSongCapsules.map(function(cap) { - return cap?.freeplayData?.data.songName; - })}'); - - if (availableSongCapsules.length == 0) + var targetSongID = currentCapsule?.freeplayData?.data.id ?? 'unknown'; + if (targetSongID == 'unknown') { - trace('No songs available!'); + letterSort.inputEnabled = false; + + var availableSongCapsules:Array = grpCapsules.members.filter(function(cap:SongMenuItem) { + // Dead capsules are ones which were removed from the list when changing filters. + return cap.alive && cap.freeplayData != null; + }); + + trace('Available songs: ${availableSongCapsules.map(function(cap) { + return cap?.freeplayData?.data.songName; + })}'); + + if (availableSongCapsules.length == 0) + { + trace('No songs available!'); + controls.active = true; + letterSort.inputEnabled = true; + FunkinSound.playOnce(Paths.sound('cancelMenu')); + return; + } + + var targetSong:SongMenuItem = FlxG.random.getObject(availableSongCapsules); + + // Seeing if I can do an animation... + curSelected = grpCapsules.members.indexOf(targetSong); + changeSelection(0); + targetSongID = currentCapsule?.freeplayData?.data.id ?? 'unknown'; + } + // Play the confirm animation so the user knows they actually did something. + FunkinSound.playOnce(Paths.sound('confirmMenu')); + // if (dj != null) dj.confirm(); + dj?.onConfirm(); + new FlxTimer().start(styleData?.getStartDelay(), function(tmr:FlxTimer) { + FlxG.switchState(() -> new ChartEditorState( + { + targetSongId: targetSongID, + targetSongDifficulty: currentDifficulty, + targetSongVariation: currentVariation, + })); + }); + return; + } + #end + #if FEATURE_STAGE_EDITOR + if (controls.DEBUG_STAGE) + { + controls.active = false; + + var targetSongID = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? 'unknown'; + if (targetSongID == 'unknown') + { + trace('CHART RANDOM SONG'); + letterSort.inputEnabled = false; + + var availableSongCapsules:Array = grpCapsules.members.filter(function(cap:SongMenuItem) { + // Dead capsules are ones which were removed from the list when changing filters. + return cap.alive && cap.freeplayData != null; + }); + + trace('Available songs: ${availableSongCapsules.map(function(cap) { + return cap?.freeplayData?.data.songName; + })}'); + + if (availableSongCapsules.length == 0) + { + trace('No songs available!'); + controls.active = true; + letterSort.inputEnabled = true; + FunkinSound.playOnce(Paths.sound('cancelMenu')); + return; + } + + var targetSong:SongMenuItem = FlxG.random.getObject(availableSongCapsules); + + // Seeing if I can do an animation... + curSelected = grpCapsules.members.indexOf(targetSong); + changeSelection(0); + targetSongID = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? 'unknown'; + } + + var targetSongNullable:Null = SongRegistry.instance.fetchEntry(targetSongID); + if (targetSongNullable == null) + { + FlxG.log.warn('WARN: could not find song with id (${targetSongID})'); + controls.active = true; + letterSort.inputEnabled = true; + return; + } + var targetSong:Song = targetSongNullable; + var targetVariation:Null = currentVariation; + + var targetDifficulty:Null = targetSong.getDifficulty(currentDifficulty, currentVariation); + if (targetDifficulty == null) + { + FlxG.log.warn('WARN: could not find difficulty with id (${currentDifficulty})'); controls.active = true; letterSort.inputEnabled = true; - FunkinSound.playOnce(Paths.sound('cancelMenu')); return; } - var targetSong:SongMenuItem = FlxG.random.getObject(availableSongCapsules); - - // Seeing if I can do an animation... - curSelected = grpCapsules.members.indexOf(targetSong); - changeSelection(0); - targetSongID = currentCapsule?.freeplayData?.data.id ?? 'unknown'; - } - // Play the confirm animation so the user knows they actually did something. - FunkinSound.playOnce(Paths.sound('confirmMenu')); - // if (dj != null) dj.confirm(); - dj?.onConfirm(); - new FlxTimer().start(styleData?.getStartDelay(), function(tmr:FlxTimer) { - FlxG.switchState(() -> new ChartEditorState( + FlxG.switchState(() -> new StageEditorState( { - targetSongId: targetSongID, - targetSongDifficulty: currentDifficulty, - targetSongVariation: currentVariation, + targetStageId: targetDifficulty.stage, + targetBfChar: targetDifficulty.characters.player, + targetGfChar: targetDifficulty.characters.girlfriend, + targetDadChar: targetDifficulty.characters.opponent })); - }); + return; + } #end }