From 34d13c61de236682535bab4b04bfca5c6744bf1d Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sun, 24 Sep 2023 00:53:53 -0400 Subject: [PATCH 1/2] Cleanup character unlock impl and queue it for after Story Mode is completed. --- source/funkin/InitState.hx | 2 +- .../data/freeplay/player/PlayerRegistry.hx | 24 +++- source/funkin/play/ResultState.hx | 74 +++++++--- source/funkin/play/components/HealthIcon.hx | 32 +++-- .../ui/charSelect/CharacterUnlockState.hx | 128 ++++++++++++++++++ source/funkin/ui/mainmenu/MainMenuState.hx | 14 +- 6 files changed, 240 insertions(+), 34 deletions(-) create mode 100644 source/funkin/ui/charSelect/CharacterUnlockState.hx diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index a203a8d77..f71de00f4 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -225,7 +225,7 @@ class InitState extends FlxState // -DRESULTS FlxG.switchState(() -> new funkin.play.ResultState( { - storyMode: false, + storyMode: true, title: "Cum Song Erect by Kawai Sprite", songId: "cum", characterId: "pico-playable", diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx index c0a15ed1c..76b1c25c1 100644 --- a/source/funkin/data/freeplay/player/PlayerRegistry.hx +++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx @@ -71,7 +71,7 @@ class PlayerRegistry extends BaseRegistry public function hasNewCharacter():Bool { - var characters = Save.instance.charactersSeen.clone(); + var charactersSeen = Save.instance.charactersSeen.clone(); for (charId in listEntryIds()) { @@ -79,7 +79,7 @@ class PlayerRegistry extends BaseRegistry if (player == null) continue; if (!player.isUnlocked()) continue; - if (characters.contains(charId)) continue; + if (charactersSeen.contains(charId)) continue; // This character is unlocked but we haven't seen them in Freeplay yet. return true; @@ -89,6 +89,26 @@ class PlayerRegistry extends BaseRegistry return false; } + public function listNewCharacters():Array + { + var charactersSeen = Save.instance.charactersSeen.clone(); + var result = []; + + for (charId in listEntryIds()) + { + var player = fetchEntry(charId); + if (player == null) continue; + + if (!player.isUnlocked()) continue; + if (charactersSeen.contains(charId)) continue; + + // This character is unlocked but we haven't seen them in Freeplay yet. + result.push(charId); + } + + return result; + } + /** * Get the playable character associated with a given stage character. * @param characterId The stage character ID. diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index ec6069ad4..2878248e2 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -731,42 +731,59 @@ class ResultState extends MusicBeatSubState } }); } + + // Determining the target state(s) to go to. + // Default to main menu because that's better than `null`. + var targetState:flixel.FlxState = new funkin.ui.mainmenu.MainMenuState(); + var shouldTween = false; + if (params.storyMode) { - openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker))); + if (PlayerRegistry.instance.hasNewCharacter()) + { + targetState = new StoryMenuState(null); + + var newCharacters = PlayerRegistry.instance.listNewCharacters(); + + for (charId in newCharacters) + { + shouldTween = true; + // This works recursively, ehe! + targetState = new funkin.ui.charSelect.CharacterUnlockState(charId, targetState); + } + } + else + { + targetState = new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)); + } } else { - var rigged:Bool = true; - if (rank > Scoring.calculateRank(params?.prevScoreData)) // if (rigged) + if (rank > Scoring.calculateRank(params?.prevScoreData)) { trace('THE RANK IS Higher.....'); - FlxTween.tween(rankBg, {alpha: 1}, 0.5, + shouldTween = true; + targetState = FreeplayState.build( { - ease: FlxEase.expoOut, - onComplete: function(_) { - FlxG.switchState(FreeplayState.build( + { + character: playerCharacterId ?? "bf", + fromResults: { - { - character: playerCharacterId ?? "bf", - fromResults: - { - oldRank: Scoring.calculateRank(params?.prevScoreData), - newRank: rank, - songId: params.songId, - difficultyId: params.difficultyId, - playRankAnim: true - } - } - })); + oldRank: Scoring.calculateRank(params?.prevScoreData), + newRank: rank, + songId: params.songId, + difficultyId: params.difficultyId, + playRankAnim: true + } } }); } else { trace('rank is lower...... and/or equal'); - openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build( + + targetState = new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build( { { fromResults: @@ -778,9 +795,24 @@ class ResultState extends MusicBeatSubState difficultyId: params.difficultyId } } - }, sticker))); + }, sticker)); } } + + if (shouldTween) + { + FlxTween.tween(rankBg, {alpha: 1}, 0.5, + { + ease: FlxEase.expoOut, + onComplete: function(_) { + FlxG.switchState(targetState); + } + }); + } + else + { + FlxG.switchState(targetState); + } } super.update(elapsed); diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx index 0e24d73fb..c11850b2a 100644 --- a/source/funkin/play/components/HealthIcon.hx +++ b/source/funkin/play/components/HealthIcon.hx @@ -49,7 +49,7 @@ class HealthIcon extends FunkinSprite * this value allows you to set a relative scale for the icon. * @default 1x scale = 150px width and height. */ - public var size:FlxPoint = new FlxPoint(1, 1); + public var size:FlxPoint; /** * Apply the "bop" animation once every X steps. @@ -120,11 +120,15 @@ class HealthIcon extends FunkinSprite { super(0, 0); this.playerId = playerId; + this.size = new FlxCallbackPoint(onSetSize); this.scrollFactor.set(); - + size.set(1.0, 1.0); this.characterId = char; + } - initTargetSize(); + function onSetSize(value:FlxPoint):Void + { + snapToTargetSize(); } function set_characterId(value:Null):Null @@ -243,6 +247,22 @@ class HealthIcon extends FunkinSprite this.updateHitbox(); } + /* + * Immediately snap the health icon to its target size without lerping. + */ + public function snapToTargetSize():Void + { + if (this.width > this.height) + { + setGraphicSize(Std.int(HEALTH_ICON_SIZE * this.size.x), 0); + } + else + { + setGraphicSize(0, Std.int(HEALTH_ICON_SIZE * this.size.y)); + } + updateHitbox(); + } + /** * Update the position (and status) of the health icon. */ @@ -301,12 +321,6 @@ class HealthIcon extends FunkinSprite } } - inline function initTargetSize():Void - { - setGraphicSize(HEALTH_ICON_SIZE); - updateHitbox(); - } - function updateHealthIcon(health:Float):Void { // We want to efficiently handle animation playback diff --git a/source/funkin/ui/charSelect/CharacterUnlockState.hx b/source/funkin/ui/charSelect/CharacterUnlockState.hx new file mode 100644 index 000000000..25cd1baf0 --- /dev/null +++ b/source/funkin/ui/charSelect/CharacterUnlockState.hx @@ -0,0 +1,128 @@ +package funkin.ui.charSelect; + +import flixel.FlxSprite; +import flixel.FlxState; +import flixel.group.FlxSpriteGroup; +import flixel.text.FlxText; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxColor; +import funkin.play.character.CharacterData; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.play.components.HealthIcon; +import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.data.freeplay.player.PlayerData; +import funkin.data.freeplay.player.PlayerRegistry; +import funkin.ui.mainmenu.MainMenuState; + +using flixel.util.FlxSpriteUtil; + +/** + * When you want the player to unlock a character, call `CharacterUnlockState.unlock(characterName)`. + * It handles both the act of unlocking the character and displaying the dialog. + */ +class CharacterUnlockState extends MusicBeatState +{ + public var targetCharacterId:String = ""; + public var targetCharacterData:Null; + + var nextState:FlxState; + + static final DIALOG_BG_COLOR:FlxColor = 0xFF000000; // Iconic + static final DIALOG_COLOR:FlxColor = 0xFF4344F6; // Iconic + static final DIALOG_FONT_COLOR:FlxColor = 0xFFFFFFFF; // Iconic + + var busy:Bool = false; + + public function new(targetPlayableCharacter:String, ?nextState:FlxState) + { + super(); + + this.targetCharacterId = targetPlayableCharacter; + this.targetCharacterData = PlayerRegistry.instance.fetchEntry(targetCharacterId); + this.nextState = nextState == null ? new MainMenuState() : nextState; + } + + override function create():Void + { + super.create(); + + handleMusic(); + + bgColor = DIALOG_BG_COLOR; + + var dialogContainer:FlxSpriteGroup = new FlxSpriteGroup(); + add(dialogContainer); + + // Build the graphic for the text... + var charName:String = targetCharacterData != null ? targetCharacterData.getName() : targetCharacterId.toTitleCase(); + // var dialogText:FlxText = new FlxText(0, 0, 0, 'You can now play as $charName.\n\nCheck it out in Freeplay!'); + var dialogText:FlxText = new FlxText(0, 0, 0, 'You can now play as $charName.'); + dialogText.setFormat("VCR OSD Mono", 32, DIALOG_FONT_COLOR, LEFT); + + // THEN we can size the dialog to match... + var dialogBG:FlxSprite = new FlxSprite(0, 0); + dialogBG.makeGraphic(Std.int(dialogText.width + 32), Std.int(dialogText.height + 32), FlxColor.TRANSPARENT); + dialogBG.drawRoundRect(0, 0, dialogBG.width, dialogBG.height, 16, 16, DIALOG_COLOR); + dialogContainer.add(dialogBG); + + dialogBG.screenCenter(XY); + + // THEN we can position the text inside that. + dialogText.x = dialogBG.x + 16; + dialogText.y = dialogBG.y + 16; + dialogContainer.add(dialogText); + + // HealthIcon handles getting the right frames for us, + // but it has a bunch of overhead in it that makes it gross to work with outside the health bar. + var healthIconCharacterId = targetCharacterData.getOwnedCharacterIds()[0]; + var baseCharacter = CharacterDataParser.fetchCharacter(healthIconCharacterId); + var healthIcon:HealthIcon = new HealthIcon(healthIconCharacterId); + @:privateAccess + healthIcon.configure(baseCharacter._data.healthIcon); + healthIcon.autoUpdate = false; + healthIcon.bopEvery = 0; // You can increase this number later once the animation is done. + healthIcon.size.set(0.5, 0.5); + healthIcon.x = dialogBG.x + 390; + healthIcon.y = dialogBG.y + 6; + healthIcon.flipX = true; + healthIcon.snapToTargetSize(); + dialogContainer.add(healthIcon); + + dialogContainer.scale.set(0, 0); + FlxTween.num(0.0, 1.0, 0.75, + { + ease: FlxEase.elasticOut, + }, function(curScale) { + dialogContainer.scale.set(curScale, curScale); + healthIcon.size.set(0.5 * curScale, 0.5 * curScale); + }); + + // performUnlock(); + } + + function handleMusic():Void + { + FlxG.sound.music.stop(); + FlxG.sound.play(Paths.sound('confirmMenu')); + } + + override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (controls.ACCEPT || controls.BACK && !busy) + { + busy = true; + startClose(); + } + } + + function startClose():Void + { + // Fade to black, then switch state. + FlxG.camera.fade(FlxColor.BLACK, 0.75, false, () -> { + FlxG.switchState(nextState); + }); + } +} diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index d2a9f046a..19dc0d687 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -344,8 +344,8 @@ class MainMenuState extends MusicBeatState } } - #if FEATURE_DEBUG_FUNCTIONS // Open the debug menu, defaults to ` / ~ + // This includes stuff like the Chart Editor, so it should be present on all builds. if (controls.DEBUG_MENU) { persistentUpdate = false; @@ -353,6 +353,18 @@ class MainMenuState extends MusicBeatState FlxG.state.openSubState(new DebugMenuSubState()); } + #if FEATURE_DEBUG_FUNCTIONS + // Ctrl+Alt+Shift+P = Character Unlock screen + // Ctrl+Alt+Shift+W = Meet requirements for Pico Unlock + // Ctrl+Alt+Shift+L = Revoke requirements for Pico Unlock + // Ctrl+Alt+Shift+R = Score/Rank conflict test + // Ctrl+Alt+Shift+E = Dump save data + + if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.P) + { + FlxG.switchState(() -> new funkin.ui.charSelect.CharacterUnlockState('pico')); + } + if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.W) { FunkinSound.playOnce(Paths.sound('confirmMenu')); From 89e1cb82588f80a0f6f9af6ab820365d68a35938 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Fri, 6 Sep 2024 19:44:33 -0400 Subject: [PATCH 2/2] small null fix for if we're not playing music --- source/funkin/ui/charSelect/CharacterUnlockState.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/ui/charSelect/CharacterUnlockState.hx b/source/funkin/ui/charSelect/CharacterUnlockState.hx index 25cd1baf0..b32a35145 100644 --- a/source/funkin/ui/charSelect/CharacterUnlockState.hx +++ b/source/funkin/ui/charSelect/CharacterUnlockState.hx @@ -103,7 +103,7 @@ class CharacterUnlockState extends MusicBeatState function handleMusic():Void { - FlxG.sound.music.stop(); + FlxG.sound.music?.stop(); FlxG.sound.play(Paths.sound('confirmMenu')); }