Funkin/source/funkin/play/GameOverSubState.hx

357 lines
10 KiB
Haxe
Raw Normal View History

package funkin.play;
2020-10-27 06:35:23 -04:00
2023-05-31 03:03:10 -04:00
import flixel.FlxG;
2020-10-27 06:35:23 -04:00
import flixel.FlxObject;
import flixel.FlxSprite;
2023-07-02 15:34:07 -04:00
import flixel.sound.FlxSound;
2023-05-17 16:42:58 -04:00
import funkin.ui.story.StoryMenuState;
2020-10-28 03:03:32 -04:00
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;
import funkin.play.PlayState;
import funkin.ui.freeplay.FreeplayState;
import funkin.play.character.BaseCharacter;
2020-10-27 06:35:23 -04:00
/**
* A substate which renders over the PlayState when the player dies.
* Displays the player death animation, plays the music, and handles restarting the song.
2023-06-08 16:30:45 -04:00
*
* The newest implementation uses a substate, which prevents having to reload the song and stage each reset.
*/
class GameOverSubState extends MusicBeatSubState
2020-10-27 06:35:23 -04:00
{
2024-01-17 23:14:28 -05:00
/**
* 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.
* For example, playing a different animation when BF dies in Week 4
* or Pico dies in Weekend 1.
*/
public static var animationSuffix:String = "";
/**
* Which alternate game over music to use.
* You can set this via script.
* For example, the bf-pixel script sets this to `-pixel`
* and the pico-playable script sets this to `Pico`.
*/
public static var musicSuffix:String = "";
/**
* Which alternate "blue ball" sound effect to use.
*/
public static var blueBallSuffix:String = "";
/**
* The boyfriend character.
*/
var boyfriend:BaseCharacter;
/**
* The invisible object in the scene which the camera focuses on.
*/
var cameraFollowPoint:FlxObject;
/**
* The music playing in the background of the state.
*/
var gameOverMusic:FlxSound = new FlxSound();
/**
* Whether the player has confirmed and prepared to restart the level.
* This means the animation and transition have already started.
*/
var isEnding:Bool = false;
var isChartingMode:Bool = false;
var transparent:Bool;
public function new(params:GameOverParams)
{
super();
this.isChartingMode = params?.isChartingMode ?? false;
transparent = params.transparent;
}
/**
* Reset the game over configuration to the default.
*/
public static function reset()
{
animationSuffix = "";
musicSuffix = "";
}
override public function create()
{
2024-01-17 23:14:28 -05:00
if (instance != null)
{
// TODO: Do something in this case? IDK.
trace('WARNING: GameOverSubState instance already exists. This should not happen.');
}
instance = this;
super.create();
//
// Set up the visuals
//
// Add a black background to the screen.
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;
bg.scrollFactor.set();
2024-01-16 00:03:30 -05:00
bg.screenCenter();
add(bg);
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
// We can then play the character's `firstDeath` animation.
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
boyfriend.isDead = true;
add(boyfriend);
boyfriend.resetCharacter();
2023-05-31 03:03:10 -04:00
// Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint);
FlxG.camera.target = null;
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
//
// Set up the audio
//
// Prepare the game over music.
FlxG.sound.list.add(gameOverMusic);
gameOverMusic.stop();
// The conductor now represents the BPM of the game over music.
Conductor.instance.update(0);
}
2023-06-01 00:31:32 -04:00
var hasStartedAnimation:Bool = false;
override function update(elapsed:Float)
{
2023-06-01 00:31:32 -04:00
if (!hasStartedAnimation)
{
hasStartedAnimation = true;
if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100))
2023-06-01 00:31:32 -04:00
{
2023-08-03 11:22:52 -04:00
boyfriend.playAnimation('fakeoutDeath', true, false);
2023-06-01 00:31:32 -04:00
}
else
{
2023-08-03 11:22:52 -04:00
boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over!
2023-06-01 00:31:32 -04:00
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
}
}
//
// Handle user inputs.
//
// MOBILE ONLY: Restart the level when tapping Boyfriend.
if (FlxG.onMobile)
{
var touch = FlxG.touches.getFirst();
if (touch != null)
{
if (touch.overlaps(boyfriend))
{
confirmDeath();
}
}
}
// KEYBOARD ONLY: Restart the level when pressing the assigned key.
2023-06-02 15:56:40 -04:00
if (controls.ACCEPT && blueballed)
{
2023-06-04 01:19:08 -04:00
blueballed = false;
confirmDeath();
}
// KEYBOARD ONLY: Return to the menu when pressing the assigned key.
if (controls.BACK)
{
2023-06-04 01:19:08 -04:00
blueballed = false;
2023-06-15 14:31:49 -04:00
PlayState.instance.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing...
gameOverMusic.stop();
if (isChartingMode)
{
this.close();
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
PlayState.instance.close(); // This only works because PlayState is a substate!
}
else if (PlayStatePlaylist.isStoryMode)
{
FlxG.switchState(new StoryMenuState());
}
else
{
FlxG.switchState(new FreeplayState());
}
}
if (gameOverMusic.playing)
{
// Match the conductor to the music.
// This enables the stepHit and beatHit events.
Conductor.instance.update(gameOverMusic.time);
}
else
{
// Music hasn't started yet.
switch (PlayStatePlaylist.campaignId)
{
// TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
// This will simplify the class and make it easier for mods to add death quotes.
case "week7":
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
{
playingJeffQuote = true;
playJeffQuote();
// Start music at lower volume
startDeathMusic(0.2, false);
2024-01-15 23:51:37 -05:00
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
default:
// Start music at normal volume once the initial death animation finishes.
if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
{
startDeathMusic(1.0, false);
boyfriend.playAnimation('deathLoop' + animationSuffix);
}
}
}
// Start death music before firstDeath gets replaced
super.update(elapsed);
}
/**
* Do behavior which occurs when you confirm and move to restart the level.
*/
function confirmDeath():Void
{
if (!isEnding)
{
isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
// After the animation finishes...
2023-05-17 16:42:58 -04:00
new FlxTimer().start(0.7, function(tmr:FlxTimer) {
// ...fade out the graphics. Then after that happens...
2023-05-17 16:42:58 -04:00
FlxG.camera.fade(FlxColor.BLACK, 2, false, function() {
// ...close the GameOverSubState.
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true;
// Readd Boyfriend to the stage.
boyfriend.isDead = false;
remove(boyfriend);
PlayState.instance.currentStage.addCharacter(boyfriend, BF);
// Close the substate.
close();
});
});
}
}
2023-06-16 17:37:56 -04:00
public override function dispatchEvent(event:ScriptEvent)
{
super.dispatchEvent(event);
ScriptEventDispatcher.callEvent(boyfriend, event);
}
/**
* Starts the death music at the appropriate volume.
2023-06-08 16:30:45 -04:00
* @param startingVolume
*/
function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
{
2024-01-17 23:14:28 -05:00
var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix);
if (isEnding)
{
2024-01-17 23:14:28 -05:00
musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
}
if (!gameOverMusic.playing || force)
{
gameOverMusic.loadEmbedded(musicPath);
gameOverMusic.volume = startingVolume;
gameOverMusic.looped = !isEnding;
gameOverMusic.play();
}
}
2023-06-04 01:19:08 -04:00
static var blueballed:Bool = false;
2023-06-02 15:56:40 -04:00
/**
* Play the sound effect that occurs when
* boyfriend's testicles get utterly annihilated.
*/
2023-06-01 00:31:32 -04:00
public static function playBlueBalledSFX()
{
2023-06-02 15:56:40 -04:00
blueballed = true;
2024-01-17 23:14:28 -05:00
FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
}
var playingJeffQuote:Bool = false;
/**
* Week 7-specific hardcoded behavior, to play a custom death quote.
* TODO: Make this a module somehow.
*/
function playJeffQuote()
{
var randomCensor:Array<Int> = [];
if (!Preferences.naughtyness) randomCensor = [1, 3, 8, 13, 17, 21];
2023-05-17 16:42:58 -04:00
FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), 1, false, null, true, function() {
// Once the quote ends, fade in the game over music.
if (!isEnding && gameOverMusic != null)
{
gameOverMusic.fadeIn(4, 0.2, 1);
}
});
}
2024-01-17 23:14:28 -05:00
public override function toString():String
{
return "GameOverSubState";
}
2020-10-27 06:35:23 -04:00
}
typedef GameOverParams =
{
var isChartingMode:Bool;
var transparent:Bool;
}