Funkin/source/funkin/play/PlayState.hx

2959 lines
86 KiB
Haxe
Raw Normal View History

package funkin.play;
2020-10-03 02:50:15 -04:00
import flixel.addons.display.FlxPieDial;
2024-02-12 16:50:29 -05:00
import flixel.addons.display.FlxPieDial;
import flixel.addons.transition.FlxTransitionableState;
2024-02-12 16:50:29 -05:00
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.transition.FlxTransitionableSubState;
import flixel.addons.transition.FlxTransitionableSubState;
import flixel.addons.transition.Transition;
import flixel.addons.transition.Transition;
2020-10-25 16:51:06 -04:00
import flixel.FlxCamera;
2020-10-04 02:42:58 -04:00
import flixel.FlxObject;
2020-10-03 02:50:15 -04:00
import flixel.FlxState;
import funkin.graphics.FunkinSprite;
2020-10-09 17:24:20 -04:00
import flixel.FlxSubState;
import funkin.graphics.FunkinSprite;
2020-10-05 05:48:30 -04:00
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
2021-01-16 17:21:06 -05:00
import flixel.math.FlxRect;
import funkin.graphics.FunkinSprite;
2020-10-03 02:50:15 -04:00
import flixel.text.FlxText;
2020-10-05 14:24:51 -04:00
import flixel.tweens.FlxEase;
2020-10-03 15:32:15 -04:00
import flixel.tweens.FlxTween;
2020-10-05 16:32:41 -04:00
import flixel.ui.FlxBar;
2020-10-04 02:42:58 -04:00
import flixel.util.FlxColor;
2020-10-03 15:32:15 -04:00
import flixel.util.FlxTimer;
2024-02-12 16:50:29 -05:00
import funkin.api.newgrounds.NGio;
import funkin.audio.VoicesGroup;
2023-09-17 07:45:10 -04:00
import funkin.audio.VoicesGroup;
2024-02-12 16:50:29 -05:00
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.notestyle.NoteStyleData;
2023-09-17 07:45:10 -04:00
import funkin.data.notestyle.NoteStyleRegistry;
2024-02-12 16:50:29 -05:00
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongRegistry;
import funkin.data.stage.StageRegistry;
import funkin.Highscore.Tallies;
2023-06-22 01:41:01 -04:00
import funkin.input.PreciseInputManager;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
2024-02-12 16:50:29 -05:00
import funkin.play.components.ComboMilestone;
import funkin.play.components.HealthIcon;
import funkin.play.components.PopUpStuff;
import funkin.play.cutscene.dialogue.Conversation;
2023-06-16 17:37:56 -04:00
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.cutscene.VideoCutscene;
2023-06-22 01:41:01 -04:00
import funkin.play.notes.NoteDirection;
2024-02-12 16:50:29 -05:00
import funkin.play.notes.NoteSplash;
2023-09-17 07:45:10 -04:00
import funkin.play.notes.NoteSprite;
2024-02-12 16:50:29 -05:00
import funkin.play.notes.NoteSprite;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.notestyle.NoteStyle;
2023-06-22 01:41:01 -04:00
import funkin.play.notes.Strumline;
import funkin.play.notes.SustainTrail;
import funkin.play.scoring.Scoring;
2022-09-22 06:34:03 -04:00
import funkin.play.song.Song;
import funkin.play.stage.Stage;
2024-02-12 16:50:29 -05:00
import funkin.save.Save;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.debug.stage.StageOffsetSubState;
2024-02-12 16:50:29 -05:00
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.options.PreferencesMenu;
2023-06-16 17:37:56 -04:00
import funkin.ui.story.StoryMenuState;
2024-02-13 01:38:11 -05:00
import funkin.graphics.FunkinCamera;
2024-02-12 16:50:29 -05:00
import funkin.ui.transition.LoadingState;
import funkin.util.SerializerUtil;
2024-02-12 16:50:29 -05:00
import haxe.Int64;
2022-03-09 10:29:03 -05:00
import lime.ui.Haptic;
2024-02-12 16:50:29 -05:00
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
import openfl.Lib;
2021-03-22 10:09:46 -04:00
#if discord_rpc
import Discord.DiscordClient;
#end
/**
* Parameters used to initialize the PlayState.
*/
typedef PlayStateParams =
{
/**
* The song to play.
*/
targetSong:Song,
/**
* The difficulty to play the song on.
* @default `Constants.DEFAULT_DIFFICULTY`
*/
?targetDifficulty:String,
/**
* The variation to play on.
* @default `Constants.DEFAULT_VARIATION`
*/
?targetVariation:String,
2024-02-05 13:35:30 -05:00
/**
* The instrumental to play with.
* Significant if the `targetSong` supports alternate instrumentals.
* @default `null`
*/
?targetInstrumental:String,
/**
* Whether the song should start in Practice Mode.
* @default `false`
*/
?practiceMode:Bool,
/**
* Whether the song should be in minimal mode.
* @default `false`
*/
?minimalMode:Bool,
/**
* If specified, the game will jump to the specified timestamp after the countdown ends.
* @default `0.0`
*/
?startTimestamp:Float,
/**
* If specified, the game will play the song with the given speed.
* @default `1.0` for 100% speed.
*/
?playbackRate:Float,
/**
* If specified, the game will not load the instrumental or vocal tracks,
* and must be loaded externally.
*/
?overrideMusic:Bool,
/**
* The initial camera follow point.
* Used to persist the position of the `cameraFollowPosition` between levels.
*/
?cameraFollowPoint:FlxPoint,
}
/**
* The gameplay state, where all the rhythm gaming happens.
* SubState so it can be loaded as a child of the chart editor.
*/
class PlayState extends MusicBeatSubState
2020-10-03 02:50:15 -04:00
{
2023-01-22 19:55:30 -05:00
/**
* STATIC VARIABLES
* Static variables should be used for information that must be persisted between states or between resets,
* such as the active song or song playlist.
*/
/**
* The currently active PlayState.
* There should be only one PlayState in existance at a time, we can use a singleton.
2023-01-22 19:55:30 -05:00
*/
public static var instance:PlayState = null;
2023-06-22 01:41:01 -04:00
/**
* This sucks. We need this because FlxG.resetState(); assumes the constructor has no arguments.
* @see https://github.com/HaxeFlixel/flixel/issues/2541
*/
static var lastParams:PlayStateParams = null;
2023-01-22 19:55:30 -05:00
/**
* PUBLIC INSTANCE VARIABLES
* Public instance variables should be used for information that must be reset or dereferenced
* every time the state is changed, but may need to be accessed externally.
2023-01-22 19:55:30 -05:00
*/
/**
* The currently selected stage.
2023-01-22 19:55:30 -05:00
*/
public var currentSong:Song = null;
2023-01-22 19:55:30 -05:00
/**
* The currently selected difficulty.
2023-01-22 19:55:30 -05:00
*/
public var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
2023-01-22 19:55:30 -05:00
/**
* The currently selected variation.
2023-01-22 19:55:30 -05:00
*/
public var currentVariation:String = Constants.DEFAULT_VARIATION;
2023-01-22 19:55:30 -05:00
2023-01-27 02:38:37 -05:00
/**
* The currently active Stage. This is the object containing all the props.
2023-01-27 02:38:37 -05:00
*/
public var currentStage:Stage = null;
2023-01-27 02:38:37 -05:00
2023-01-22 19:55:30 -05:00
/**
* Gets set to true when the PlayState needs to reset (player opted to restart or died).
* Gets disabled once resetting happens.
*/
public var needsReset:Bool = false;
2023-01-22 19:55:30 -05:00
/**
* The current 'Blueball Counter' to display in the pause menu.
2023-01-22 19:55:30 -05:00
* Resets when you beat a song or go back to the main menu.
*/
public var deathCounter:Int = 0;
2023-01-22 19:55:30 -05:00
/**
* The player's current health.
2023-01-22 19:55:30 -05:00
*/
public var health:Float = Constants.HEALTH_STARTING;
2023-01-22 19:55:30 -05:00
/**
* The player's current score.
2023-06-22 01:41:01 -04:00
* TODO: Move this to its own class.
2023-01-22 19:55:30 -05:00
*/
public var songScore:Int = 0;
2023-01-22 19:55:30 -05:00
/**
* Start at this point in the song once the countdown is done.
* For example, if `startTimestamp` is `30000`, the song will start at the 30 second mark.
* Used for chart playtesting or practice.
*/
public var startTimestamp:Float = 0.0;
/**
* Play back the song at this speed.
* @default `1.0` for normal speed.
*/
public var playbackRate:Float = 1.0;
2023-01-22 19:55:30 -05:00
/**
* An empty FlxObject contained in the scene.
2023-06-22 01:41:01 -04:00
* The current gameplay camera will always follow this object. Tween its position to move the camera smoothly.
*
2023-06-22 01:41:01 -04:00
* It needs to be an object in the scene for the camera to be configured to follow it.
* We optionally make this a sprite so we can draw a debug graphic with it.
2023-01-22 19:55:30 -05:00
*/
2023-06-22 01:41:01 -04:00
public var cameraFollowPoint:FlxObject;
2023-01-22 19:55:30 -05:00
/**
* The camera follow point from the last stage.
* Used to persist the position of the `cameraFollowPosition` between levels.
2023-01-22 19:55:30 -05:00
*/
public var previousCameraFollowPoint:FlxPoint = null;
2023-01-22 19:55:30 -05:00
/**
* The current camera zoom level.
*
* The camera zoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect.
* Defaults to 1.05 but may be larger or smaller depending on the current stage,
* and may be changed by the `ZoomCamera` song event.
*/
public var defaultCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
2023-01-22 19:55:30 -05:00
/**
* The current HUD camera zoom level.
*
* The camera zoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect.
2023-01-22 19:55:30 -05:00
*/
public var defaultHUDCameraZoom:Float = FlxCamera.defaultZoom * 1.0;
2023-01-22 19:55:30 -05:00
/**
* Intensity of the gameplay camera zoom.
* @default `1.5%`
2023-01-22 19:55:30 -05:00
*/
public var cameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY;
2023-01-22 19:55:30 -05:00
/**
* Intensity of the HUD camera zoom.
* @default `3.0%`
2023-01-22 19:55:30 -05:00
*/
public var hudCameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY * 2.0;
2023-01-22 19:55:30 -05:00
/**
* How many beats (quarter notes) between camera zooms.
* @default One camera zoom per measure (four beats).
2023-01-22 19:55:30 -05:00
*/
public var cameraZoomRate:Int = Constants.DEFAULT_ZOOM_RATE;
/**
* Whether the game is currently in the countdown before the song resumes.
*/
public var isInCountdown:Bool = false;
/**
* Whether the game is currently in Practice Mode.
* If true, player will not lose gain or lose score from notes.
*/
public var isPracticeMode:Bool = false;
/**
* Whether the player has dropped below zero health,
* and we are just waiting for an animation to play out before transitioning.
*/
public var isPlayerDying:Bool = false;
/**
* In Minimal Mode, the stage and characters are not loaded and a standard background is used.
*/
public var isMinimalMode:Bool = false;
/**
* Whether the game is currently in an animated cutscene, and gameplay should be stopped.
*/
public var isInCutscene:Bool = false;
/**
* Whether the inputs should be disabled for whatever reason... used for the stage edit lol!
*/
public var disableKeys:Bool = false;
2023-01-22 19:55:30 -05:00
2023-08-31 18:47:23 -04:00
public var isSubState(get, never):Bool;
function get_isSubState():Bool
{
return this._parentState != null;
}
2023-08-31 18:47:23 -04:00
public var isChartingMode(get, never):Bool;
function get_isChartingMode():Bool
{
return this._parentState != null && Std.isOfType(this._parentState, ChartEditorState);
}
2023-06-16 17:37:56 -04:00
/**
* The current dialogue.
*/
public var currentConversation:Conversation;
2023-06-22 01:41:01 -04:00
/**
* Key press inputs which have been received but not yet processed.
* These are encoded with an OS timestamp, so they
**/
var inputPressQueue:Array<PreciseInputEvent> = [];
/**
* Key release inputs which have been received but not yet processed.
* These are encoded with an OS timestamp, so they
**/
var inputReleaseQueue:Array<PreciseInputEvent> = [];
2024-02-28 00:19:08 -05:00
/**
* If we just unpaused the game, we shouldn't be able to pause again for one frame.
*/
var justUnpaused:Bool = false;
2023-01-22 19:55:30 -05:00
/**
* PRIVATE INSTANCE VARIABLES
* Private instance variables should be used for information that must be reset or dereferenced
* every time the state is reset, but should not be accessed externally.
*/
/**
* The Array containing the upcoming song events.
* The `update()` function regularly shifts these out to trigger events.
*/
var songEvents:Array<SongEventData>;
2023-01-22 19:55:30 -05:00
/**
* If true, the player is allowed to pause the game.
* Disabled during the ending of a song.
*/
var mayPauseGame:Bool = true;
2023-01-22 19:55:30 -05:00
/**
* The displayed value of the player's health.
* Used to provide smooth animations based on linear interpolation of the player's health.
*/
var healthLerp:Float = Constants.HEALTH_STARTING;
2023-01-22 19:55:30 -05:00
/**
* How long the user has held the "Skip Video Cutscene" button for.
*/
var skipHeldTimer:Float = 0;
/**
* Whether the PlayState was started with instrumentals and vocals already provided.
* Used by the chart editor to prevent replacing the music.
*/
var overrideMusic:Bool = false;
2023-01-22 19:55:30 -05:00
/**
* Forcibly disables all update logic while the game moves back to the Menu state.
* This is used only when a critical error occurs and the game absolutely cannot continue.
2023-01-22 19:55:30 -05:00
*/
var criticalFailure:Bool = false;
2023-01-22 19:55:30 -05:00
/**
* False as long as the countdown has not finished yet.
*/
var startingSong:Bool = false;
/**
* False if `FlxG.sound.music`
*/
var musicPausedBySubState:Bool = false;
/**
* False until `create()` has completed.
*/
var initialized:Bool = false;
/**
* A group of audio tracks, used to play the song's vocals.
*/
public var vocals:VoicesGroup;
2023-06-22 01:41:01 -04:00
#if discord_rpc
// Discord RPC variables
var storyDifficultyText:String = '';
var iconRPC:String = '';
var detailsText:String = '';
var detailsPausedText:String = '';
#end
2023-01-22 19:55:30 -05:00
/**
* RENDER OBJECTS
*/
/**
* The FlxText which displays the current score.
*/
var scoreText:FlxText;
2023-01-22 19:55:30 -05:00
/**
* The bar which displays the player's health.
* Dynamically updated based on the value of `healthLerp` (which is based on `health`).
*/
public var healthBar:FlxBar;
/**
* The background image used for the health bar.
* Emma says the image is slightly skewed so I'm leaving it as an image instead of a `createGraphic`.
*/
public var healthBarBG:FunkinSprite;
2023-01-22 19:55:30 -05:00
/**
* The health icon representing the player.
*/
public var iconP1:HealthIcon;
/**
* The health icon representing the opponent.
*/
public var iconP2:HealthIcon;
/**
* The sprite group containing active player's strumline notes.
*/
public var playerStrumline:Strumline;
/**
* The sprite group containing opponent's strumline notes.
*/
2023-06-22 01:41:01 -04:00
public var opponentStrumline:Strumline;
2023-01-22 19:55:30 -05:00
/**
* The camera which contains, and controls visibility of, the user interface elements.
*/
public var camHUD:FlxCamera;
/**
* The camera which contains, and controls visibility of, the stage and characters.
*/
2024-02-13 01:38:11 -05:00
public var camGame:FlxCamera;
2023-01-22 19:55:30 -05:00
/**
* The camera which contains, and controls visibility of, a video cutscene.
*/
public var camCutscene:FlxCamera;
2023-06-22 01:41:01 -04:00
/**
* The combo popups. Includes the real-time combo counter and the rating.
*/
var comboPopUps:PopUpStuff;
2023-01-22 19:55:30 -05:00
/**
* PROPERTIES
*/
/**
* If a substate is rendering over the PlayState, it is paused and normal update logic is skipped.
* Examples include:
* - The Pause screen is open.
* - The Game Over screen is open.
* - The Chart Editor screen is open.
*/
var isGamePaused(get, never):Bool;
2023-01-22 19:55:30 -05:00
function get_isGamePaused():Bool
{
// Note: If there is a substate which requires the game to act unpaused,
// this should be changed to include something like `&& Std.isOfType()`
return this.subState != null;
}
2024-02-13 01:38:11 -05:00
var isExitingViaPauseMenu(get, never):Bool;
function get_isExitingViaPauseMenu():Bool
{
if (this.subState == null) return false;
if (!Std.isOfType(this.subState, PauseSubState)) return false;
var pauseSubState:PauseSubState = cast this.subState;
2024-02-28 00:19:08 -05:00
return !pauseSubState.allowInput;
2024-02-13 01:38:11 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Data for the current difficulty for the current song.
* Includes chart data, scroll speed, and other information.
*/
2023-08-31 18:47:23 -04:00
public var currentChart(get, never):SongDifficulty;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
function get_currentChart():SongDifficulty
{
if (currentSong == null || currentDifficulty == null) return null;
return currentSong.getDifficulty(currentDifficulty, currentVariation);
2023-06-22 01:41:01 -04:00
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
/**
* The internal ID of the currently active Stage.
* Used to retrieve the data required to build the `currentStage`.
*/
2023-08-31 18:47:23 -04:00
public var currentStageId(get, never):String;
2023-06-22 01:41:01 -04:00
function get_currentStageId():String
{
if (currentChart == null || currentChart.stage == null || currentChart.stage == '') return Constants.DEFAULT_STAGE;
return currentChart.stage;
}
2023-01-22 19:55:30 -05:00
/**
2023-06-22 01:41:01 -04:00
* The length of the current song, in milliseconds.
*/
2023-06-22 01:41:01 -04:00
var currentSongLengthMs(get, never):Float;
function get_currentSongLengthMs():Float
{
return FlxG?.sound?.music?.length;
}
// TODO: Refactor or document
var generatedMusic:Bool = false;
var perfectMode:Bool = false;
static final BACKGROUND_COLOR:FlxColor = FlxColor.MAGENTA;
2023-06-22 01:41:01 -04:00
/**
* Instantiate a new PlayState.
* @param params The parameters used to initialize the PlayState.
* Includes information about what song to play and more.
*/
public function new(params:PlayStateParams)
2023-01-22 19:55:30 -05:00
{
super();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Validate parameters.
if (params == null && lastParams == null)
2023-01-22 19:55:30 -05:00
{
throw 'PlayState constructor called with no available parameters.';
2023-01-22 19:55:30 -05:00
}
else if (params == null)
{
trace('WARNING: PlayState constructor called with no parameters. Reusing previous parameters.');
params = lastParams;
}
else
{
lastParams = params;
}
2023-06-22 01:41:01 -04:00
// Apply parameters.
currentSong = params.targetSong;
if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty;
if (params.targetVariation != null) currentVariation = params.targetVariation;
isPracticeMode = params.practiceMode ?? false;
isMinimalMode = params.minimalMode ?? false;
startTimestamp = params.startTimestamp ?? 0.0;
playbackRate = params.playbackRate ?? 1.0;
overrideMusic = params.overrideMusic ?? false;
previousCameraFollowPoint = params.cameraFollowPoint;
2023-06-22 01:41:01 -04:00
// Don't do anything else here! Wait until create() when we attach to the camera.
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
/**
* Called when the PlayState is switched to.
*/
public override function create():Void
{
if (instance != null)
{
2023-06-22 01:41:01 -04:00
// TODO: Do something in this case? IDK.
trace('WARNING: PlayState instance already exists. This should not happen.');
}
2023-01-22 19:55:30 -05:00
instance = this;
if (!assertChartExists()) return;
2023-01-22 19:55:30 -05:00
// TODO: Add something to toggle this on!
2023-06-22 01:41:01 -04:00
if (false)
{
// Displays the camera follow point as a sprite for debug purposes.
var cameraFollowPoint = new FunkinSprite(0, 0);
cameraFollowPoint.makeSolidColor(8, 8, 0xFF00FF00);
2023-06-22 01:41:01 -04:00
cameraFollowPoint.visible = false;
cameraFollowPoint.zIndex = 1000000;
this.cameraFollowPoint = cameraFollowPoint;
2023-06-22 01:41:01 -04:00
}
else
{
// Camera follow point is an invisible point in space.
cameraFollowPoint = new FlxObject(0, 0);
}
2023-01-22 19:55:30 -05:00
// Reduce physics accuracy (who cares!!!) to improve animation quality.
FlxG.fixedTimestep = false;
// This state receives update() even when a substate is active.
this.persistentUpdate = true;
// This state receives draw calls even when a substate is active.
this.persistentDraw = true;
// Stop any pre-existing music.
if (!overrideMusic && FlxG.sound.music != null) FlxG.sound.music.stop();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Prepare the current song's instrumental and vocals to be played.
if (!overrideMusic && currentChart != null)
2023-01-22 19:55:30 -05:00
{
currentChart.cacheInst();
currentChart.cacheVocals();
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
// Prepare the Conductor.
Conductor.instance.forceBPM(null);
2023-12-08 01:57:46 -05:00
if (currentChart.offsets != null)
{
Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset();
2023-12-08 01:57:46 -05:00
}
Conductor.instance.mapTimeChanges(currentChart.timeChanges);
Conductor.instance.update((Conductor.instance.beatLengthMs * -5) + startTimestamp);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// The song is now loaded. We can continue to initialize the play state.
initCameras();
initHealthBar();
if (!isMinimalMode)
{
initStage();
initCharacters();
}
else
{
initMinimalMode();
}
2023-06-22 01:41:01 -04:00
initStrumlines();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Initialize the judgements and combo meter.
2023-01-22 19:55:30 -05:00
comboPopUps = new PopUpStuff();
comboPopUps.zIndex = 900;
2023-01-22 19:55:30 -05:00
add(comboPopUps);
comboPopUps.cameras = [camHUD];
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
#if discord_rpc
// Initialize Discord Rich Presence.
initDiscord();
#end
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Read the song's note data and pass it to the strumlines.
generateSong();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Reset the camera's zoom and force it to focus on the camera follow point.
2023-01-22 19:55:30 -05:00
resetCamera();
2023-06-22 01:41:01 -04:00
initPreciseInputs();
2023-06-22 01:41:01 -04:00
FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// The song is loaded and in the process of starting.
// This gets set back to false when the chart actually starts.
2023-01-22 19:55:30 -05:00
startingSong = true;
2023-06-22 01:41:01 -04:00
// TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead.
if ((currentSong?.id ?? '').toLowerCase() == 'winter-horrorland')
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// VanillaCutscenes will call startCountdown later.
VanillaCutscenes.playHorrorStartCutscene();
2023-01-22 19:55:30 -05:00
}
else
{
2023-06-22 01:41:01 -04:00
// Call a script event to start the countdown.
// Songs with cutscenes should call event.cancel().
// As long as they call `PlayState.instance.startCountdown()` later, the countdown will start.
2023-01-22 19:55:30 -05:00
startCountdown();
}
// Do this last to prevent beatHit from being called before create() is done.
super.create();
2023-06-22 01:41:01 -04:00
leftWatermarkText.cameras = [camHUD];
rightWatermarkText.cameras = [camHUD];
// Initialize some debug stuff.
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-06-22 01:41:01 -04:00
// Display the version number (and git commit hash) in the bottom right corner.
2023-01-22 19:55:30 -05:00
this.rightWatermarkText.text = Constants.VERSION;
FlxG.console.registerObject('playState', this);
#end
initialized = true;
2023-01-22 19:55:30 -05:00
}
public override function draw():Void
{
// if (FlxG.renderBlit)
// {
// camGame.fill(BACKGROUND_COLOR);
// }
// else if (FlxG.renderTile)
// {
// FlxG.log.warn("PlayState background not displayed properly on tile renderer!");
// }
// else
// {
// FlxG.log.warn("PlayState background not displayed properly, unknown renderer!");
// }
super.draw();
}
function assertChartExists():Bool
{
// Returns null if the song failed to load or doesn't have the selected difficulty.
if (currentSong == null || currentChart == null || currentChart.notes == null)
{
// We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic.
criticalFailure = true;
// Choose an error message.
var message:String = 'There was a critical error. Click OK to return to the main menu.';
if (currentSong == null)
{
message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.';
}
else if (currentDifficulty == null)
{
message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.';
}
else if (currentChart == null)
{
message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
}
else if (currentChart.notes == null)
{
message = 'The was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
}
// Display a popup. This blocks the application until the user clicks OK.
lime.app.Application.current.window.alert(message, 'Error loading PlayState');
// Force the user back to the main menu.
if (isSubState)
{
this.close();
}
else
{
FlxG.switchState(() -> new MainMenuState());
}
return false;
}
return true;
}
2023-06-22 01:41:01 -04:00
public override function update(elapsed:Float):Void
2023-01-22 19:55:30 -05:00
{
2024-02-28 00:19:08 -05:00
// TOTAL: 9.42% CPU Time when profiled in VS 2019.
2023-06-22 01:41:01 -04:00
if (criticalFailure) return;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
super.update(elapsed);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
updateHealthBar();
updateScoreText();
// Handle restarting the song when needed (player death or pressing Retry)
if (needsReset)
2023-01-22 19:55:30 -05:00
{
if (!assertChartExists()) return;
dispatchEvent(new ScriptEvent(SONG_RETRY));
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
resetCamera();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
persistentUpdate = true;
persistentDraw = true;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
startingSong = true;
isPlayerDying = false;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
inputSpitter = [];
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Reset music properly.
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
FlxG.sound.music.pitch = playbackRate;
2023-06-22 01:41:01 -04:00
FlxG.sound.music.pause();
2023-10-18 01:02:10 -04:00
if (!overrideMusic)
{
// Stop the vocals if they already exist.
if (vocals != null) vocals.stop();
2023-10-18 01:02:10 -04:00
vocals = currentChart.buildVocals();
if (vocals.members.length == 0)
{
trace('WARNING: No vocals found for this song.');
}
}
vocals.pause();
2023-06-22 01:41:01 -04:00
vocals.time = 0;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
FlxG.sound.music.volume = 1;
vocals.volume = 1;
vocals.playerVolume = 1;
vocals.opponentVolume = 1;
2023-01-22 19:55:30 -05:00
if (currentStage != null) currentStage.resetStage();
2023-06-22 01:41:01 -04:00
2023-07-31 13:42:13 -04:00
playerStrumline.vwooshNotes();
opponentStrumline.vwooshNotes();
playerStrumline.clean();
opponentStrumline.clean();
2023-06-22 01:41:01 -04:00
// Delete all notes and reset the arrays.
regenNoteData();
// Reset camera zooming
cameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY;
hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * 2.0;
cameraZoomRate = Constants.DEFAULT_ZOOM_RATE;
health = Constants.HEALTH_STARTING;
2023-06-22 01:41:01 -04:00
songScore = 0;
Highscore.tallies.combo = 0;
Countdown.performCountdown(currentStageId.startsWith('school'));
needsReset = false;
}
// Update the conductor.
if (startingSong)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
if (isInCountdown)
{
Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000);
if (Conductor.instance.songPosition >= (startTimestamp)) startSong();
2023-06-22 01:41:01 -04:00
}
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
else
{
if (Constants.EXT_SOUND == 'mp3')
{
Conductor.instance.formatOffset = Constants.MP3_DELAY_MS;
}
else
{
Conductor.instance.formatOffset = 0.0;
}
2023-01-22 19:55:30 -05:00
Conductor.instance.update(); // Normal conductor update.
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
var androidPause:Bool = false;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
#if android
androidPause = FlxG.android.justPressed.BACK;
#end
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Attempt to pause the game.
2024-02-28 00:19:08 -05:00
if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame && !justUnpaused)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000));
2023-06-22 01:41:01 -04:00
dispatchEvent(event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (!event.eventCanceled)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// Pause updates while the substate is open, preventing the game state from advancing.
persistentUpdate = false;
// Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu.
persistentDraw = true;
2023-06-22 01:41:01 -04:00
// There is a 1/1000 change to use a special pause menu.
// This prevents the player from resuming, but that's the point.
// It's a reference to Gitaroo Man, which doesn't let you pause the game.
if (!isSubState && event.gitaroo)
2023-06-22 01:41:01 -04:00
{
FlxG.switchState(() -> new GitarooPause(
2023-06-22 01:41:01 -04:00
{
targetSong: currentSong,
targetDifficulty: currentDifficulty,
targetVariation: currentVariation,
2023-06-22 01:41:01 -04:00
}));
}
else
{
var boyfriendPos:FlxPoint = new FlxPoint(0, 0);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Prevent the game from crashing if Boyfriend isn't present.
if (currentStage != null && currentStage.getBoyfriend() != null)
{
boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
}
2024-02-28 00:19:08 -05:00
var pauseSubState:FlxSubState = new PauseSubState({mode: isChartingMode ? Charting : Standard});
2023-06-22 01:41:01 -04:00
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
2023-06-22 01:41:01 -04:00
pauseSubState.camera = camHUD;
2024-02-28 00:19:08 -05:00
openSubState(pauseSubState);
2023-06-22 01:41:01 -04:00
// boyfriendPos.put(); // TODO: Why is this here?
}
#if discord_rpc
DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
#end
2023-01-22 19:55:30 -05:00
}
}
2023-06-22 01:41:01 -04:00
// Cap health.
if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX;
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Lerp the camera zoom towards the target level.
if (subState == null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
2023-01-22 19:55:30 -05:00
}
if (currentStage != null && currentStage.getBoyfriend() != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
}
FlxG.watch.addQuick('health', health);
2023-06-22 01:41:01 -04:00
// TODO: Add a song event for Handle GF dance speed.
2023-06-22 01:41:01 -04:00
// Handle player death.
if (!isInCutscene && !disableKeys)
2023-06-22 01:41:01 -04:00
{
// RESET = Quick Game Over Screen
if (controls.RESET)
2023-01-22 19:55:30 -05:00
{
health = Constants.HEALTH_MIN;
2023-06-22 01:41:01 -04:00
trace('RESET = True');
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
#if CAN_CHEAT // brandon's a pussy
if (controls.CHEAT)
{
health += 0.25 * Constants.HEALTH_MAX; // +25% health.
2023-06-22 01:41:01 -04:00
trace('User is cheating!');
}
#end
2023-01-22 19:55:30 -05:00
if (health <= Constants.HEALTH_MIN && !isPracticeMode && !isPlayerDying)
2023-06-22 01:41:01 -04:00
{
vocals.pause();
FlxG.sound.music.pause();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
deathCounter += 1;
2023-01-22 19:55:30 -05:00
dispatchEvent(new ScriptEvent(GAME_OVER));
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Disable updates, preventing animations in the background from playing.
persistentUpdate = false;
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-06-22 01:41:01 -04:00
if (FlxG.keys.pressed.THREE)
{
// TODO: Change the key or delete this?
// In debug builds, pressing 3 to kill the player makes the background transparent.
persistentDraw = true;
}
else
{
#end
persistentDraw = false;
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-06-22 01:41:01 -04:00
}
#end
isPlayerDying = true;
var deathPreTransitionDelay = currentStage?.getBoyfriend()?.getDeathPreTransitionDelay() ?? 0.0;
if (deathPreTransitionDelay > 0)
{
new FlxTimer().start(deathPreTransitionDelay, function(_) {
moveToGameOver();
});
}
else
{
// Transition immediately.
moveToGameOver();
}
2023-06-22 01:41:01 -04:00
#if discord_rpc
// Game Over doesn't get his own variable because it's only used here
DiscordClient.changePresence('Game Over - ' + detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
#end
}
else if (isPlayerDying)
{
// Wait up.
}
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
processSongEvents();
// Handle keybinds.
processInputQueue();
if (!isInCutscene && !disableKeys) debugKeyShit();
if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
// Moving notes into position is now done by Strumline.update().
processNotes(elapsed);
justUnpaused = false;
}
function moveToGameOver():Void
{
var gameOverSubState = new GameOverSubState(
{
isChartingMode: isChartingMode,
transparent: persistentDraw
});
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
openSubState(gameOverSubState);
}
function processSongEvents():Void
{
2023-06-22 01:41:01 -04:00
// Query and activate song events.
// TODO: Check that these work appropriately even when songPosition is less than 0, to play events during countdown.
2023-06-22 01:41:01 -04:00
if (songEvents != null && songEvents.length > 0)
{
var songEventsToActivate:Array<SongEventData> = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (songEventsToActivate.length > 0)
{
trace('Found ${songEventsToActivate.length} event(s) to activate.');
for (event in songEventsToActivate)
{
// If an event is trying to play, but it's over 1 second old, skip it.
var eventAge:Float = Conductor.instance.songPosition - event.time;
if (eventAge > 1000)
{
event.activated = true;
continue;
};
2023-06-22 01:41:01 -04:00
var eventEvent:SongEventScriptEvent = new SongEventScriptEvent(event);
dispatchEvent(eventEvent);
// Calling event.cancelEvent() skips the event. Neat!
if (!eventEvent.eventCanceled)
{
SongEventRegistry.handleEvent(event);
2023-06-22 01:41:01 -04:00
}
}
}
2023-01-22 19:55:30 -05:00
}
}
2023-06-22 01:41:01 -04:00
public override function dispatchEvent(event:ScriptEvent):Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// ORDER: Module, Stage, Character, Song, Conversation, Note
// Modules should get the first chance to cancel the event.
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// super.dispatchEvent(event) dispatches event to module scripts.
super.dispatchEvent(event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Dispatch event to stage script.
ScriptEventDispatcher.callEvent(currentStage, event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Dispatch event to character script(s).
if (currentStage != null) currentStage.dispatchToCharacters(event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Dispatch event to song script.
ScriptEventDispatcher.callEvent(currentSong, event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Dispatch event to conversation script.
ScriptEventDispatcher.callEvent(currentConversation, event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// TODO: Dispatch event to note scripts
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Function called before opening a new substate.
* @param subState The substate to open.
*/
public override function openSubState(subState:FlxSubState):Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// If there is a substate which requires the game to continue,
// then make this a condition.
var shouldPause = (Std.isOfType(subState, PauseSubState) || Std.isOfType(subState, GameOverSubState));
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (shouldPause)
{
2023-06-22 01:41:01 -04:00
// Pause the music.
if (FlxG.sound.music != null)
{
musicPausedBySubState = FlxG.sound.music.playing;
if (musicPausedBySubState)
{
FlxG.sound.music.pause();
}
2023-06-22 01:41:01 -04:00
if (vocals != null) vocals.pause();
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Pause the countdown.
Countdown.pauseCountdown();
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
super.openSubState(subState);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Function called before closing the current substate.
* @param subState
*/
public override function closeSubState():Void
2023-01-22 19:55:30 -05:00
{
if (Std.isOfType(subState, PauseSubState))
2023-06-22 01:41:01 -04:00
{
var event:ScriptEvent = new ScriptEvent(RESUME, true);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
dispatchEvent(event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (event.eventCanceled) return;
2023-01-22 19:55:30 -05:00
// Resume
if (musicPausedBySubState)
{
FlxG.sound.music.play();
}
if (currentConversation != null)
{
currentConversation.resumeMusic();
}
2023-06-22 01:41:01 -04:00
if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Resume the countdown.
Countdown.resumeCountdown();
2023-06-22 01:41:01 -04:00
#if discord_rpc
if (startTimer.finished)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true,
currentSongLengthMs - Conductor.instance.songPosition);
2023-01-22 19:55:30 -05:00
}
else
{
2023-06-22 01:41:01 -04:00
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
#end
2024-02-28 00:19:08 -05:00
justUnpaused = true;
2023-01-22 19:55:30 -05:00
}
else if (Std.isOfType(subState, Transition))
{
// Do nothing.
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
super.closeSubState();
2023-01-22 19:55:30 -05:00
}
2023-01-22 19:55:30 -05:00
#if discord_rpc
2023-06-22 01:41:01 -04:00
/**
* Function called when the game window gains focus.
*/
public override function onFocus():Void
2023-01-22 19:55:30 -05:00
{
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause)
2023-01-22 19:55:30 -05:00
{
if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
2023-06-22 01:41:01 -04:00
+ ' ('
+ storyDifficultyText
+ ')', iconRPC, true,
currentSongLengthMs
- Conductor.instance.songPosition);
2023-01-22 19:55:30 -05:00
else
DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
2023-01-22 19:55:30 -05:00
}
super.onFocus();
}
2023-06-22 01:41:01 -04:00
/**
* Function called when the game window loses focus.
*/
public override function onFocusLost():Void
2023-01-22 19:55:30 -05:00
{
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText,
currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
2023-01-22 19:55:30 -05:00
super.onFocusLost();
}
#end
/**
* Removes any references to the current stage, then clears the stage cache,
* then reloads all the stages.
2023-06-08 16:30:45 -04:00
*
2023-01-22 19:55:30 -05:00
* This is useful for when you want to edit a stage without reloading the whole game.
* Reloading works on both the JSON and the HXC, if applicable.
2023-06-08 16:30:45 -04:00
*
2023-01-22 19:55:30 -05:00
* Call this by pressing F5 on a debug build.
*/
override function debug_refreshModules():Void
2023-01-22 19:55:30 -05:00
{
// Prevent further gameplay updates, which will try to reference dead objects.
criticalFailure = true;
2023-01-22 19:55:30 -05:00
// Remove the current stage. If the stage gets deleted while it's still in use,
// it'll probably crash the game or something.
if (this.currentStage != null)
{
remove(currentStage);
var event:ScriptEvent = new ScriptEvent(DESTROY, false);
2023-01-22 19:55:30 -05:00
ScriptEventDispatcher.callEvent(currentStage, event);
currentStage = null;
}
// Stop the instrumental.
if (FlxG.sound.music != null)
{
FlxG.sound.music.stop();
}
// Stop the vocals.
if (vocals != null && vocals.exists)
{
vocals.stop();
}
2023-01-22 19:55:30 -05:00
super.debug_refreshModules();
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentSong, event);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
override function stepHit():Bool
2023-01-22 19:55:30 -05:00
{
if (criticalFailure || !initialized) return false;
2023-06-22 01:41:01 -04:00
// super.stepHit() returns false if a module cancelled the event.
if (!super.stepHit()) return false;
2023-01-22 19:55:30 -05:00
2023-08-03 11:40:19 -04:00
if (isGamePaused) return false;
if (!startingSong
&& FlxG.sound.music != null
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
trace("VOCALS NEED RESYNC");
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
2023-06-22 01:41:01 -04:00
resyncVocals();
2023-01-22 19:55:30 -05:00
}
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
return true;
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
override function beatHit():Bool
2023-01-22 19:55:30 -05:00
{
if (criticalFailure || !initialized) return false;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// super.beatHit() returns false if a module cancelled the event.
if (!super.beatHit()) return false;
2023-01-22 19:55:30 -05:00
2023-08-03 11:40:19 -04:00
if (isGamePaused) return false;
2023-06-22 01:41:01 -04:00
if (generatedMusic)
{
// TODO: Sort more efficiently, or less often, to improve performance.
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
// Only zoom camera if we are zoomed by less than 35%.
if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// Zoom camera in (1.5%)
FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom;
// Hud zooms double (3%)
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
2023-01-22 19:55:30 -05:00
}
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// That combo milestones that got spoiled that one time.
// Comes with NEAT visual and audio effects.
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// bruh this var is bonkers i thot it was a function lmfaooo
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Break up into individual lines to aid debugging.
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
var shouldShowComboText:Bool = false;
// TODO: Re-enable combo text (how to do this without sections?).
// if (currentSong != null)
// {
// shouldShowComboText = (Conductor.instance.currentBeat % 8 == 7);
// var daSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16)];
2023-06-22 01:41:01 -04:00
// shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection);
// shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5);
//
// var daNextSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16) + 1];
// var isEndOfSong = .getSong().length < Std.int(Conductor.instance.currentBeat / 16);
2023-06-22 01:41:01 -04:00
// shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection));
// }
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (shouldShowComboText)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
var animShit:ComboMilestone = new ComboMilestone(-100, 300, Highscore.tallies.combo);
animShit.scrollFactor.set(0.6, 0.6);
animShit.zIndex = 1100;
2023-06-22 01:41:01 -04:00
animShit.cameras = [camHUD];
add(animShit);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
2023-01-22 19:55:30 -05:00
new FlxTimer().start(((Conductor.instance.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) {
2023-06-22 01:41:01 -04:00
animShit.forceFinish();
});
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
if (playerStrumline != null) playerStrumline.onBeatHit();
if (opponentStrumline != null) opponentStrumline.onBeatHit();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Make the characters dance on the beat
danceOnBeat();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
return true;
2023-01-22 19:55:30 -05:00
}
2023-12-15 21:09:01 -05:00
public override function destroy():Void
2023-01-22 19:55:30 -05:00
{
2023-12-15 21:09:01 -05:00
performCleanup();
2023-06-22 01:41:01 -04:00
super.destroy();
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Handles characters dancing to the beat of the current song.
*
* TODO: Move some of this logic into `Bopper.hx`, or individual character scripts.
*/
function danceOnBeat():Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
if (currentStage == null) return;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// TODO: Add HEY! song events to Tutorial.
if (Conductor.instance.currentBeat % 16 == 15
2023-06-22 01:41:01 -04:00
&& currentStage.getDad().characterId == 'gf'
&& Conductor.instance.currentBeat > 16
&& Conductor.instance.currentBeat < 48)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
currentStage.getBoyfriend().playAnimation('hey', true);
currentStage.getDad().playAnimation('cheer', true);
2023-01-22 19:55:30 -05:00
}
}
2023-06-22 01:41:01 -04:00
/**
* Initializes the game and HUD cameras.
*/
function initCameras():Void
2023-01-22 19:55:30 -05:00
{
camGame = new FunkinCamera();
camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
2023-06-22 01:41:01 -04:00
camHUD = new FlxCamera();
camHUD.bgColor.alpha = 0; // Show the game scene behind the camera.
camCutscene = new FlxCamera();
camCutscene.bgColor.alpha = 0; // Show the game scene behind the camera.
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
FlxG.cameras.reset(camGame);
FlxG.cameras.add(camHUD, false);
FlxG.cameras.add(camCutscene, false);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Configure camera follow point.
if (previousCameraFollowPoint != null)
{
cameraFollowPoint.setPosition(previousCameraFollowPoint.x, previousCameraFollowPoint.y);
previousCameraFollowPoint = null;
}
add(cameraFollowPoint);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Initializes the health bar on the HUD.
*/
function initHealthBar():Void
2023-01-22 19:55:30 -05:00
{
var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
healthBarBG = FunkinSprite.create(0, healthBarYPos, Paths.image('healthBar'));
2023-06-22 01:41:01 -04:00
healthBarBG.screenCenter(X);
healthBarBG.scrollFactor.set(0, 0);
healthBarBG.zIndex = 800;
2023-06-22 01:41:01 -04:00
add(healthBarBG);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this,
'healthLerp', 0, 2);
healthBar.scrollFactor.set();
healthBar.createFilledBar(Constants.COLOR_HEALTH_BAR_RED, Constants.COLOR_HEALTH_BAR_GREEN);
healthBar.zIndex = 801;
2023-06-22 01:41:01 -04:00
add(healthBar);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// The score text below the health bar.
scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, '', 20);
scoreText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
scoreText.scrollFactor.set();
scoreText.zIndex = 802;
2023-06-22 01:41:01 -04:00
add(scoreText);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Move the health bar to the HUD camera.
healthBar.cameras = [camHUD];
healthBarBG.cameras = [camHUD];
scoreText.cameras = [camHUD];
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Generates the stage and all its props.
*/
function initStage():Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
loadStage(currentStageId);
}
2023-01-22 19:55:30 -05:00
function initMinimalMode():Void
{
// Create the green background.
var menuBG = FunkinSprite.create(Paths.image('menuDesat'));
menuBG.color = 0xFF4CAF50;
menuBG.setGraphicSize(Std.int(menuBG.width * 1.1));
menuBG.updateHitbox();
menuBG.screenCenter();
menuBG.scrollFactor.set(0, 0);
menuBG.zIndex = -1000;
add(menuBG);
}
2023-06-22 01:41:01 -04:00
/**
* Loads stage data from cache, assembles the props,
* and adds it to the state.
* @param id
*/
function loadStage(id:String):Void
{
currentStage = StageRegistry.instance.fetchEntry(id);
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (currentStage != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// Actually create and position the sprites.
var event:ScriptEvent = new ScriptEvent(CREATE, false);
2023-06-22 01:41:01 -04:00
ScriptEventDispatcher.callEvent(currentStage, event);
2023-01-22 19:55:30 -05:00
resetCameraZoom();
2023-02-22 14:46:46 -05:00
2023-06-22 01:41:01 -04:00
// Add the stage to the scene.
this.add(currentStage);
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-06-22 01:41:01 -04:00
FlxG.console.registerObject('stage', currentStage);
#end
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
else
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// lolol
lime.app.Application.current.window.alert('Unable to load stage ${id}, is its data corrupted?.', 'Stage Error');
2023-06-22 01:41:01 -04:00
}
}
public function resetCameraZoom():Void
{
// Apply camera zoom level from stage data.
defaultCameraZoom = currentStage.camZoom;
}
2023-06-22 01:41:01 -04:00
/**
* Generates the character sprites and adds them to the stage.
*/
function initCharacters():Void
{
if (currentSong == null || currentChart == null)
{
trace('Song difficulty could not be loaded.');
}
2023-01-22 19:55:30 -05:00
var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the variation we are playing on by manipulating targetVariation.
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
//
// GIRLFRIEND
//
var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.girlfriend);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (girlfriend != null)
{
girlfriend.characterType = CharacterType.GF;
2023-01-22 19:55:30 -05:00
}
else if (currentCharacterData.girlfriend != '')
2023-01-22 19:55:30 -05:00
{
trace('WARNING: Could not load girlfriend character with ID ${currentCharacterData.girlfriend}, skipping...');
2023-01-22 19:55:30 -05:00
}
else
{
2023-06-22 01:41:01 -04:00
// Chosen GF was '' so we don't load one.
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
//
// DAD
//
var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.opponent);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (dad != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
dad.characterType = CharacterType.DAD;
2023-01-22 19:55:30 -05:00
//
// OPPONENT HEALTH ICON
//
iconP2 = new HealthIcon('dad', 1);
iconP2.y = healthBar.y - (iconP2.height / 2);
dad.initHealthIcon(true); // Apply the character ID here
iconP2.zIndex = 850;
add(iconP2);
iconP2.cameras = [camHUD];
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
//
// BOYFRIEND
//
var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.player);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (boyfriend != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
boyfriend.characterType = CharacterType.BF;
2023-01-22 19:55:30 -05:00
//
// PLAYER HEALTH ICON
//
iconP1 = new HealthIcon('bf', 0);
iconP1.y = healthBar.y - (iconP1.height / 2);
boyfriend.initHealthIcon(false); // Apply the character ID here
iconP1.zIndex = 850;
add(iconP1);
iconP1.cameras = [camHUD];
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
//
// ADD CHARACTERS TO SCENE
//
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (currentStage != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// Characters get added to the stage, not the main scene.
if (girlfriend != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
currentStage.addCharacter(girlfriend, GF);
2023-01-22 19:55:30 -05:00
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-06-22 01:41:01 -04:00
FlxG.console.registerObject('gf', girlfriend);
#end
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
if (boyfriend != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
currentStage.addCharacter(boyfriend, BF);
2023-01-22 19:55:30 -05:00
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-06-22 01:41:01 -04:00
FlxG.console.registerObject('bf', boyfriend);
2023-01-22 19:55:30 -05:00
#end
2023-06-16 17:37:56 -04:00
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (dad != null)
2023-06-16 17:37:56 -04:00
{
2023-06-22 01:41:01 -04:00
currentStage.addCharacter(dad, DAD);
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
2023-01-22 19:55:30 -05:00
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-06-22 01:41:01 -04:00
FlxG.console.registerObject('dad', dad);
2023-01-22 19:55:30 -05:00
#end
}
2023-06-22 01:41:01 -04:00
// Rearrange by z-indexes.
currentStage.refresh();
2023-01-22 19:55:30 -05:00
}
2023-06-16 17:37:56 -04:00
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
/**
* Constructs the strumlines for each player.
*/
function initStrumlines():Void
{
var noteStyleId:String = switch (currentStageId)
2023-01-22 19:55:30 -05:00
{
case 'school': 'pixel';
case 'schoolEvil': 'pixel';
default: Constants.DEFAULT_NOTE_STYLE;
}
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
2023-01-22 19:55:30 -05:00
playerStrumline = new Strumline(noteStyle, true);
opponentStrumline = new Strumline(noteStyle, false);
2023-06-22 01:41:01 -04:00
add(playerStrumline);
add(opponentStrumline);
2023-01-22 19:55:30 -05:00
// Position the player strumline on the right half of the screen
playerStrumline.x = FlxG.width / 2 + Constants.STRUMLINE_X_OFFSET; // Classic style
// playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; // Centered style
playerStrumline.y = Preferences.downscroll ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
playerStrumline.zIndex = 1001;
2023-06-22 01:41:01 -04:00
playerStrumline.cameras = [camHUD];
2023-01-22 19:55:30 -05:00
// Position the opponent strumline on the left half of the screen
2023-06-22 01:41:01 -04:00
opponentStrumline.x = Constants.STRUMLINE_X_OFFSET;
opponentStrumline.y = Preferences.downscroll ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
opponentStrumline.zIndex = 1000;
2023-06-22 01:41:01 -04:00
opponentStrumline.cameras = [camHUD];
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (!PlayStatePlaylist.isStoryMode)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
playerStrumline.fadeInArrows();
opponentStrumline.fadeInArrows();
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
this.refresh();
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Initializes the Discord Rich Presence.
*/
function initDiscord():Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
#if discord_rpc
storyDifficultyText = difficultyString();
iconRPC = currentSong.player2;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// To avoid having duplicate images in Discord assets
switch (iconRPC)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
case 'senpai-angry':
iconRPC = 'senpai';
case 'monster-christmas':
iconRPC = 'monster';
case 'mom-car':
iconRPC = 'mom';
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
// String that contains the mode defined here so it isn't necessary to call changePresence for each mode
detailsText = isStoryMode ? 'Story Mode: Week $storyWeek' : 'Freeplay';
detailsPausedText = 'Paused - $detailsText';
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Updating Discord Rich Presence.
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC);
#end
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
function initPreciseInputs():Void
{
FlxG.keys.preventDefaultKeys = [];
PreciseInputManager.instance.onInputPressed.add(onKeyPress);
PreciseInputManager.instance.onInputReleased.add(onKeyRelease);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Initializes the song (applying the chart, generating the notes, etc.)
* Should be done before the countdown starts.
*/
function generateSong():Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
if (currentChart == null)
{
2023-06-22 01:41:01 -04:00
trace('Song difficulty could not be loaded.');
}
2023-01-22 19:55:30 -05:00
// Conductor.instance.forceBPM(currentChart.getStartingBPM());
2023-01-22 19:55:30 -05:00
if (!overrideMusic)
2023-01-22 19:55:30 -05:00
{
// Stop the vocals if they already exist.
if (vocals != null) vocals.stop();
vocals = currentChart.buildVocals();
if (vocals.members.length == 0)
{
trace('WARNING: No vocals found for this song.');
}
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
regenNoteData();
2023-01-22 19:55:30 -05:00
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentSong, event);
2023-06-22 01:41:01 -04:00
generatedMusic = true;
2023-01-22 19:55:30 -05:00
}
/**
2023-06-22 01:41:01 -04:00
* Read note data from the chart and generate the notes.
2023-01-22 19:55:30 -05:00
*/
function regenNoteData(startTime:Float = 0):Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
Highscore.tallies.combo = 0;
Highscore.tallies = new Tallies();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Reset song events.
songEvents = currentChart.getEvents();
SongEventRegistry.resetEvents(songEvents);
2023-06-22 01:41:01 -04:00
// Reset the notes on each strumline.
var playerNoteData:Array<SongNoteData> = [];
var opponentNoteData:Array<SongNoteData> = [];
for (songNote in currentChart.notes)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
var strumTime:Float = songNote.time;
if (strumTime < startTime) continue; // Skip notes that are before the start time.
2023-06-22 01:41:01 -04:00
var noteData:Int = songNote.getDirection();
var playerNote:Bool = true;
if (noteData > 3) playerNote = false;
switch (songNote.getStrumlineIndex())
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
case 0:
playerNoteData.push(songNote);
// increment totalNotes for total possible notes able to be hit by the player
Highscore.tallies.totalNotes++;
2023-06-22 01:41:01 -04:00
case 1:
opponentNoteData.push(songNote);
2023-01-22 19:55:30 -05:00
}
}
2023-06-22 01:41:01 -04:00
playerStrumline.applyNoteData(playerNoteData);
opponentStrumline.applyNoteData(opponentNoteData);
}
2023-06-22 01:41:01 -04:00
/**
* Prepares to start the countdown.
* Ends any running cutscenes, creates the strumlines, and starts the countdown.
* This is public so that scripts can call it.
*/
public function startCountdown():Void
{
2023-06-22 01:41:01 -04:00
// If Countdown.performCountdown returns false, then the countdown was canceled by a script.
var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school'));
if (!result) return;
2023-06-22 01:41:01 -04:00
isInCutscene = false;
camCutscene.visible = false;
camHUD.visible = true;
}
2023-06-22 01:41:01 -04:00
/**
* Displays a dialogue cutscene with the given ID.
* This is used by song scripts to display dialogue.
*/
public function startConversation(conversationId:String):Void
{
2023-06-22 01:41:01 -04:00
isInCutscene = true;
currentConversation = ConversationRegistry.instance.fetchEntry(conversationId);
2023-06-22 01:41:01 -04:00
if (currentConversation == null) return;
if (!currentConversation.alive) currentConversation.revive();
2023-06-22 01:41:01 -04:00
currentConversation.completeCallback = onConversationComplete;
currentConversation.cameras = [camCutscene];
currentConversation.zIndex = 1000;
add(currentConversation);
refresh();
var event:ScriptEvent = new ScriptEvent(CREATE, false);
2023-06-22 01:41:01 -04:00
ScriptEventDispatcher.callEvent(currentConversation, event);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Handler function called when a conversation ends.
*/
function onConversationComplete():Void
2023-01-22 19:55:30 -05:00
{
isInCutscene = false;
if (currentConversation != null)
{
currentConversation.kill();
remove(currentConversation);
currentConversation = null;
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (startingSong && !isInCountdown)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
startCountdown();
2023-01-22 19:55:30 -05:00
}
}
2023-06-22 01:41:01 -04:00
/**
* Starts playing the song after the countdown has completed.
*/
function startSong():Void
2023-01-22 19:55:30 -05:00
{
dispatchEvent(new ScriptEvent(SONG_START));
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
startingSong = false;
2023-01-22 19:55:30 -05:00
if (!overrideMusic && !isGamePaused && currentChart != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
currentChart.playInst(1.0, false);
2023-01-22 19:55:30 -05:00
}
FlxG.sound.music.onComplete = endSong.bind(false);
// A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
FlxG.sound.music.pitch = playbackRate;
// I am going insane.
FlxG.sound.music.volume = 1.0;
2024-03-04 05:26:38 -05:00
FlxG.sound.music.fadeTween?.cancel();
2023-06-22 01:41:01 -04:00
trace('Playing vocals...');
add(vocals);
vocals.play();
vocals.pitch = playbackRate;
resyncVocals();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
#if discord_rpc
// Updating Discord Rich Presence (with Time Left)
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, currentSongLengthMs);
#end
if (startTimestamp > 0)
{
// FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
handleSkippedNotes();
}
2023-01-22 19:55:30 -05:00
}
/**
2023-06-22 01:41:01 -04:00
* Resyncronize the vocal tracks if they have become offset from the instrumental.
2023-01-22 19:55:30 -05:00
*/
2023-06-22 01:41:01 -04:00
function resyncVocals():Void
2023-01-22 19:55:30 -05:00
{
if (vocals == null) return;
2023-01-22 19:55:30 -05:00
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
if (!FlxG.sound.music.playing) return;
2023-06-22 01:41:01 -04:00
vocals.pause();
FlxG.sound.music.play(FlxG.sound.music.time);
2023-06-22 01:41:01 -04:00
vocals.time = FlxG.sound.music.time;
vocals.play(false, FlxG.sound.music.time);
2023-06-22 01:41:01 -04:00
}
2023-06-22 01:41:01 -04:00
/**
* Updates the position and contents of the score display.
*/
function updateScoreText():Void
{
// TODO: Add functionality for modules to update the score text.
scoreText.text = 'Score:' + songScore;
}
2023-06-22 01:41:01 -04:00
/**
* Updates the values of the health bar.
*/
function updateHealthBar():Void
{
healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Callback executed when one of the note keys is pressed.
*/
function onKeyPress(event:PreciseInputEvent):Void
2023-01-22 19:55:30 -05:00
{
if (isGamePaused) return;
2023-06-22 01:41:01 -04:00
// Do the minimal possible work here.
inputPressQueue.push(event);
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
/**
* Callback executed when one of the note keys is released.
*/
function onKeyRelease(event:PreciseInputEvent):Void
{
if (isGamePaused) return;
2023-06-22 01:41:01 -04:00
// Do the minimal possible work here.
inputReleaseQueue.push(event);
}
/**
2023-06-22 01:41:01 -04:00
* Handles opponent note hits and player note misses.
*/
2023-06-27 17:22:51 -04:00
function processNotes(elapsed:Float):Void
{
if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return;
2023-02-22 14:46:46 -05:00
2023-06-22 01:41:01 -04:00
// Process notes on the opponent's side.
for (note in opponentStrumline.notes.members)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
if (note == null) continue;
2023-01-22 19:55:30 -05:00
2024-01-15 14:20:44 -05:00
// TODO: Does this properly account for offsets?
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
2023-06-22 01:41:01 -04:00
var hitWindowCenter = note.strumTime;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
2023-01-22 19:55:30 -05:00
if (Conductor.instance.songPosition > hitWindowEnd)
2023-06-22 01:41:01 -04:00
{
if (note.hasMissed) continue;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
note.tooEarly = false;
note.mayHit = false;
note.hasMissed = true;
2023-01-22 19:55:30 -05:00
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
2023-06-22 01:41:01 -04:00
}
else if (Conductor.instance.songPosition > hitWindowCenter)
2023-01-22 19:55:30 -05:00
{
if (note.hasBeenHit) continue;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Call an event to allow canceling the note hit.
// NOTE: This is what handles the character animations!
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, 0, true);
2023-06-22 01:41:01 -04:00
dispatchEvent(event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Command the opponent to hit the note on time.
// NOTE: This is what handles the strumline and cleaning up the note itself!
opponentStrumline.hitNote(note);
2023-07-05 22:11:58 -04:00
if (note.holdNoteSprite != null)
2023-01-22 19:55:30 -05:00
{
2023-07-05 22:11:58 -04:00
opponentStrumline.playNoteHoldCover(note.holdNoteSprite);
2023-01-22 19:55:30 -05:00
}
}
else if (Conductor.instance.songPosition > hitWindowStart)
{
if (note.hasBeenHit || note.hasMissed) continue;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
note.tooEarly = false;
note.mayHit = true;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
2023-01-22 19:55:30 -05:00
}
else
{
2023-06-22 01:41:01 -04:00
note.tooEarly = true;
note.mayHit = false;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
}
2023-06-22 01:41:01 -04:00
}
2023-01-22 19:55:30 -05:00
// Process hold notes on the opponent's side.
for (holdNote in opponentStrumline.holdNotes.members)
{
if (holdNote == null || !holdNote.alive) continue;
// While the hold note is being hit, and there is length on the hold note...
if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
{
// Make sure the opponent keeps singing while the note is held.
if (currentStage != null && currentStage.getDad() != null && currentStage.getDad().isSinging())
2023-01-22 19:55:30 -05:00
{
currentStage.getDad().holdTimer = 0;
2023-01-22 19:55:30 -05:00
}
}
2024-01-15 14:20:44 -05:00
if (holdNote.missedNote && !holdNote.handledMiss)
{
// When the opponent drops a hold note.
2024-01-15 14:20:44 -05:00
holdNote.handledMiss = true;
// We dropped a hold note.
// Mute vocals and play miss animation, but don't penalize.
vocals.opponentVolume = 0;
currentStage.getOpponent().playSingAnimation(holdNote.noteData.getDirection(), true);
2024-01-15 14:20:44 -05:00
}
}
2023-06-22 01:41:01 -04:00
// Process notes on the player's side.
for (note in playerStrumline.notes.members)
{
if (note == null) continue;
if (note.hasBeenHit)
{
note.tooEarly = false;
note.mayHit = false;
note.hasMissed = false;
continue;
}
2023-01-22 19:55:30 -05:00
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
var hitWindowCenter = note.strumTime;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
2023-01-22 19:55:30 -05:00
if (Conductor.instance.songPosition > hitWindowEnd)
{
note.tooEarly = false;
note.mayHit = false;
note.hasMissed = true;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
}
else if (Conductor.instance.songPosition > hitWindowStart)
{
note.tooEarly = false;
note.mayHit = true;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
}
else
{
note.tooEarly = true;
note.mayHit = false;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false;
}
2023-01-22 19:55:30 -05:00
// This becomes true when the note leaves the hit window.
// It might still be on screen.
if (note.hasMissed && !note.handledMiss)
2023-06-22 01:41:01 -04:00
{
// Call an event to allow canceling the note miss.
// NOTE: This is what handles the character animations!
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, -Constants.HEALTH_MISS_PENALTY, 0, true);
2023-06-22 01:41:01 -04:00
dispatchEvent(event);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Judge the miss.
// NOTE: This is what handles the scoring.
trace('Missed note! ${note.noteData}');
onNoteMiss(note, event.playSound, event.healthChange);
note.handledMiss = true;
2023-06-22 01:41:01 -04:00
}
2023-01-22 19:55:30 -05:00
}
// Process hold notes on the player's side.
// This handles scoring so we don't need it on the opponent's side.
2023-06-27 17:22:51 -04:00
for (holdNote in playerStrumline.holdNotes.members)
2023-01-22 19:55:30 -05:00
{
if (holdNote == null || !holdNote.alive) continue;
2023-06-27 17:22:51 -04:00
// While the hold note is being hit, and there is length on the hold note...
if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
2023-06-27 17:22:51 -04:00
{
// Grant the player health.
health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
2023-06-27 17:22:51 -04:00
}
if (holdNote.missedNote && !holdNote.handledMiss)
{
// The player dropped a hold note.
holdNote.handledMiss = true;
// Mute vocals and play miss animation, but don't penalize.
vocals.playerVolume = 0;
if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
}
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
}
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
/**
* Spitting out the input for ravy 🙇!!
*/
var inputSpitter:Array<ScoreInput> = [];
function handleSkippedNotes():Void
{
for (note in playerStrumline.notes.members)
{
if (note == null || note.hasBeenHit) continue;
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
if (Conductor.instance.songPosition > hitWindowEnd)
{
// We have passed this note.
// Flag the note for deletion without actually penalizing the player.
note.handledMiss = true;
}
}
// Respawns notes that were b
playerStrumline.handleSkippedNotes();
opponentStrumline.handleSkippedNotes();
}
2023-06-22 01:41:01 -04:00
/**
* PreciseInputEvents are put into a queue between update() calls,
* and then processed here.
*/
function processInputQueue():Void
{
if (inputPressQueue.length + inputReleaseQueue.length == 0) return;
// Ignore inputs during cutscenes.
if (isInCutscene || disableKeys)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
inputPressQueue = [];
inputReleaseQueue = [];
return;
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
// Generate a list of notes within range.
var notesInRange:Array<NoteSprite> = playerStrumline.getNotesMayHit();
var holdNotesInRange:Array<SustainTrail> = playerStrumline.getHoldNotesHitOrMissed();
2023-06-22 01:41:01 -04:00
// If there are notes in range, pressing a key will cause a ghost miss.
var notesByDirection:Array<Array<NoteSprite>> = [[], [], [], []];
for (note in notesInRange)
notesByDirection[note.direction].push(note);
while (inputPressQueue.length > 0)
2023-03-02 04:20:03 -05:00
{
2023-06-22 01:41:01 -04:00
var input:PreciseInputEvent = inputPressQueue.shift();
2023-03-02 04:20:03 -05:00
playerStrumline.pressKey(input.noteDirection);
2023-06-22 01:41:01 -04:00
var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection];
if (!Constants.GHOST_TAPPING && notesInDirection.length == 0)
{
// Pressed a wrong key with no notes nearby.
// Perform a ghost miss (anti-spam).
ghostNoteMiss(input.noteDirection, notesInRange.length > 0);
// Play the strumline animation.
playerStrumline.playPress(input.noteDirection);
}
else if (Constants.GHOST_TAPPING && (holdNotesInRange.length + notesInRange.length > 0) && notesInDirection.length == 0)
{
// Pressed a wrong key with no notes nearby AND with notes in a different direction available.
// Perform a ghost miss (anti-spam).
2023-06-22 01:41:01 -04:00
ghostNoteMiss(input.noteDirection, notesInRange.length > 0);
// Play the strumline animation.
playerStrumline.playPress(input.noteDirection);
}
2023-06-22 01:41:01 -04:00
else if (notesInDirection.length > 0)
{
2023-06-22 01:41:01 -04:00
// Choose the first note, deprioritizing low priority notes.
var targetNote:Null<NoteSprite> = notesInDirection.find((note) -> !note.lowPriority);
if (targetNote == null) targetNote = notesInDirection[0];
if (targetNote == null) continue;
// Judge and hit the note.
trace('Hit note! ${targetNote.noteData}');
2023-06-22 01:41:01 -04:00
goodNoteHit(targetNote, input);
notesInDirection.remove(targetNote);
2023-06-22 01:41:01 -04:00
// Play the strumline animation.
playerStrumline.playConfirm(input.noteDirection);
}
else
{
2023-06-22 01:41:01 -04:00
// Play the strumline animation.
playerStrumline.playPress(input.noteDirection);
}
2023-03-02 04:20:03 -05:00
}
2023-06-22 01:41:01 -04:00
while (inputReleaseQueue.length > 0)
{
var input:PreciseInputEvent = inputReleaseQueue.shift();
// Play the strumline animation.
playerStrumline.playStatic(input.noteDirection);
playerStrumline.releaseKey(input.noteDirection);
2023-06-22 01:41:01 -04:00
}
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void
{
// Calculate the input latency (do this as late as possible).
// trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
// trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
// Get the offset and compensate for input latency.
// Round inward (trim remainder) for consistency.
var noteDiff:Int = Std.int(Conductor.instance.songPosition - note.noteData.time - inputLatencyMs);
var score = Scoring.scoreNote(noteDiff, PBOT1);
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
var healthChange = 0.0;
switch (daRating)
{
case 'sick':
healthChange = Constants.HEALTH_SICK_BONUS;
case 'good':
healthChange = Constants.HEALTH_GOOD_BONUS;
case 'bad':
healthChange = Constants.HEALTH_BAD_BONUS;
case 'shit':
healthChange = Constants.HEALTH_SHIT_BONUS;
}
// Send the note hit event.
var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, Highscore.tallies.combo + 1);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) return;
// Display the combo meter and add the calculation to the score.
popUpScore(note, event.score, event.judgement, event.healthChange);
2023-06-22 01:41:01 -04:00
if (note.isHoldNote && note.holdNoteSprite != null)
{
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
}
vocals.playerVolume = 1;
2023-01-22 19:55:30 -05:00
}
/**
2023-06-22 01:41:01 -04:00
* Called when a note leaves the screen and is considered missed by the player.
* @param note
*/
function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthChange:Float):Void
{
// If we are here, we already CALLED the onNoteMiss script hook!
health += healthChange;
songScore -= 10;
2023-06-22 01:41:01 -04:00
if (!isPracticeMode)
{
2023-06-22 01:41:01 -04:00
// messy copy paste rn lol
var pressArray:Array<Bool> = [
controls.NOTE_LEFT_P,
controls.NOTE_DOWN_P,
controls.NOTE_UP_P,
controls.NOTE_RIGHT_P
];
2023-06-22 01:41:01 -04:00
var indices:Array<Int> = [];
for (i in 0...pressArray.length)
{
if (pressArray[i]) indices.push(i);
}
if (indices.length > 0)
{
for (i in 0...indices.length)
{
inputSpitter.push(
{
t: Std.int(Conductor.instance.songPosition),
2023-06-22 01:41:01 -04:00
d: indices[i],
l: 20
});
}
}
else
{
inputSpitter.push(
{
t: Std.int(Conductor.instance.songPosition),
2023-06-22 01:41:01 -04:00
d: -1,
l: 20
});
}
}
vocals.playerVolume = 0;
Highscore.tallies.missed++;
2023-06-22 01:41:01 -04:00
if (Highscore.tallies.combo != 0)
{
// Break the combo.
2023-06-22 01:41:01 -04:00
Highscore.tallies.combo = comboPopUps.displayCombo(0);
}
if (playSound)
2023-06-22 01:41:01 -04:00
{
vocals.playerVolume = 0;
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
}
}
2023-01-22 19:55:30 -05:00
/**
* Called when a player presses a key with no note present.
* Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
* or even cancel the event entirely.
2023-06-08 16:30:45 -04:00
*
* @param direction
* @param hasPossibleNotes
2023-01-22 19:55:30 -05:00
*/
2023-06-22 01:41:01 -04:00
function ghostNoteMiss(direction:NoteDirection, hasPossibleNotes:Bool = true):Void
2023-01-22 19:55:30 -05:00
{
var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
hasPossibleNotes, // Whether there was a note you could have hit.
- 1 * Constants.HEALTH_MISS_PENALTY, // How much health to add (negative).
2023-01-22 19:55:30 -05:00
- 10 // Amount of score to add (negative).
);
dispatchEvent(event);
// Calling event.cancelEvent() skips animations and penalties. Neat!
if (event.eventCanceled) return;
2023-01-22 19:55:30 -05:00
health += event.healthChange;
songScore += event.scoreChange;
2023-01-22 19:55:30 -05:00
2023-03-02 04:20:03 -05:00
if (!isPracticeMode)
{
var pressArray:Array<Bool> = [
controls.NOTE_LEFT_P,
controls.NOTE_DOWN_P,
controls.NOTE_UP_P,
controls.NOTE_RIGHT_P
];
var indices:Array<Int> = [];
for (i in 0...pressArray.length)
{
if (pressArray[i]) indices.push(i);
}
for (i in 0...indices.length)
{
inputSpitter.push(
{
t: Std.int(Conductor.instance.songPosition),
d: indices[i],
l: 20
});
}
2023-03-02 04:20:03 -05:00
}
2023-01-22 19:55:30 -05:00
if (event.playSound)
{
vocals.playerVolume = 0;
2023-01-22 19:55:30 -05:00
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
}
}
2023-06-22 01:41:01 -04:00
/**
* Debug keys. Disabled while in cutscenes.
*/
function debugKeyShit():Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
#if !debug
perfectMode = false;
#else
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
#end
2023-01-22 19:55:30 -05:00
// Open the stage editor overlaying the current state.
if (controls.DEBUG_STAGE)
2023-06-22 01:41:01 -04:00
{
// hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!!
disableKeys = true;
persistentUpdate = false;
openSubState(new StageOffsetSubState());
}
// Redirect to the chart editor playing the current song.
if (controls.DEBUG_CHART)
{
disableKeys = true;
persistentUpdate = false;
FlxG.switchState(() -> new ChartEditorState(
{
targetSongId: currentSong.id,
}));
}
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-06-22 01:41:01 -04:00
// 1: End the song immediately.
if (FlxG.keys.justPressed.ONE) endSong(true);
2023-06-22 01:41:01 -04:00
// 2: Gain 10% health.
if (FlxG.keys.justPressed.TWO) health += 0.1 * Constants.HEALTH_MAX;
2023-06-22 01:41:01 -04:00
// 3: Lose 5% health.
if (FlxG.keys.justPressed.THREE) health -= 0.05 * Constants.HEALTH_MAX;
2023-06-22 01:41:01 -04:00
#end
// 9: Toggle the old icon.
if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon();
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
// PAGEUP: Skip forward two sections.
// SHIFT+PAGEUP: Skip forward twenty sections.
if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 20 : 2);
// PAGEDOWN: Skip backward two section. Doesn't replace notes.
// SHIFT+PAGEDOWN: Skip backward twenty sections.
if (FlxG.keys.justPressed.PAGEDOWN) changeSection(FlxG.keys.pressed.SHIFT ? -20 : -2);
2023-06-22 01:41:01 -04:00
#end
if (FlxG.keys.justPressed.B) trace(inputSpitter.join('\n'));
}
/**
* Handles health, score, and rating popups when a note is hit.
*/
function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
vocals.playerVolume = 1;
2023-01-22 19:55:30 -05:00
if (daRating == 'miss')
{
// If daRating is 'miss', that means we made a mistake and should not continue.
trace('[WARNING] popUpScore judged a note as a miss!');
// TODO: Remove this.
comboPopUps.displayRating('miss');
return;
}
var isComboBreak = false;
2023-06-22 01:41:01 -04:00
switch (daRating)
{
case 'sick':
Highscore.tallies.sick += 1;
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
2023-06-22 01:41:01 -04:00
case 'good':
Highscore.tallies.good += 1;
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
2023-06-22 01:41:01 -04:00
case 'bad':
Highscore.tallies.bad += 1;
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
2023-06-22 01:41:01 -04:00
case 'shit':
Highscore.tallies.shit += 1;
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
2023-06-22 01:41:01 -04:00
}
2023-01-22 19:55:30 -05:00
health += healthChange;
if (isComboBreak)
{
// Break the combo, but don't increment tallies.misses.
Highscore.tallies.combo = comboPopUps.displayCombo(0);
2023-06-22 01:41:01 -04:00
}
else
{
Highscore.tallies.combo++;
Highscore.tallies.totalNotesHit++;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
}
playerStrumline.hitNote(daNote, !isComboBreak);
2023-01-22 19:55:30 -05:00
if (daRating == "sick")
2023-06-22 01:41:01 -04:00
{
playerStrumline.playNoteSplash(daNote.noteData.getDirection());
}
2023-03-02 04:20:03 -05:00
songScore += score;
2023-03-02 04:20:03 -05:00
if (!isPracticeMode)
{
2023-06-22 01:41:01 -04:00
// TODO: Input splitter uses old input system, make it pull from the precise input queue directly.
2023-03-02 04:20:03 -05:00
var pressArray:Array<Bool> = [
controls.NOTE_LEFT_P,
controls.NOTE_DOWN_P,
controls.NOTE_UP_P,
controls.NOTE_RIGHT_P
];
var indices:Array<Int> = [];
for (i in 0...pressArray.length)
{
if (pressArray[i]) indices.push(i);
}
if (indices.length > 0)
{
for (i in 0...indices.length)
{
inputSpitter.push(
{
t: Std.int(Conductor.instance.songPosition),
d: indices[i],
l: 20
});
}
}
else
{
inputSpitter.push(
{
t: Std.int(Conductor.instance.songPosition),
d: -1,
l: 20
});
}
2023-03-02 04:20:03 -05:00
}
2023-06-22 01:41:01 -04:00
comboPopUps.displayRating(daRating);
if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo);
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
/**
* Handle keyboard inputs during cutscenes.
* This includes advancing conversations and skipping videos.
* @param elapsed Time elapsed since last game update.
*/
function handleCutsceneKeys(elapsed:Float):Void
2023-01-22 19:55:30 -05:00
{
2024-02-28 00:19:08 -05:00
if (isGamePaused) return;
2023-06-22 01:41:01 -04:00
if (currentConversation != null)
2023-01-22 19:55:30 -05:00
{
2024-02-28 00:19:08 -05:00
// Pause/unpause may conflict with advancing the conversation!
if (controls.CUTSCENE_ADVANCE && !justUnpaused)
2023-01-22 19:55:30 -05:00
{
currentConversation.advanceConversation();
2023-01-22 19:55:30 -05:00
}
2024-02-28 00:19:08 -05:00
else if (controls.PAUSE && !justUnpaused)
2023-06-22 01:41:01 -04:00
{
currentConversation.pauseMusic();
2024-02-28 00:19:08 -05:00
var pauseSubState:FlxSubState = new PauseSubState({mode: Conversation});
persistentUpdate = false;
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
pauseSubState.camera = camCutscene;
openSubState(pauseSubState);
2023-06-22 01:41:01 -04:00
}
}
else if (VideoCutscene.isPlaying())
{
// This is a video cutscene.
2024-02-28 00:19:08 -05:00
if (controls.PAUSE && !justUnpaused)
2023-06-22 01:41:01 -04:00
{
2024-02-28 03:53:36 -05:00
VideoCutscene.pauseVideo();
2024-02-28 00:19:08 -05:00
var pauseSubState:FlxSubState = new PauseSubState({mode: Cutscene});
2023-06-22 01:41:01 -04:00
2024-02-28 00:19:08 -05:00
persistentUpdate = false;
FlxTransitionableSubState.skipNextTransIn = true;
FlxTransitionableSubState.skipNextTransOut = true;
pauseSubState.camera = camCutscene;
openSubState(pauseSubState);
}
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
}
2023-01-22 19:55:30 -05:00
/**
* Handle logic for actually skipping a video cutscene after it has been held.
*/
function skipVideoCutscene():Void
{
VideoCutscene.finishVideo();
}
2023-06-22 01:41:01 -04:00
/**
* End the song. Handle saving high scores and transitioning to the results screen.
*
* Broadcasts an `onSongEnd` event, which can be cancelled to prevent the song from ending (for a cutscene or something).
* Remember to call `endSong` again when the song should actually end!
* @param rightGoddamnNow If true, don't play the fancy animation where you zoom onto Girlfriend. Used after a cutscene.
2023-06-22 01:41:01 -04:00
*/
public function endSong(rightGoddamnNow:Bool = false):Void
2023-06-22 01:41:01 -04:00
{
FlxG.sound.music.volume = 0;
vocals.volume = 0;
mayPauseGame = false;
// Check if any events want to prevent the song from ending.
var event = new ScriptEvent(SONG_END, true);
dispatchEvent(event);
if (event.eventCanceled) return;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
#if sys
// spitter for ravy, teehee!!
var writer = new json2object.JsonWriter<Array<ScoreInput>>();
var output = writer.write(inputSpitter, ' ');
2023-06-22 01:41:01 -04:00
sys.io.File.saveContent("./scores.json", output);
#end
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
deathCounter = 0;
2023-06-22 01:41:01 -04:00
if (currentSong != null && currentSong.validScore)
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
var data =
{
score: songScore,
tallies:
{
sick: Highscore.tallies.sick,
good: Highscore.tallies.good,
bad: Highscore.tallies.bad,
shit: Highscore.tallies.shit,
missed: Highscore.tallies.missed,
combo: Highscore.tallies.combo,
maxCombo: Highscore.tallies.maxCombo,
totalNotesHit: Highscore.tallies.totalNotesHit,
totalNotes: currentChart.notes.length,
},
accuracy: Highscore.tallies.totalNotesHit / currentChart.notes.length,
};
2024-03-04 21:18:40 -05:00
// adds current song data into the tallies for the level (story levels)
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
{
Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
#if newgrounds
NGio.postScore(score, currentSong.id);
#end
}
2023-06-22 01:41:01 -04:00
}
if (PlayStatePlaylist.isStoryMode)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
PlayStatePlaylist.campaignScore += songScore;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// Pop the next song ID from the list.
// Returns null if the list is empty.
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (targetSongId == null)
{
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
2023-01-22 19:55:30 -05:00
// transIn = FlxTransitionableState.defaultTransIn;
// transOut = FlxTransitionableState.defaultTransOut;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// TODO: Rework week unlock logic.
// StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
if (currentSong.validScore)
{
NGio.unlockMedal(60961);
2023-01-22 19:55:30 -05:00
var data =
{
score: PlayStatePlaylist.campaignScore,
tallies:
{
// TODO: Sum up the values for the whole level!
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
},
accuracy: Highscore.tallies.totalNotesHit / currentChart.notes.length,
};
if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
{
Save.instance.setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
#if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end
}
}
2023-01-22 19:55:30 -05:00
if (isSubState)
{
this.close();
}
else
{
if (rightGoddamnNow)
{
moveToResultsScreen();
}
else
{
zoomIntoResultsScreen();
}
}
2023-06-22 01:41:01 -04:00
}
else
{
var difficulty:String = '';
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
trace('Loading next song ($targetSongId : $difficulty)');
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
FlxG.sound.music.stop();
vocals.stop();
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
// TODO: Softcode this cutscene.
if (currentSong.id == 'eggnog')
2023-06-22 01:41:01 -04:00
{
var blackBG:FunkinSprite = new FunkinSprite(-FlxG.width * FlxG.camera.zoom, -FlxG.height * FlxG.camera.zoom);
blackBG.makeSolidColor(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
blackBG.scrollFactor.set();
add(blackBG);
2023-06-22 01:41:01 -04:00
camHUD.visible = false;
isInCutscene = true;
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() {
// no camFollow so it centers on horror tree
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
LoadingState.loadPlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetVariation: currentVariation,
cameraFollowPoint: cameraFollowPoint.getPosition(),
});
});
2023-06-22 01:41:01 -04:00
}
else
{
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
LoadingState.loadPlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetVariation: currentVariation,
cameraFollowPoint: cameraFollowPoint.getPosition(),
});
}
2023-06-22 01:41:01 -04:00
}
}
else
2023-01-22 19:55:30 -05:00
{
if (isSubState)
{
this.close();
}
else
{
if (rightGoddamnNow)
{
moveToResultsScreen();
}
else
{
zoomIntoResultsScreen();
}
}
2023-01-22 19:55:30 -05:00
}
}
public override function close():Void
{
criticalFailure = true; // Stop game updates.
performCleanup();
super.close();
}
2023-01-22 19:55:30 -05:00
/**
2023-06-22 01:41:01 -04:00
* Perform necessary cleanup before leaving the PlayState.
2023-01-22 19:55:30 -05:00
*/
2023-06-22 01:41:01 -04:00
function performCleanup():Void
2023-01-22 19:55:30 -05:00
{
if (currentConversation != null)
{
remove(currentConversation);
currentConversation.kill();
}
2023-06-22 01:41:01 -04:00
if (currentChart != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
// TODO: Uncache the song.
2023-01-22 19:55:30 -05:00
}
if (!overrideMusic)
{
// Stop the music.
FlxG.sound.music.pause();
2024-02-05 13:35:30 -05:00
if (vocals != null) vocals.stop();
}
else
{
FlxG.sound.music.pause();
2024-02-05 13:35:30 -05:00
if (vocals != null)
{
2024-02-28 00:19:08 -05:00
vocals.destroy();
2024-02-05 13:35:30 -05:00
remove(vocals);
}
}
2023-06-22 01:41:01 -04:00
// Remove reference to stage and remove sprites from it to save memory.
if (currentStage != null)
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
remove(currentStage);
currentStage.kill();
dispatchEvent(new ScriptEvent(DESTROY, false));
2023-06-22 01:41:01 -04:00
currentStage = null;
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
GameOverSubState.reset();
PauseSubState.reset();
2023-06-22 01:41:01 -04:00
// Clear the static reference to this state.
instance = null;
2023-01-22 19:55:30 -05:00
}
/**
* Play the camera zoom animation and then move to the results screen once it's done.
2023-01-22 19:55:30 -05:00
*/
function zoomIntoResultsScreen():Void
2023-06-16 17:37:56 -04:00
{
2023-06-22 01:41:01 -04:00
trace('WENT TO RESULTS SCREEN!');
2023-06-16 17:37:56 -04:00
2023-06-22 01:41:01 -04:00
// Stop camera zooming on beat.
cameraZoomRate = 0;
2023-06-16 17:37:56 -04:00
2023-06-22 01:41:01 -04:00
// If the opponent is GF, zoom in on the opponent.
// Else, if there is no GF, zoom in on BF.
// Else, zoom in on GF.
var targetDad:Bool = currentStage.getDad() != null && currentStage.getDad().characterId == 'gf';
var targetBF:Bool = currentStage.getGirlfriend() == null && !targetDad;
2023-06-16 17:37:56 -04:00
2023-06-22 01:41:01 -04:00
if (targetBF)
2023-06-16 17:37:56 -04:00
{
2023-06-22 01:41:01 -04:00
FlxG.camera.follow(currentStage.getBoyfriend(), null, 0.05);
FlxG.camera.targetOffset.y -= 350;
FlxG.camera.targetOffset.x += 20;
2023-06-16 17:37:56 -04:00
}
2023-06-22 01:41:01 -04:00
else if (targetDad)
2023-06-16 17:37:56 -04:00
{
2023-06-22 01:41:01 -04:00
FlxG.camera.follow(currentStage.getDad(), null, 0.05);
FlxG.camera.targetOffset.y -= 350;
FlxG.camera.targetOffset.x += 20;
}
else
{
FlxG.camera.follow(currentStage.getGirlfriend(), null, 0.05);
FlxG.camera.targetOffset.y -= 350;
FlxG.camera.targetOffset.x += 20;
2023-06-16 17:37:56 -04:00
}
2023-06-22 01:41:01 -04:00
FlxTween.tween(camHUD, {alpha: 0}, 0.6);
2023-06-16 17:37:56 -04:00
2023-06-22 01:41:01 -04:00
// Zoom in on Girlfriend (or BF if no GF)
new FlxTimer().start(0.8, function(_) {
if (targetBF)
{
currentStage.getBoyfriend().animation.play('hey');
}
else if (targetDad)
{
currentStage.getDad().animation.play('cheer');
}
else
{
currentStage.getGirlfriend().animation.play('cheer');
}
// Zoom over to the Results screen.
FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1,
{
ease: FlxEase.expoIn,
onComplete: function(_) {
moveToResultsScreen();
2023-06-22 01:41:01 -04:00
}
});
});
2023-01-22 19:55:30 -05:00
}
/**
* Move to the results screen right goddamn now.
*/
function moveToResultsScreen():Void
{
persistentUpdate = false;
vocals.stop();
camHUD.alpha = 1;
2024-03-04 21:18:40 -05:00
var talliesToUse:Tallies = PlayStatePlaylist.isStoryMode ? Highscore.talliesLevel : Highscore.tallies;
var res:ResultState = new ResultState(
{
storyMode: PlayStatePlaylist.isStoryMode,
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
2024-03-04 21:18:40 -05:00
tallies: talliesToUse,
});
res.camera = camHUD;
openSubState(res);
}
2023-01-22 19:55:30 -05:00
/**
2023-06-22 01:41:01 -04:00
* Pauses music and vocals easily.
2023-01-22 19:55:30 -05:00
*/
2023-06-22 01:41:01 -04:00
public function pauseMusic():Void
2023-01-22 19:55:30 -05:00
{
2023-06-22 01:41:01 -04:00
FlxG.sound.music.pause();
vocals.pause();
2023-01-22 19:55:30 -05:00
}
/**
* Resets the camera's zoom level and focus point.
*/
public function resetCamera():Void
{
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
FlxG.camera.targetOffset.set();
FlxG.camera.zoom = defaultCameraZoom;
// Snap the camera to the follow point immediately.
2023-01-22 19:55:30 -05:00
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}
2024-02-13 00:29:46 -05:00
#if (debug || FORCE_DEBUG_VERSION)
2023-01-22 19:55:30 -05:00
/**
2023-06-22 01:41:01 -04:00
* Jumps forward or backward a number of sections in the song.
* Accounts for BPM changes, does not prevent death from skipped notes.
* @param sections The number of sections to jump, negative to go backwards.
2023-01-22 19:55:30 -05:00
*/
2023-06-22 01:41:01 -04:00
function changeSection(sections:Int):Void
2023-01-22 19:55:30 -05:00
{
// FlxG.sound.music.pause();
2023-01-22 19:55:30 -05:00
var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.stepsPerMeasure * sections);
var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
2023-01-22 19:55:30 -05:00
// Don't go back in time to before the song started.
targetTimeMs = Math.max(0, targetTimeMs);
FlxG.sound.music.time = targetTimeMs;
2023-01-22 19:55:30 -05:00
handleSkippedNotes();
SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition);
// regenNoteData(FlxG.sound.music.time);
Conductor.instance.update(FlxG.sound.music.time);
2023-01-22 19:55:30 -05:00
2023-06-22 01:41:01 -04:00
resyncVocals();
2023-01-22 19:55:30 -05:00
}
2023-06-22 01:41:01 -04:00
#end
2021-08-27 18:08:01 -04:00
}