package funkin.play; import funkin.util.MathUtil; import funkin.ui.story.StoryMenuState; import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.FlxSprite; import funkin.graphics.FunkinSprite; import flixel.effects.FlxFlicker; import flixel.graphics.frames.FlxBitmapFont; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxPoint; import funkin.ui.MusicBeatSubState; import flixel.math.FlxRect; import flixel.text.FlxBitmapText; import funkin.ui.freeplay.FreeplayScore; import flixel.text.FlxText; import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.freeplay.player.PlayerData; import funkin.ui.freeplay.charselect.PlayableCharacter; import flixel.util.FlxColor; import flixel.tweens.FlxEase; import funkin.graphics.FunkinCamera; import funkin.ui.freeplay.FreeplayState; import flixel.tweens.FlxTween; import flixel.addons.display.FlxBackdrop; import funkin.audio.FunkinSound; import flixel.util.FlxGradient; import flixel.util.FlxTimer; import funkin.save.Save; import funkin.play.scoring.Scoring; import funkin.save.Save.SaveScoreData; import funkin.graphics.shaders.LeftMaskShader; import funkin.play.components.TallyCounter; import funkin.play.components.ClearPercentCounter; /** * The state for the results screen after a song or week is finished. */ @:nullSafety class ResultState extends MusicBeatSubState { final params:ResultsStateParams; final rank:ScoringRank; final songName:FlxBitmapText; final difficulty:FlxSprite; final clearPercentSmall:ClearPercentCounter; final maskShaderSongName:LeftMaskShader = new LeftMaskShader(); final maskShaderDifficulty:LeftMaskShader = new LeftMaskShader(); final resultsAnim:FunkinSprite; final ratingsPopin:FunkinSprite; final scorePopin:FunkinSprite; final bgFlash:FlxSprite; final highscoreNew:FlxSprite; final score:ResultScore; var characterAtlasAnimations:Array< { sprite:FlxAtlasSprite, delay:Float, forceLoop:Bool }> = []; var characterSparrowAnimations:Array< { sprite:FunkinSprite, delay:Float }> = []; var playerCharacterId:Null; var rankBg:FunkinSprite; final cameraBG:FunkinCamera; final cameraScroll:FunkinCamera; final cameraEverything:FunkinCamera; public function new(params:ResultsStateParams) { super(); this.params = params; rank = Scoring.calculateRank(params.scoreData) ?? SHIT; cameraBG = new FunkinCamera('resultsBG', 0, 0, FlxG.width, FlxG.height); cameraScroll = new FunkinCamera('resultsScroll', 0, 0, FlxG.width, FlxG.height); cameraEverything = new FunkinCamera('resultsEverything', 0, 0, FlxG.width, FlxG.height); // We build a lot of this stuff in the constructor, then place it in create(). // This prevents having to do `null` checks everywhere. var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890"; songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62))); songName.text = params.title; songName.letterSpacing = -15; songName.angle = -4.4; songName.zIndex = 1000; difficulty = new FlxSprite(555); difficulty.zIndex = 1000; clearPercentSmall = new ClearPercentCounter(FlxG.width / 2 + 300, FlxG.height / 2 - 100, 100, true); clearPercentSmall.zIndex = 1000; clearPercentSmall.visible = false; bgFlash = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFF1A6, 0xFFFFF1BE], 90); resultsAnim = FunkinSprite.createSparrow(-200, -10, "resultScreen/results"); ratingsPopin = FunkinSprite.createSparrow(-135, 135, "resultScreen/ratingsPopin"); scorePopin = FunkinSprite.createSparrow(-180, 515, "resultScreen/scorePopin"); highscoreNew = new FlxSprite(44, 557); score = new ResultScore(35, 305, 10, params.scoreData.score); rankBg = new FunkinSprite(0, 0); } override function create():Void { if (FlxG.sound.music != null) FlxG.sound.music.stop(); // We need multiple cameras so we can put one at an angle. cameraScroll.angle = -3.8; cameraBG.bgColor = FlxColor.MAGENTA; cameraScroll.bgColor = FlxColor.TRANSPARENT; cameraEverything.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(cameraBG, false); FlxG.cameras.add(cameraScroll, false); FlxG.cameras.add(cameraEverything, false); FlxG.cameras.setDefaultDrawTarget(cameraEverything, true); this.camera = cameraEverything; // Reset the camera zoom on the results screen. FlxG.camera.zoom = 1.0; var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90); bg.scrollFactor.set(); bg.zIndex = 10; bg.cameras = [cameraBG]; add(bg); bgFlash.scrollFactor.set(); bgFlash.visible = false; bgFlash.zIndex = 20; // bgFlash.cameras = [cameraBG]; add(bgFlash); // The sound system which falls into place behind the score text. Plays every time! var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem'); soundSystem.animation.addByPrefix("idle", "sound system", 24, false); soundSystem.visible = false; new FlxTimer().start(8 / 24, _ -> { soundSystem.animation.play("idle"); soundSystem.visible = true; }); soundSystem.zIndex = 1100; add(soundSystem); // Fetch playable character data. Default to BF on the results screen if we can't find it. playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId); var playerCharacter:Null = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf'); trace('Got playable character: ${playerCharacter?.getName()}'); // Query JSON data based on the rank, then use that to build the animation(s) the player sees. var playerAnimationDatas:Array = playerCharacter != null ? playerCharacter.getResultsAnimationDatas(rank) : []; for (animData in playerAnimationDatas) { if (animData == null) continue; var animPath:String = Paths.stripLibrary(animData.assetPath); var animLibrary:String = Paths.getLibrary(animData.assetPath); var offsets = animData.offsets ?? [0, 0]; switch (animData.renderType) { case 'animateatlas': var animation:FlxAtlasSprite = new FlxAtlasSprite(offsets[0], offsets[1], Paths.animateAtlas(animPath, animLibrary)); animation.zIndex = animData.zIndex ?? 500; animation.scale.set(animData.scale ?? 1.0, animData.scale ?? 1.0); if (!(animData.looped ?? true)) { // Animation is not looped. animation.onAnimationComplete.add((_name:String) -> { if (animation != null) { animation.anim.pause(); } }); } else if (animData.loopFrameLabel != null) { animation.onAnimationComplete.add((_name:String) -> { if (animation != null) { animation.playAnimation(animData.loopFrameLabel ?? ''); // unpauses this anim, since it's on PlayOnce! } }); } else if (animData.loopFrame != null) { animation.onAnimationComplete.add((_name:String) -> { if (animation != null) { animation.anim.curFrame = animData.loopFrame ?? 0; animation.anim.play(); // unpauses this anim, since it's on PlayOnce! } }); } // Hide until ready to play. animation.visible = false; // Queue to play. characterAtlasAnimations.push( { sprite: animation, delay: animData.delay ?? 0.0, forceLoop: (animData.loopFrame ?? -1) == 0 }); // Add to the scene. add(animation); case 'sparrow': var animation:FunkinSprite = FunkinSprite.createSparrow(offsets[0], offsets[1], animPath); animation.animation.addByPrefix('idle', '', 24, false, false, false); if (animData.loopFrame != null) { animation.animation.finishCallback = (_name:String) -> { if (animation != null) { animation.animation.play('idle', true, false, animData.loopFrame ?? 0); } } } // Hide until ready to play. animation.visible = false; // Queue to play. characterSparrowAnimations.push( { sprite: animation, delay: animData.delay ?? 0.0 }); // Add to the scene. add(animation); } } var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}'; difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr)); add(difficulty); add(songName); var angleRad = songName.angle * Math.PI / 180; speedOfTween.x = -1.0 * Math.cos(angleRad); speedOfTween.y = -1.0 * Math.sin(angleRad); timerThenSongName(1.0, false); songName.shader = maskShaderSongName; difficulty.shader = maskShaderDifficulty; // maskShaderSongName.swagMaskX = difficulty.x - 15; maskShaderDifficulty.swagMaskX = difficulty.x - 15; var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack")); blackTopBar.y = -blackTopBar.height; FlxTween.tween(blackTopBar, {y: 0}, 7 / 24, {ease: FlxEase.quartOut, startDelay: 3 / 24}); blackTopBar.zIndex = 1010; add(blackTopBar); resultsAnim.animation.addByPrefix("result", "results instance 1", 24, false); resultsAnim.visible = false; resultsAnim.zIndex = 1200; add(resultsAnim); new FlxTimer().start(6 / 24, _ -> { resultsAnim.visible = true; resultsAnim.animation.play("result"); }); ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false); ratingsPopin.visible = false; ratingsPopin.zIndex = 1200; add(ratingsPopin); new FlxTimer().start(21 / 24, _ -> { ratingsPopin.visible = true; ratingsPopin.animation.play("idle"); }); scorePopin.animation.addByPrefix("score", "tally score", 24, false); scorePopin.visible = false; scorePopin.zIndex = 1200; add(scorePopin); new FlxTimer().start(36 / 24, _ -> { scorePopin.visible = true; scorePopin.animation.play("score"); scorePopin.animation.finishCallback = anim -> {}; }); new FlxTimer().start(37 / 24, _ -> { score.visible = true; score.animateNumbers(); startRankTallySequence(); }); new FlxTimer().start(rank.getBFDelay(), _ -> { afterRankTallySequence(); }); new FlxTimer().start(rank.getFlashDelay(), _ -> { displayRankText(); }); highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew"); highscoreNew.animation.addByPrefix("new", "highscoreAnim0", 24, false); highscoreNew.visible = false; // highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8)); highscoreNew.updateHitbox(); highscoreNew.zIndex = 1200; add(highscoreNew); new FlxTimer().start(rank.getHighscoreDelay(), _ -> { if (params.isNewHighscore ?? false) { highscoreNew.visible = true; highscoreNew.animation.play("new"); highscoreNew.animation.finishCallback = _ -> highscoreNew.animation.play("new", true, false, 16); } else { highscoreNew.visible = false; } }); var hStuf:Int = 50; var ratingGrp:FlxTypedGroup = new FlxTypedGroup(); ratingGrp.zIndex = 1200; add(ratingGrp); /** * NOTE: We display how many notes were HIT, not how many notes there were in total. * */ var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.scoreData.tallies.totalNotesHit); ratingGrp.add(totalHit); var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.scoreData.tallies.maxCombo); ratingGrp.add(maxCombo); hStuf += 2; var extraYOffset:Float = 7; hStuf += 2; var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E); ratingGrp.add(tallySick); var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.scoreData.tallies.good, 0xFF89C9E5); ratingGrp.add(tallyGood); var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.scoreData.tallies.bad, 0xFFE6CF8A); ratingGrp.add(tallyBad); var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.scoreData.tallies.shit, 0xFFE68C8A); ratingGrp.add(tallyShit); var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6); ratingGrp.add(tallyMissed); score.visible = false; score.zIndex = 1200; add(score); for (ind => rating in ratingGrp.members) { rating.visible = false; new FlxTimer().start((0.3 * ind) + 1.20, _ -> { rating.visible = true; FlxTween.tween(rating, {curNumber: rating.neededNumber}, 0.5, {ease: FlxEase.quartOut}); }); } // if (params.isNewHighscore ?? false) // { // highscoreNew.visible = true; // highscoreNew.animation.play("new"); // //FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); // } // else // { // highscoreNew.visible = false; // } new FlxTimer().start(rank.getMusicDelay(), _ -> { var introMusic:String = Paths.music(getMusicPath(playerCharacter, rank) + '/' + getMusicPath(playerCharacter, rank) + '-intro'); if (Assets.exists(introMusic)) { // Play the intro music. FunkinSound.load(introMusic, 1.0, false, true, true, () -> { FunkinSound.playMusic(getMusicPath(playerCharacter, rank), { startingVolume: 1.0, overrideExisting: true, restartTrack: true, loop: rank.shouldMusicLoop() }); }); } else { FunkinSound.playMusic(getMusicPath(playerCharacter, rank), { startingVolume: 1.0, overrideExisting: true, restartTrack: true, loop: rank.shouldMusicLoop() }); } }); rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xFF000000); rankBg.zIndex = 99999; add(rankBg); rankBg.alpha = 0; refresh(); super.create(); } function getMusicPath(playerCharacter:Null, rank:ScoringRank):String { return playerCharacter?.getResultsMusicPath(rank) ?? 'resultsNORMAL'; } var rankTallyTimer:Null = null; var clearPercentTarget:Int = 100; var clearPercentLerp:Int = 0; function startRankTallySequence():Void { bgFlash.visible = true; FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24); // NOTE: Only divide if totalNotes > 0 to prevent divide-by-zero errors. var clearPercentFloat = params.scoreData.tallies.totalNotes == 0 ? 0.0 : (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100; clearPercentTarget = Math.floor(clearPercentFloat); // Prevent off-by-one errors. clearPercentLerp = Std.int(Math.max(0, clearPercentTarget - 36)); trace('Clear percent target: ' + clearPercentFloat + ', round: ' + clearPercentTarget); var clearPercentCounter:ClearPercentCounter = new ClearPercentCounter(FlxG.width / 2 + 190, FlxG.height / 2 - 70, clearPercentLerp); FlxTween.tween(clearPercentCounter, {curNumber: clearPercentTarget}, 58 / 24, { ease: FlxEase.quartOut, onUpdate: _ -> { // Only play the tick sound if the number increased. if (clearPercentLerp != clearPercentCounter.curNumber) { clearPercentLerp = clearPercentCounter.curNumber; FunkinSound.playOnce(Paths.sound('scrollMenu')); } }, onComplete: _ -> { // Play confirm sound. FunkinSound.playOnce(Paths.sound('confirmMenu')); // Just to be sure that the lerp didn't mess things up. clearPercentCounter.curNumber = clearPercentTarget; clearPercentCounter.flash(true); new FlxTimer().start(0.4, _ -> { clearPercentCounter.flash(false); }); // displayRankText(); // previously 2.0 seconds new FlxTimer().start(0.25, _ -> { FlxTween.tween(clearPercentCounter, {alpha: 0}, 0.5, { startDelay: 0.5, ease: FlxEase.quartOut, onComplete: _ -> { remove(clearPercentCounter); } }); // afterRankTallySequence(); }); } }); clearPercentCounter.zIndex = 450; add(clearPercentCounter); if (ratingsPopin == null) { trace("Could not build ratingsPopin!"); } else { // ratingsPopin.animation.play("idle"); // ratingsPopin.visible = true; ratingsPopin.animation.finishCallback = anim -> { // scorePopin.animation.play("score"); // scorePopin.visible = true; if (params.isNewHighscore ?? false) { highscoreNew.visible = true; highscoreNew.animation.play("new"); } else { highscoreNew.visible = false; } }; } refresh(); } function displayRankText():Void { bgFlash.visible = true; bgFlash.alpha = 1; FlxTween.tween(bgFlash, {alpha: 0}, 14 / 24); var rankTextVert:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getVerTextAsset()), Y, 0, 30); rankTextVert.x = FlxG.width - 44; rankTextVert.y = 100; rankTextVert.zIndex = 990; add(rankTextVert); FlxFlicker.flicker(rankTextVert, 2 / 24 * 3, 2 / 24, true); // Scrolling. new FlxTimer().start(30 / 24, _ -> { rankTextVert.velocity.y = -80; }); for (i in 0...12) { var rankTextBack:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getHorTextAsset()), X, 10, 0); rankTextBack.x = FlxG.width / 2 - 320; rankTextBack.y = 50 + (135 * i / 2) + 10; // rankTextBack.angle = -3.8; rankTextBack.zIndex = 100; rankTextBack.cameras = [cameraScroll]; add(rankTextBack); // Scrolling. rankTextBack.velocity.x = (i % 2 == 0) ? -7.0 : 7.0; } refresh(); } function afterRankTallySequence():Void { showSmallClearPercent(); for (atlas in characterAtlasAnimations) { new FlxTimer().start(atlas.delay, _ -> { if (atlas.sprite == null) return; atlas.sprite.visible = true; atlas.sprite.playAnimation(''); }); } for (sprite in characterSparrowAnimations) { new FlxTimer().start(sprite.delay, _ -> { if (sprite.sprite == null) return; sprite.sprite.visible = true; sprite.sprite.animation.play('idle', true); }); } } function timerThenSongName(timerLength:Float = 3.0, autoScroll:Bool = true):Void { movingSongStuff = false; difficulty.x = 555; var diffYTween:Float = 122; difficulty.y = -difficulty.height; FlxTween.tween(difficulty, {y: diffYTween}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.8}); if (clearPercentSmall != null) { clearPercentSmall.x = (difficulty.x + difficulty.width) + 60; clearPercentSmall.y = -clearPercentSmall.height; FlxTween.tween(clearPercentSmall, {y: 122 - 5}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.85}); } songName.y = -songName.height; var fuckedupnumber = (10) * (songName.text.length / 15); FlxTween.tween(songName, {y: diffYTween - 25 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9}); songName.x = clearPercentSmall.x + 94; new FlxTimer().start(timerLength, _ -> { var tempSpeed = FlxPoint.get(speedOfTween.x, speedOfTween.y); speedOfTween.set(0, 0); FlxTween.tween(speedOfTween, {x: tempSpeed.x, y: tempSpeed.y}, 0.7, {ease: FlxEase.quadIn}); movingSongStuff = (autoScroll); }); } function showSmallClearPercent():Void { if (clearPercentSmall != null) { add(clearPercentSmall); clearPercentSmall.visible = true; clearPercentSmall.flash(true); new FlxTimer().start(0.4, _ -> { clearPercentSmall.flash(false); }); clearPercentSmall.curNumber = clearPercentTarget; clearPercentSmall.zIndex = 1000; refresh(); } new FlxTimer().start(2.5, _ -> { movingSongStuff = true; }); } var movingSongStuff:Bool = false; var speedOfTween:FlxPoint = FlxPoint.get(-1, 1); override function draw():Void { super.draw(); songName.clipRect = FlxRect.get(Math.max(0, 520 - songName.x), 0, FlxG.width, songName.height); // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!! // if (songName != null && songName.frame != null) // maskShaderSongName.frameUV = songName.frame.uv; } override function update(elapsed:Float):Void { // if(FlxG.keys.justPressed.R){ // FlxG.switchState(() -> new funkin.play.ResultState( // { // storyMode: false, // title: "Cum Song Erect by Kawai Sprite", // songId: "cum", // difficultyId: "nightmare", // isNewHighscore: true, // scoreData: // { // score: 1_234_567, // tallies: // { // sick: 200, // good: 0, // bad: 0, // shit: 0, // missed: 0, // combo: 0, // maxCombo: 69, // totalNotesHit: 200, // totalNotes: 200 // 0, // } // }, // })); // } // maskShaderSongName.swagSprX = songName.x; maskShaderDifficulty.swagSprX = difficulty.x; if (movingSongStuff) { songName.x += speedOfTween.x; difficulty.x += speedOfTween.x; clearPercentSmall.x += speedOfTween.x; songName.y += speedOfTween.y; difficulty.y += speedOfTween.y; clearPercentSmall.y += speedOfTween.y; if (songName.x + songName.width < 100) { timerThenSongName(); } } if (FlxG.keys.justPressed.RIGHT) speedOfTween.x += 0.1; if (FlxG.keys.justPressed.LEFT) { speedOfTween.x -= 0.1; } if (controls.PAUSE) { if (FlxG.sound.music != null) { FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8); FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1, { onComplete: _ -> { FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4); } }); } if (params.storyMode) { openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker))); } else { var rigged:Bool = true; if (rank > Scoring.calculateRank(params?.prevScoreData)) // if (rigged) { trace('THE RANK IS Higher.....'); FlxTween.tween(rankBg, {alpha: 1}, 0.5, { ease: FlxEase.expoOut, onComplete: function(_) { FlxG.switchState(FreeplayState.build( { { character: playerCharacterId ?? "bf", fromResults: { 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( { { fromResults: { oldRank: null, playRankAnim: false, newRank: rank, songId: params.songId, difficultyId: params.difficultyId } } }, sticker))); } } } super.update(elapsed); } } typedef ResultsStateParams = { /** * True if results are for a level, false if results are for a single song. */ var storyMode:Bool; /** * A readable title for the song we just played. * Either "Song Name by Artist Name" or "Week Name" */ var title:String; /** * The internal song ID for the song we just played. */ var songId:String; /** * The character ID for the song we just played. * @default `bf` */ var ?characterId:String; /** * Whether the displayed score is a new highscore */ var ?isNewHighscore:Bool; /** * The difficulty ID of the song/week we just played. * @default Normal */ var ?difficultyId:String; /** * The score, accuracy, and judgements. */ var scoreData:SaveScoreData; /** * The previous score data, used for rank comparision. */ var ?prevScoreData:SaveScoreData; };