2023-06-02 18:35:08 +00:00
|
|
|
package funkin.play.cutscene;
|
|
|
|
|
|
|
|
import funkin.play.PlayState;
|
|
|
|
import flixel.FlxSprite;
|
|
|
|
import flixel.tweens.FlxEase;
|
|
|
|
import flixel.tweens.FlxTween;
|
|
|
|
import flixel.util.FlxColor;
|
2024-03-23 03:53:29 +00:00
|
|
|
import flixel.util.FlxSignal;
|
2023-06-02 18:35:08 +00:00
|
|
|
import flixel.util.FlxTimer;
|
|
|
|
#if html5
|
|
|
|
import funkin.graphics.video.FlxVideo;
|
2024-01-13 02:52:28 +00:00
|
|
|
#end
|
|
|
|
#if hxCodec
|
2023-06-02 18:35:08 +00:00
|
|
|
import hxcodec.flixel.FlxVideoSprite;
|
|
|
|
#end
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Assumes you are in the PlayState.
|
|
|
|
*/
|
|
|
|
class VideoCutscene
|
|
|
|
{
|
|
|
|
static var blackScreen:FlxSprite;
|
2024-02-27 00:03:04 +00:00
|
|
|
static var cutsceneType:CutsceneType;
|
|
|
|
|
|
|
|
#if html5
|
|
|
|
static var vid:FlxVideo;
|
|
|
|
#end
|
|
|
|
#if hxCodec
|
|
|
|
static var vid:FlxVideoSprite;
|
|
|
|
#end
|
2023-06-02 18:35:08 +00:00
|
|
|
|
2024-03-23 03:46:03 +00:00
|
|
|
/**
|
|
|
|
* Called when the video is started.
|
|
|
|
*/
|
|
|
|
public static final onVideoStarted:FlxSignal = new FlxSignal();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called if the video is paused.
|
|
|
|
*/
|
|
|
|
public static final onVideoPaused:FlxSignal = new FlxSignal();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called if the video is resumed.
|
|
|
|
*/
|
|
|
|
public static final onVideoResumed:FlxSignal = new FlxSignal();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called if the video is restarted. onVideoStarted is not called.
|
|
|
|
*/
|
|
|
|
public static final onVideoRestarted:FlxSignal = new FlxSignal();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when the video is ended or skipped.
|
|
|
|
*/
|
|
|
|
public static final onVideoEnded:FlxSignal = new FlxSignal();
|
|
|
|
|
2023-06-02 18:35:08 +00:00
|
|
|
/**
|
|
|
|
* Play a video cutscene.
|
|
|
|
* TODO: Currently this is hardcoded to start the countdown after the video is done.
|
|
|
|
* @param path The path to the video file. Use Paths.file(path) to get the correct path.
|
2024-02-27 00:03:04 +00:00
|
|
|
* @param cutseneType The type of cutscene to play, determines what the game does after. Defaults to `CutsceneType.STARTING`.
|
2023-06-02 18:35:08 +00:00
|
|
|
*/
|
2024-02-27 00:03:04 +00:00
|
|
|
public static function play(filePath:String, ?cutsceneType:CutsceneType = STARTING):Void
|
2023-06-02 18:35:08 +00:00
|
|
|
{
|
|
|
|
if (PlayState.instance == null) return;
|
|
|
|
|
|
|
|
if (!openfl.Assets.exists(filePath))
|
|
|
|
{
|
2023-07-02 20:46:40 +00:00
|
|
|
// Display a popup.
|
2024-04-05 05:24:03 +00:00
|
|
|
// lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
|
|
|
|
// return;
|
|
|
|
|
|
|
|
// TODO: After moving videos to their own library,
|
|
|
|
// this function ALWAYS FAILS on web, but the video still plays.
|
|
|
|
// I think that's due to a weird quirk with how OpenFL libraries work.
|
|
|
|
trace('Video file does not exist: ${filePath}');
|
2023-06-02 18:35:08 +00:00
|
|
|
}
|
|
|
|
|
2024-02-23 01:56:41 +00:00
|
|
|
var rawFilePath = Paths.stripLibrary(filePath);
|
|
|
|
|
2023-06-02 18:35:08 +00:00
|
|
|
// Trigger the cutscene. Don't play the song in the background.
|
|
|
|
PlayState.instance.isInCutscene = true;
|
|
|
|
PlayState.instance.camHUD.visible = false;
|
|
|
|
|
|
|
|
// Display a black screen to hide the game while the video is playing.
|
|
|
|
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
|
|
|
blackScreen.scrollFactor.set(0, 0);
|
2024-05-20 21:52:45 +00:00
|
|
|
blackScreen.cameras = [PlayState.instance.camOther];
|
2023-06-02 18:35:08 +00:00
|
|
|
PlayState.instance.add(blackScreen);
|
|
|
|
|
2024-02-27 00:03:04 +00:00
|
|
|
VideoCutscene.cutsceneType = cutsceneType;
|
|
|
|
|
2023-06-02 18:35:08 +00:00
|
|
|
#if html5
|
2024-03-07 07:37:50 +00:00
|
|
|
playVideoHTML5(rawFilePath);
|
2024-02-23 01:56:41 +00:00
|
|
|
#elseif hxCodec
|
|
|
|
playVideoNative(rawFilePath);
|
|
|
|
#else
|
|
|
|
throw "No video support for this platform!";
|
2023-06-02 18:35:08 +00:00
|
|
|
#end
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function isPlaying():Bool
|
|
|
|
{
|
2024-01-13 02:52:28 +00:00
|
|
|
#if (html5 || hxCodec)
|
2023-06-02 18:35:08 +00:00
|
|
|
return vid != null;
|
2024-01-13 02:52:28 +00:00
|
|
|
#else
|
|
|
|
return false;
|
|
|
|
#end
|
2023-06-02 18:35:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#if html5
|
|
|
|
static function playVideoHTML5(filePath:String):Void
|
|
|
|
{
|
|
|
|
// Video displays OVER the FlxState.
|
|
|
|
vid = new FlxVideo(filePath);
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.zIndex = 0;
|
|
|
|
|
|
|
|
vid.finishCallback = finishVideo.bind(0.5);
|
|
|
|
|
2024-05-20 21:52:45 +00:00
|
|
|
vid.cameras = [PlayState.instance.camOther];
|
2023-06-02 18:35:08 +00:00
|
|
|
|
|
|
|
PlayState.instance.add(vid);
|
|
|
|
|
|
|
|
PlayState.instance.refresh();
|
2024-03-23 03:46:03 +00:00
|
|
|
|
|
|
|
onVideoStarted.dispatch();
|
2023-06-02 18:35:08 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
trace('ALERT: Video is null! Could not play cutscene!');
|
|
|
|
}
|
|
|
|
}
|
2024-01-13 02:52:28 +00:00
|
|
|
#end
|
|
|
|
|
|
|
|
#if hxCodec
|
2023-06-02 18:35:08 +00:00
|
|
|
static function playVideoNative(filePath:String):Void
|
|
|
|
{
|
|
|
|
// Video displays OVER the FlxState.
|
|
|
|
vid = new FlxVideoSprite(0, 0);
|
|
|
|
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.zIndex = 0;
|
2023-06-06 21:38:40 +00:00
|
|
|
vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
|
2024-06-24 08:55:59 +00:00
|
|
|
vid.autoPause = FlxG.autoPause;
|
2023-06-02 18:35:08 +00:00
|
|
|
|
2024-05-20 21:52:45 +00:00
|
|
|
vid.cameras = [PlayState.instance.camOther];
|
2023-06-02 18:35:08 +00:00
|
|
|
|
|
|
|
PlayState.instance.add(vid);
|
|
|
|
|
|
|
|
PlayState.instance.refresh();
|
2023-06-06 21:38:40 +00:00
|
|
|
vid.play(filePath, false);
|
2024-02-23 01:56:41 +00:00
|
|
|
|
|
|
|
// Resize videos bigger or smaller than the screen.
|
|
|
|
vid.bitmap.onTextureSetup.add(() -> {
|
|
|
|
vid.setGraphicSize(FlxG.width, FlxG.height);
|
|
|
|
vid.updateHitbox();
|
|
|
|
vid.x = 0;
|
|
|
|
vid.y = 0;
|
|
|
|
// vid.scale.set(0.5, 0.5);
|
|
|
|
});
|
2024-03-23 03:46:03 +00:00
|
|
|
|
|
|
|
onVideoStarted.dispatch();
|
2023-06-02 18:35:08 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
trace('ALERT: Video is null! Could not play cutscene!');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#end
|
|
|
|
|
2024-02-28 08:53:36 +00:00
|
|
|
public static function restartVideo(resume:Bool = true):Void
|
2024-02-28 05:19:08 +00:00
|
|
|
{
|
|
|
|
#if html5
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.restartVideo();
|
2024-03-23 03:46:03 +00:00
|
|
|
onVideoRestarted.dispatch();
|
2024-02-28 05:19:08 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
|
|
|
|
#if hxCodec
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
// Seek to the start of the video.
|
|
|
|
vid.bitmap.time = 0;
|
2024-02-28 08:53:36 +00:00
|
|
|
if (resume)
|
|
|
|
{
|
|
|
|
// Resume the video if it was paused.
|
|
|
|
vid.resume();
|
|
|
|
}
|
2024-03-23 03:46:03 +00:00
|
|
|
|
|
|
|
onVideoRestarted.dispatch();
|
2024-02-28 08:53:36 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function pauseVideo():Void
|
|
|
|
{
|
|
|
|
#if html5
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.pauseVideo();
|
2024-03-23 03:46:03 +00:00
|
|
|
onVideoPaused.dispatch();
|
2024-02-28 08:53:36 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
|
|
|
|
#if hxCodec
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.pause();
|
2024-03-23 03:46:03 +00:00
|
|
|
onVideoPaused.dispatch();
|
|
|
|
}
|
|
|
|
#end
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function hideVideo():Void
|
|
|
|
{
|
|
|
|
#if html5
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.visible = false;
|
2024-03-23 03:53:29 +00:00
|
|
|
blackScreen.visible = false;
|
2024-03-23 03:46:03 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
|
|
|
|
#if hxCodec
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.visible = false;
|
2024-03-23 03:53:29 +00:00
|
|
|
blackScreen.visible = false;
|
2024-03-23 03:46:03 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function showVideo():Void
|
|
|
|
{
|
|
|
|
#if html5
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.visible = true;
|
2024-03-23 03:53:29 +00:00
|
|
|
blackScreen.visible = false;
|
2024-03-23 03:46:03 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
|
|
|
|
#if hxCodec
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.visible = true;
|
2024-03-23 03:53:29 +00:00
|
|
|
blackScreen.visible = false;
|
2024-02-28 08:53:36 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function resumeVideo():Void
|
|
|
|
{
|
|
|
|
#if html5
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.resumeVideo();
|
2024-03-23 03:46:03 +00:00
|
|
|
onVideoResumed.dispatch();
|
2024-02-28 08:53:36 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
|
|
|
|
#if hxCodec
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.resume();
|
2024-03-23 03:46:03 +00:00
|
|
|
onVideoResumed.dispatch();
|
2024-02-28 05:19:08 +00:00
|
|
|
}
|
|
|
|
#end
|
|
|
|
}
|
|
|
|
|
2024-02-27 00:03:04 +00:00
|
|
|
/**
|
|
|
|
* Finish the active video cutscene. Done when the video is finished or when the player skips the cutscene.
|
|
|
|
* @param transitionTime The duration of the transition to the next state. Defaults to 0.5 seconds (this time is always used when cancelling the video).
|
|
|
|
* @param finishCutscene The callback to call when the transition is finished.
|
|
|
|
*/
|
2023-06-02 18:35:08 +00:00
|
|
|
public static function finishVideo(?transitionTime:Float = 0.5):Void
|
|
|
|
{
|
|
|
|
trace('ALERT: Finish video cutscene called!');
|
|
|
|
|
2024-02-27 00:03:04 +00:00
|
|
|
var cutsceneType:CutsceneType = VideoCutscene.cutsceneType;
|
|
|
|
|
2023-06-02 18:35:08 +00:00
|
|
|
#if html5
|
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
PlayState.instance.remove(vid);
|
|
|
|
}
|
2024-01-13 02:52:28 +00:00
|
|
|
#end
|
|
|
|
|
|
|
|
#if hxCodec
|
2023-06-02 18:35:08 +00:00
|
|
|
if (vid != null)
|
|
|
|
{
|
|
|
|
vid.stop();
|
|
|
|
PlayState.instance.remove(vid);
|
|
|
|
}
|
|
|
|
#end
|
2024-01-13 02:52:28 +00:00
|
|
|
|
|
|
|
#if (html5 || hxCodec)
|
2023-06-02 18:35:08 +00:00
|
|
|
vid.destroy();
|
|
|
|
vid = null;
|
2024-01-13 02:52:28 +00:00
|
|
|
#end
|
2023-06-02 18:35:08 +00:00
|
|
|
|
|
|
|
PlayState.instance.camHUD.visible = true;
|
|
|
|
|
|
|
|
FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,
|
|
|
|
{
|
|
|
|
ease: FlxEase.quadInOut,
|
|
|
|
onComplete: function(twn:FlxTween) {
|
|
|
|
PlayState.instance.remove(blackScreen);
|
|
|
|
blackScreen = null;
|
|
|
|
}
|
|
|
|
});
|
2024-03-28 18:05:38 +00:00
|
|
|
FlxTween.tween(FlxG.camera, {zoom: PlayState.instance.stageZoom}, transitionTime,
|
2023-06-02 18:35:08 +00:00
|
|
|
{
|
|
|
|
ease: FlxEase.quadInOut,
|
|
|
|
onComplete: function(twn:FlxTween) {
|
2024-03-23 03:46:03 +00:00
|
|
|
onVideoEnded.dispatch();
|
2024-02-27 00:03:04 +00:00
|
|
|
onCutsceneFinish(cutsceneType);
|
2023-06-02 18:35:08 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-02-27 00:03:04 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The default callback used when a cutscene is finished.
|
|
|
|
* You can specify your own callback when calling `VideoCutscene#play()`.
|
|
|
|
*/
|
|
|
|
static function onCutsceneFinish(cutsceneType:CutsceneType):Void
|
|
|
|
{
|
|
|
|
switch (cutsceneType)
|
|
|
|
{
|
|
|
|
case CutsceneType.STARTING:
|
|
|
|
PlayState.instance.startCountdown();
|
|
|
|
case CutsceneType.ENDING:
|
|
|
|
PlayState.instance.endSong(true); // true = right goddamn now
|
|
|
|
case CutsceneType.MIDSONG:
|
2024-05-02 07:39:18 +00:00
|
|
|
// Do nothing.
|
|
|
|
// throw "Not implemented!";
|
2024-02-27 00:03:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum CutsceneType
|
|
|
|
{
|
|
|
|
STARTING; // The default cutscene type. Starts the countdown after the video is done.
|
2024-05-02 07:39:18 +00:00
|
|
|
MIDSONG; // Does nothing.
|
2024-02-27 00:03:04 +00:00
|
|
|
ENDING; // Ends the song after the video is done.
|
2023-06-02 18:35:08 +00:00
|
|
|
}
|