Funkin/source/funkin/play/Countdown.hx

369 lines
10 KiB
Haxe
Raw Normal View History

package funkin.play;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;
2024-06-19 23:47:11 -04:00
import funkin.util.EaseUtil;
2024-03-23 17:50:48 -04:00
import funkin.audio.FunkinSound;
2024-07-13 15:45:58 -04:00
import openfl.utils.Assets;
2024-07-16 13:53:12 -04:00
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
class Countdown
{
/**
* The current step of the countdown.
*/
public static var countdownStep(default, null):CountdownStep = BEFORE;
2024-07-13 15:45:58 -04:00
/**
2024-07-16 13:53:12 -04: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 15:45:58 -04: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 13:53:12 -04:00
static var noteStyle:NoteStyle;
2024-07-17 16:19:18 -04:00
static var fallbackNoteStyle:Null<NoteStyle>;
2024-07-16 13:53:12 -04:00
static var isPixel:Bool = false;
/**
* 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 15:45:58 -04:00
public static function performCountdown():Bool
{
countdownStep = BEFORE;
var cancelled:Bool = propagateCountdownEvent(countdownStep);
if (cancelled)
{
return false;
}
// Stop any existing countdown.
stopCountdown();
PlayState.instance.isInCountdown = true;
Conductor.instance.update(PlayState.instance.startTimestamp + Conductor.instance.beatLengthMs * -5);
// Handle onBeatHit events manually
// @:privateAccess
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
// The timer function gets called based on the beat of the song.
countdownTimer = new FlxTimer();
countdownTimer.start(Conductor.instance.beatLengthMs / 1000, function(tmr:FlxTimer) {
if (PlayState.instance == null)
{
tmr.cancel();
return;
}
countdownStep = decrement(countdownStep);
// onBeatHit events are now properly dispatched by the Conductor even at negative timestamps,
// so calling this is no longer necessary.
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
// Countdown graphic.
2024-07-18 10:56:54 -04:00
showCountdownGraphic(countdownStep);
// Countdown sound.
2024-07-18 10:56:54 -04:00
playCountdownSound(countdownStep);
// Event handling bullshit.
var cancelled:Bool = propagateCountdownEvent(countdownStep);
if (cancelled)
{
pauseCountdown();
}
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:
event = new CountdownScriptEvent(COUNTDOWN_START, index);
case THREE | TWO | ONE | GO: // I didn't know you could use `|` in a switch/case block!
event = new CountdownScriptEvent(COUNTDOWN_STEP, index);
case AFTER:
event = new CountdownScriptEvent(COUNTDOWN_END, index, false);
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 16:30:45 -04: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-19 23:47:11 -04:00
public static function pauseCountdown():Void
{
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 16:30:45 -04: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-19 23:47:11 -04:00
public static function resumeCountdown():Void
{
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 16:30:45 -04: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-19 23:47:11 -04:00
public static function stopCountdown():Void
{
if (countdownTimer != null)
{
countdownTimer.cancel();
countdownTimer.destroy();
countdownTimer = null;
}
}
/**
* Stops the current countdown, then starts the song for you.
*/
2024-06-19 23:47:11 -04:00
public static function skipCountdown():Void
{
stopCountdown();
// This will trigger PlayState.startSong()
Conductor.instance.update(0);
// 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 15:45:58 -04:00
/**
* Reset the countdown configuration to the default.
*/
public static function reset()
{
2024-07-16 13:53:12 -04:00
noteStyle = NoteStyleRegistry.instance.fetchDefault();
isPixel = false;
2024-07-13 15:45:58 -04:00
}
2024-07-17 16:19:18 -04:00
static function fetchNoteStyle():Void
{
var fetchedNoteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(PlayState.instance.currentChart.noteStyle);
if (fetchedNoteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
else
noteStyle = fetchedNoteStyle;
fallbackNoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle.getFallbackID());
isPixel = false;
}
/**
* Retrieves the graphic to use for this step of the countdown.
*/
2024-07-18 10:56:54 -04:00
public static function showCountdownGraphic(index:CountdownStep):Void
{
2024-07-16 13:53:12 -04:00
var indexString:String = null;
switch (index)
{
case TWO:
indexString = 'ready';
case ONE:
indexString = 'set';
case GO:
indexString = 'go';
default:
// null
}
if (indexString == null) return;
var spritePath:String = null;
2024-07-18 10:56:54 -04:00
spritePath = resolveGraphicPath(indexString);
if (spritePath == null) return;
var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
countdownSprite.scrollFactor.set(0, 0);
2024-07-13 15:45:58 -04:00
if (isGraphicPixel) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
2024-07-16 13:53:12 -04:00
else
countdownSprite.setGraphicSize(Std.int(countdownSprite.width * 0.7));
2024-07-13 15:45:58 -04:00
var fadeEase = FlxEase.cubeInOut;
if (isGraphicPixel) fadeEase = EaseUtil.stepped(8);
2024-07-17 16:19:18 -04:00
countdownSprite.antialiasing = !isPixel;
countdownSprite.cameras = [PlayState.instance.camHUD];
countdownSprite.updateHitbox();
// Fade sprite in, then out, then destroy it.
2024-07-18 10:56:54 -04:00
FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000,
{
2024-07-18 10:56:54 -04:00
ease: fadeEase,
onComplete: function(twn:FlxTween) {
countdownSprite.destroy();
}
});
PlayState.instance.add(countdownSprite);
2024-07-18 10:56:54 -04:00
countdownSprite.screenCenter();
}
2024-07-18 10:56:54 -04:00
static function resolveGraphicPath(index:String):Null<String>
{
2024-07-17 16:19:18 -04:00
fetchNoteStyle();
2024-07-13 15:45:58 -04:00
var basePath:String = 'ui/countdown/';
2024-07-16 13:53:12 -04:00
var spritePath:String = basePath + noteStyle.id + '/$index';
2024-07-17 16:19:18 -04:00
while (!Assets.exists(Paths.image(spritePath)) && fallbackNoteStyle != null)
{
noteStyle = fallbackNoteStyle;
fallbackNoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle.getFallbackID());
spritePath = basePath + noteStyle.id + '/$index';
}
if (noteStyle.isHoldNotePixel()) isPixel = true;
// If ABSOLUTELY nothing is found, revert it to default notestyle skin
2024-07-16 13:53:12 -04:00
if (!Assets.exists(Paths.image(spritePath)))
{
2024-07-16 13:53:12 -04:00
if (!isPixel) spritePath = basePath + Constants.DEFAULT_NOTE_STYLE + '/$index';
else
spritePath = basePath + Constants.DEFAULT_PIXEL_NOTE_STYLE + '/$index';
}
2024-07-16 13:53:12 -04:00
2024-07-13 15:45:58 -04:00
trace('Resolved sprite path: ' + Paths.image(spritePath));
return spritePath;
}
/**
* Retrieves the sound file to use for this step of the countdown.
*/
2024-07-18 10:56:54 -04:00
public static function playCountdownSound(step:CountdownStep):Void
2024-07-13 15:45:58 -04:00
{
2024-07-18 10:56:54 -04:00
return FunkinSound.playOnce(Paths.sound(resolveSoundPath(step)), Constants.COUNTDOWN_VOLUME);
2024-07-13 15:45:58 -04:00
}
2024-07-18 10:56:54 -04:00
static function resolveSoundPath(step:CountdownStep):Null<String>
2024-07-13 15:45:58 -04:00
{
2024-07-16 13:53:12 -04:00
if (step == CountdownStep.BEFORE || step == CountdownStep.AFTER) return null;
2024-07-17 16:19:18 -04:00
fetchNoteStyle();
2024-07-16 13:53:12 -04:00
var basePath:String = 'gameplay/countdown/';
var soundPath:String = basePath + noteStyle.id + '/intro$step';
2024-07-17 16:19:18 -04:00
while (!Assets.exists(Paths.sound(soundPath)) && fallbackNoteStyle != null)
{
noteStyle = fallbackNoteStyle;
fallbackNoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle.getFallbackID());
soundPath = basePath + noteStyle.id + '/intro$step';
}
if (noteStyle.isHoldNotePixel()) isPixel = true;
// If ABSOLUTELY nothing is found, revert it to default notestyle sound
2024-07-16 13:53:12 -04:00
if (!Assets.exists(Paths.sound(soundPath)))
2024-07-13 15:45:58 -04:00
{
2024-07-16 13:53:12 -04:00
if (!isPixel) soundPath = basePath + Constants.DEFAULT_NOTE_STYLE + '/intro$step';
else
soundPath = basePath + Constants.DEFAULT_PIXEL_NOTE_STYLE + '/intro$step';
2024-07-13 15:45:58 -04:00
}
2024-07-16 13:53:12 -04:00
2024-07-13 15:45:58 -04:00
trace('Resolved sound path: ' + soundPath);
return soundPath;
}
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;
}
}
}
/**
* The countdown step.
* This can't be an enum abstract because scripts may need it.
*/
enum CountdownStep
{
BEFORE;
THREE;
TWO;
ONE;
GO;
AFTER;
}