2022-03-11 06:30:01 +00:00
|
|
|
package funkin.play;
|
|
|
|
|
2022-03-13 18:36:03 +00:00
|
|
|
import flixel.tweens.FlxEase;
|
|
|
|
import flixel.tweens.FlxTween;
|
|
|
|
import flixel.FlxSprite;
|
2024-02-22 23:55:24 +00:00
|
|
|
import funkin.graphics.FunkinSprite;
|
2022-03-13 18:36:03 +00:00
|
|
|
import funkin.modding.events.ScriptEventDispatcher;
|
|
|
|
import funkin.modding.module.ModuleHandler;
|
|
|
|
import funkin.modding.events.ScriptEvent;
|
2023-10-26 09:46:22 +00:00
|
|
|
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
|
2022-03-13 18:36:03 +00:00
|
|
|
import flixel.util.FlxTimer;
|
2024-06-20 03:47:11 +00:00
|
|
|
import funkin.util.EaseUtil;
|
2024-03-23 21:50:48 +00:00
|
|
|
import funkin.audio.FunkinSound;
|
2024-07-16 17:53:12 +00:00
|
|
|
import funkin.data.notestyle.NoteStyleRegistry;
|
|
|
|
import funkin.play.notes.notestyle.NoteStyle;
|
2022-03-13 18:36:03 +00:00
|
|
|
|
|
|
|
class Countdown
|
|
|
|
{
|
2023-01-23 03:25:45 +00:00
|
|
|
/**
|
|
|
|
* The current step of the countdown.
|
|
|
|
*/
|
|
|
|
public static var countdownStep(default, null):CountdownStep = BEFORE;
|
|
|
|
|
2024-07-13 19:45:58 +00:00
|
|
|
/**
|
2024-07-16 17:53:12 +00:00
|
|
|
* Which alternate graphic/sound on countdown to use.
|
|
|
|
* This is set via the current notestyle.
|
|
|
|
* For example, in Week 6 it is `pixel`.
|
2024-07-13 19:45:58 +00:00
|
|
|
*/
|
|
|
|
public static var soundSuffix:String = '';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Which alternate graphic on countdown to use.
|
|
|
|
* You can set this via script.
|
|
|
|
* For example, in Week 6 it is `-pixel`.
|
|
|
|
*/
|
|
|
|
public static var graphicSuffix:String = '';
|
|
|
|
|
2024-07-16 17:53:12 +00:00
|
|
|
static var noteStyle:NoteStyle;
|
|
|
|
|
2024-07-17 20:19:18 +00:00
|
|
|
static var fallbackNoteStyle:Null<NoteStyle>;
|
|
|
|
|
2023-01-23 03:25:45 +00:00
|
|
|
/**
|
|
|
|
* The currently running countdown. This will be null if there is no countdown running.
|
|
|
|
*/
|
|
|
|
static var countdownTimer:FlxTimer = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Performs the countdown.
|
|
|
|
* Pauses the song, plays the countdown graphics/sound, and then starts the song.
|
|
|
|
* This will automatically stop and restart the countdown if it is already running.
|
|
|
|
* @returns `false` if the countdown was cancelled by a script.
|
|
|
|
*/
|
2024-07-13 19:45:58 +00:00
|
|
|
public static function performCountdown():Bool
|
2023-01-23 03:25:45 +00:00
|
|
|
{
|
|
|
|
countdownStep = BEFORE;
|
|
|
|
var cancelled:Bool = propagateCountdownEvent(countdownStep);
|
2023-08-04 21:25:13 +00:00
|
|
|
if (cancelled)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2023-01-23 03:25:45 +00:00
|
|
|
|
|
|
|
// Stop any existing countdown.
|
|
|
|
stopCountdown();
|
|
|
|
|
2023-06-06 21:38:31 +00:00
|
|
|
PlayState.instance.isInCountdown = true;
|
2023-12-14 21:56:20 +00:00
|
|
|
Conductor.instance.update(PlayState.instance.startTimestamp + Conductor.instance.beatLengthMs * -5);
|
2023-01-23 03:25:45 +00:00
|
|
|
// Handle onBeatHit events manually
|
2023-08-04 20:15:07 +00:00
|
|
|
// @:privateAccess
|
2023-10-26 09:46:22 +00:00
|
|
|
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
|
2023-01-23 03:25:45 +00:00
|
|
|
|
|
|
|
// The timer function gets called based on the beat of the song.
|
|
|
|
countdownTimer = new FlxTimer();
|
|
|
|
|
2023-12-14 21:56:20 +00:00
|
|
|
countdownTimer.start(Conductor.instance.beatLengthMs / 1000, function(tmr:FlxTimer) {
|
2023-07-27 00:03:31 +00:00
|
|
|
if (PlayState.instance == null)
|
|
|
|
{
|
|
|
|
tmr.cancel();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-01-23 03:25:45 +00:00
|
|
|
countdownStep = decrement(countdownStep);
|
|
|
|
|
2023-08-04 16:35:01 +00:00
|
|
|
// onBeatHit events are now properly dispatched by the Conductor even at negative timestamps,
|
|
|
|
// so calling this is no longer necessary.
|
2023-10-26 09:46:22 +00:00
|
|
|
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
|
2023-01-23 03:25:45 +00:00
|
|
|
|
|
|
|
// Countdown graphic.
|
2024-07-18 14:56:54 +00:00
|
|
|
showCountdownGraphic(countdownStep);
|
2023-01-23 03:25:45 +00:00
|
|
|
|
|
|
|
// Countdown sound.
|
2024-07-18 14:56:54 +00:00
|
|
|
playCountdownSound(countdownStep);
|
2023-01-23 03:25:45 +00:00
|
|
|
|
|
|
|
// Event handling bullshit.
|
|
|
|
var cancelled:Bool = propagateCountdownEvent(countdownStep);
|
|
|
|
|
2023-08-04 21:25:13 +00:00
|
|
|
if (cancelled)
|
|
|
|
{
|
|
|
|
pauseCountdown();
|
|
|
|
}
|
2023-01-23 03:25:45 +00:00
|
|
|
|
|
|
|
if (countdownStep == AFTER)
|
|
|
|
{
|
|
|
|
stopCountdown();
|
|
|
|
}
|
|
|
|
}, 5); // Before, 3, 2, 1, GO!, After
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return TRUE if the event was cancelled.
|
|
|
|
*/
|
|
|
|
static function propagateCountdownEvent(index:CountdownStep):Bool
|
|
|
|
{
|
|
|
|
var event:ScriptEvent;
|
|
|
|
|
|
|
|
switch (index)
|
|
|
|
{
|
|
|
|
case BEFORE:
|
2023-10-26 09:46:22 +00:00
|
|
|
event = new CountdownScriptEvent(COUNTDOWN_START, index);
|
2023-01-23 03:25:45 +00:00
|
|
|
case THREE | TWO | ONE | GO: // I didn't know you could use `|` in a switch/case block!
|
2023-10-26 09:46:22 +00:00
|
|
|
event = new CountdownScriptEvent(COUNTDOWN_STEP, index);
|
2023-01-23 03:25:45 +00:00
|
|
|
case AFTER:
|
2023-10-26 09:46:22 +00:00
|
|
|
event = new CountdownScriptEvent(COUNTDOWN_END, index, false);
|
2023-01-23 03:25:45 +00:00
|
|
|
default:
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Modules, stages, characters.
|
|
|
|
@:privateAccess
|
|
|
|
PlayState.instance.dispatchEvent(event);
|
|
|
|
|
|
|
|
return event.eventCanceled;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pauses the countdown at the current step. You can start it up again later by calling resumeCountdown().
|
2023-06-08 20:30:45 +00:00
|
|
|
*
|
2023-01-23 03:25:45 +00:00
|
|
|
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStep event.
|
|
|
|
*/
|
2024-06-20 03:47:11 +00:00
|
|
|
public static function pauseCountdown():Void
|
2023-01-23 03:25:45 +00:00
|
|
|
{
|
|
|
|
if (countdownTimer != null && !countdownTimer.finished)
|
|
|
|
{
|
|
|
|
countdownTimer.active = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resumes the countdown at the current step. Only makes sense if you called pauseCountdown() first.
|
2023-06-08 20:30:45 +00:00
|
|
|
*
|
2023-01-23 03:25:45 +00:00
|
|
|
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStep event.
|
|
|
|
*/
|
2024-06-20 03:47:11 +00:00
|
|
|
public static function resumeCountdown():Void
|
2023-01-23 03:25:45 +00:00
|
|
|
{
|
|
|
|
if (countdownTimer != null && !countdownTimer.finished)
|
|
|
|
{
|
|
|
|
countdownTimer.active = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the countdown at the current step. You will have to restart it again later.
|
2023-06-08 20:30:45 +00:00
|
|
|
*
|
2023-01-23 03:25:45 +00:00
|
|
|
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStart event.
|
|
|
|
*/
|
2024-06-20 03:47:11 +00:00
|
|
|
public static function stopCountdown():Void
|
2023-01-23 03:25:45 +00:00
|
|
|
{
|
|
|
|
if (countdownTimer != null)
|
|
|
|
{
|
|
|
|
countdownTimer.cancel();
|
|
|
|
countdownTimer.destroy();
|
|
|
|
countdownTimer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the current countdown, then starts the song for you.
|
|
|
|
*/
|
2024-06-20 03:47:11 +00:00
|
|
|
public static function skipCountdown():Void
|
2023-01-23 03:25:45 +00:00
|
|
|
{
|
|
|
|
stopCountdown();
|
|
|
|
// This will trigger PlayState.startSong()
|
2023-12-14 21:56:20 +00:00
|
|
|
Conductor.instance.update(0);
|
2023-01-23 03:25:45 +00:00
|
|
|
// PlayState.isInCountdown = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resets the countdown. Only works if it's already running.
|
|
|
|
*/
|
|
|
|
public static function resetCountdown()
|
|
|
|
{
|
|
|
|
if (countdownTimer != null)
|
|
|
|
{
|
|
|
|
countdownTimer.reset();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-13 19:45:58 +00:00
|
|
|
/**
|
|
|
|
* Reset the countdown configuration to the default.
|
|
|
|
*/
|
|
|
|
public static function reset()
|
|
|
|
{
|
2024-07-28 21:10:32 +00:00
|
|
|
noteStyle = null;
|
2024-07-13 19:45:58 +00:00
|
|
|
}
|
|
|
|
|
2024-07-28 21:10:32 +00:00
|
|
|
/**
|
|
|
|
* Retrieve the note style data (if we haven't already)
|
|
|
|
* @param noteStyleId The id of the note style to fetch. Defaults to the one used by the current PlayState.
|
|
|
|
* @param force Fetch the note style from the registry even if we've already fetched it.
|
|
|
|
*/
|
|
|
|
static function fetchNoteStyle(?noteStyleId:String, force:Bool = false):Void
|
2024-07-17 20:19:18 +00:00
|
|
|
{
|
2024-07-28 21:10:32 +00:00
|
|
|
if (noteStyle != null && !force) return;
|
|
|
|
|
|
|
|
if (noteStyleId == null) noteStyleId = PlayState.instance?.currentChart?.noteStyle;
|
|
|
|
|
|
|
|
noteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
|
|
|
|
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
|
2024-07-17 20:19:18 +00:00
|
|
|
}
|
|
|
|
|
2023-01-23 03:25:45 +00:00
|
|
|
/**
|
|
|
|
* Retrieves the graphic to use for this step of the countdown.
|
|
|
|
*/
|
2024-07-18 14:56:54 +00:00
|
|
|
public static function showCountdownGraphic(index:CountdownStep):Void
|
2023-01-23 03:25:45 +00:00
|
|
|
{
|
2024-07-28 21:10:32 +00:00
|
|
|
fetchNoteStyle();
|
2023-01-23 03:25:45 +00:00
|
|
|
|
2024-07-28 21:10:32 +00:00
|
|
|
var countdownSprite = noteStyle.buildCountdownSprite(index);
|
|
|
|
if (countdownSprite == null) return;
|
2024-07-13 19:45:58 +00:00
|
|
|
|
|
|
|
var fadeEase = FlxEase.cubeInOut;
|
2024-07-28 21:10:32 +00:00
|
|
|
if (noteStyle.isCountdownSpritePixel(index)) fadeEase = EaseUtil.stepped(8);
|
2023-01-23 03:25:45 +00:00
|
|
|
|
|
|
|
// Fade sprite in, then out, then destroy it.
|
2024-07-18 14:56:54 +00:00
|
|
|
FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000,
|
2023-01-23 03:25:45 +00:00
|
|
|
{
|
2024-07-18 14:56:54 +00:00
|
|
|
ease: fadeEase,
|
2023-06-06 21:38:31 +00:00
|
|
|
onComplete: function(twn:FlxTween) {
|
2023-01-23 03:25:45 +00:00
|
|
|
countdownSprite.destroy();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-07-28 21:10:32 +00:00
|
|
|
countdownSprite.cameras = [PlayState.instance.camHUD];
|
2023-01-23 03:25:45 +00:00
|
|
|
PlayState.instance.add(countdownSprite);
|
2024-07-18 14:56:54 +00:00
|
|
|
countdownSprite.screenCenter();
|
2024-07-16 17:53:12 +00:00
|
|
|
|
2024-07-28 21:10:32 +00:00
|
|
|
var offsets = noteStyle.getCountdownSpriteOffsets(index);
|
|
|
|
countdownSprite.x += offsets[0];
|
|
|
|
countdownSprite.y += offsets[1];
|
2024-07-13 19:45:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves the sound file to use for this step of the countdown.
|
|
|
|
*/
|
2024-07-28 21:10:32 +00:00
|
|
|
public static function playCountdownSound(step:CountdownStep):FunkinSound
|
2024-07-13 19:45:58 +00:00
|
|
|
{
|
2024-07-17 20:19:18 +00:00
|
|
|
fetchNoteStyle();
|
2024-07-28 21:10:32 +00:00
|
|
|
var path = noteStyle.getCountdownSoundPath(step);
|
|
|
|
if (path == null) return null;
|
2024-07-16 17:53:12 +00:00
|
|
|
|
2024-07-28 21:10:32 +00:00
|
|
|
return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME);
|
2023-01-23 03:25:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public static function decrement(step:CountdownStep):CountdownStep
|
|
|
|
{
|
|
|
|
switch (step)
|
|
|
|
{
|
|
|
|
case BEFORE:
|
|
|
|
return THREE;
|
|
|
|
case THREE:
|
|
|
|
return TWO;
|
|
|
|
case TWO:
|
|
|
|
return ONE;
|
|
|
|
case ONE:
|
|
|
|
return GO;
|
|
|
|
case GO:
|
|
|
|
return AFTER;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return AFTER;
|
|
|
|
}
|
|
|
|
}
|
2022-03-13 18:36:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The countdown step.
|
|
|
|
* This can't be an enum abstract because scripts may need it.
|
|
|
|
*/
|
|
|
|
enum CountdownStep
|
2022-03-11 06:30:01 +00:00
|
|
|
{
|
2023-01-23 03:25:45 +00:00
|
|
|
BEFORE;
|
|
|
|
THREE;
|
|
|
|
TWO;
|
|
|
|
ONE;
|
|
|
|
GO;
|
|
|
|
AFTER;
|
2022-03-11 06:30:01 +00:00
|
|
|
}
|