mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2024-11-10 00:34:40 +00:00
e90c37ad75
Whenever you pause gameplay, the `PlayState` pauses the music. But if you start a song from the main menu, pause during the 3,2,1 countdown, and then unpause, the main menu music will incorrectly start playing again due to `FlxG.sound.music` still referencing it before the gameplay song starts playing. Only restart the music if it was actually playing to begin with by storing the play state in `PlayState.openSubState` when we pause. This hopefully avoids any other cases where the pause state gets pushed while the music is stopped.
2838 lines
83 KiB
Haxe
2838 lines
83 KiB
Haxe
package funkin.play;
|
|
|
|
import flixel.addons.display.FlxPieDial;
|
|
import flixel.addons.display.FlxPieDial;
|
|
import flixel.addons.transition.FlxTransitionableState;
|
|
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;
|
|
import flixel.FlxCamera;
|
|
import flixel.FlxObject;
|
|
import flixel.FlxSprite;
|
|
import flixel.FlxState;
|
|
import flixel.FlxSubState;
|
|
import flixel.math.FlxMath;
|
|
import flixel.math.FlxPoint;
|
|
import flixel.math.FlxRect;
|
|
import flixel.text.FlxText;
|
|
import flixel.tweens.FlxEase;
|
|
import flixel.tweens.FlxTween;
|
|
import flixel.ui.FlxBar;
|
|
import flixel.util.FlxColor;
|
|
import flixel.util.FlxTimer;
|
|
import funkin.api.newgrounds.NGio;
|
|
import funkin.audio.VoicesGroup;
|
|
import funkin.audio.VoicesGroup;
|
|
import funkin.data.dialogue.ConversationRegistry;
|
|
import funkin.data.event.SongEventRegistry;
|
|
import funkin.data.notestyle.NoteStyleData;
|
|
import funkin.data.notestyle.NoteStyleRegistry;
|
|
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;
|
|
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;
|
|
import funkin.play.components.ComboMilestone;
|
|
import funkin.play.components.HealthIcon;
|
|
import funkin.play.components.PopUpStuff;
|
|
import funkin.play.cutscene.dialogue.Conversation;
|
|
import funkin.play.cutscene.dialogue.Conversation;
|
|
import funkin.play.cutscene.VanillaCutscenes;
|
|
import funkin.play.cutscene.VideoCutscene;
|
|
import funkin.play.notes.NoteDirection;
|
|
import funkin.play.notes.NoteSplash;
|
|
import funkin.play.notes.NoteSprite;
|
|
import funkin.play.notes.NoteSprite;
|
|
import funkin.play.notes.notestyle.NoteStyle;
|
|
import funkin.play.notes.notestyle.NoteStyle;
|
|
import funkin.play.notes.Strumline;
|
|
import funkin.play.notes.SustainTrail;
|
|
import funkin.play.scoring.Scoring;
|
|
import funkin.play.song.Song;
|
|
import funkin.play.stage.Stage;
|
|
import funkin.save.Save;
|
|
import funkin.ui.debug.charting.ChartEditorState;
|
|
import funkin.ui.debug.stage.StageOffsetSubState;
|
|
import funkin.ui.mainmenu.MainMenuState;
|
|
import funkin.ui.MusicBeatSubState;
|
|
import funkin.ui.options.PreferencesMenu;
|
|
import funkin.ui.story.StoryMenuState;
|
|
import funkin.graphics.FunkinCamera;
|
|
import funkin.ui.transition.LoadingState;
|
|
import funkin.util.SerializerUtil;
|
|
import haxe.Int64;
|
|
import lime.ui.Haptic;
|
|
import openfl.display.BitmapData;
|
|
import openfl.geom.Rectangle;
|
|
import openfl.Lib;
|
|
#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,
|
|
/**
|
|
* 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.
|
|
*/
|
|
?startTimestamp:Float,
|
|
/**
|
|
* If specified, the game will not load the instrumental or vocal tracks,
|
|
* and must be loaded externally.
|
|
*/
|
|
?overrideMusic:Bool,
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
/**
|
|
* 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.
|
|
*/
|
|
public static var instance:PlayState = null;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
/**
|
|
* The currently selected stage.
|
|
*/
|
|
public var currentSong:Song = null;
|
|
|
|
/**
|
|
* The currently selected difficulty.
|
|
*/
|
|
public var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
|
|
|
|
/**
|
|
* The currently selected variation.
|
|
*/
|
|
public var currentVariation:String = Constants.DEFAULT_VARIATION;
|
|
|
|
/**
|
|
* The currently active Stage. This is the object containing all the props.
|
|
*/
|
|
public var currentStage:Stage = null;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* The current 'Blueball Counter' to display in the pause menu.
|
|
* Resets when you beat a song or go back to the main menu.
|
|
*/
|
|
public var deathCounter:Int = 0;
|
|
|
|
/**
|
|
* The player's current health.
|
|
*/
|
|
public var health:Float = Constants.HEALTH_STARTING;
|
|
|
|
/**
|
|
* The player's current score.
|
|
* TODO: Move this to its own class.
|
|
*/
|
|
public var songScore:Int = 0;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* An empty FlxObject contained in the scene.
|
|
* The current gameplay camera will always follow this object. Tween its position to move the camera smoothly.
|
|
*
|
|
* It needs to be an object in the scene for the camera to be configured to follow it.
|
|
* We optionally make this an FlxSprite so we can draw a debug graphic with it.
|
|
*/
|
|
public var cameraFollowPoint:FlxObject;
|
|
|
|
/**
|
|
* The camera follow point from the last stage.
|
|
* Used to persist the position of the `cameraFollowPosition` between levels.
|
|
*/
|
|
public var previousCameraFollowPoint:FlxSprite = null;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public var defaultHUDCameraZoom:Float = FlxCamera.defaultZoom * 1.0;
|
|
|
|
/**
|
|
* Intensity of the gameplay camera zoom.
|
|
* @default `1.5%`
|
|
*/
|
|
public var cameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY;
|
|
|
|
/**
|
|
* Intensity of the HUD camera zoom.
|
|
* @default `3.0%`
|
|
*/
|
|
public var hudCameraZoomIntensity:Float = Constants.DEFAULT_ZOOM_INTENSITY * 2.0;
|
|
|
|
/**
|
|
* How many beats (quarter notes) between camera zooms.
|
|
* @default One camera zoom per measure (four beats).
|
|
*/
|
|
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;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
public var isSubState(get, never):Bool;
|
|
|
|
function get_isSubState():Bool
|
|
{
|
|
return this._parentState != null;
|
|
}
|
|
|
|
public var isChartingMode(get, never):Bool;
|
|
|
|
function get_isChartingMode():Bool
|
|
{
|
|
return this._parentState != null && Std.isOfType(this._parentState, ChartEditorState);
|
|
}
|
|
|
|
/**
|
|
* The current dialogue.
|
|
*/
|
|
public var currentConversation:Conversation;
|
|
|
|
/**
|
|
* 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> = [];
|
|
|
|
/**
|
|
* 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>;
|
|
|
|
/**
|
|
* If true, the player is allowed to pause the game.
|
|
* Disabled during the ending of a song.
|
|
*/
|
|
var mayPauseGame:Bool = true;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
var criticalFailure:Bool = false;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
#if discord_rpc
|
|
// Discord RPC variables
|
|
var storyDifficultyText:String = '';
|
|
var iconRPC:String = '';
|
|
var detailsText:String = '';
|
|
var detailsPausedText:String = '';
|
|
#end
|
|
|
|
/**
|
|
* RENDER OBJECTS
|
|
*/
|
|
/**
|
|
* The FlxText which displays the current score.
|
|
*/
|
|
var scoreText:FlxText;
|
|
|
|
/**
|
|
* 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:FlxSprite;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public var opponentStrumline:Strumline;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public var camGame:FlxCamera;
|
|
|
|
/**
|
|
* The camera which contains, and controls visibility of, a video cutscene.
|
|
*/
|
|
public var camCutscene:FlxCamera;
|
|
|
|
/**
|
|
* The combo popups. Includes the real-time combo counter and the rating.
|
|
*/
|
|
var comboPopUps:PopUpStuff;
|
|
|
|
/**
|
|
* The circular sprite that appears while the user is holding down the Skip Cutscene button.
|
|
*/
|
|
var skipTimer:FlxPieDial;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
return pauseSubState.exitingToMenu;
|
|
}
|
|
|
|
/**
|
|
* Data for the current difficulty for the current song.
|
|
* Includes chart data, scroll speed, and other information.
|
|
*/
|
|
public var currentChart(get, never):SongDifficulty;
|
|
|
|
function get_currentChart():SongDifficulty
|
|
{
|
|
if (currentSong == null || currentDifficulty == null) return null;
|
|
return currentSong.getDifficulty(currentDifficulty, currentVariation);
|
|
}
|
|
|
|
/**
|
|
* The internal ID of the currently active Stage.
|
|
* Used to retrieve the data required to build the `currentStage`.
|
|
*/
|
|
public var currentStageId(get, never):String;
|
|
|
|
function get_currentStageId():String
|
|
{
|
|
if (currentChart == null || currentChart.stage == null || currentChart.stage == '') return Constants.DEFAULT_STAGE;
|
|
return currentChart.stage;
|
|
}
|
|
|
|
/**
|
|
* The length of the current song, in milliseconds.
|
|
*/
|
|
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;
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
super();
|
|
|
|
// Validate parameters.
|
|
if (params == null && lastParams == null)
|
|
{
|
|
throw 'PlayState constructor called with no available parameters.';
|
|
}
|
|
else if (params == null)
|
|
{
|
|
trace('WARNING: PlayState constructor called with no parameters. Reusing previous parameters.');
|
|
params = lastParams;
|
|
}
|
|
else
|
|
{
|
|
lastParams = params;
|
|
}
|
|
|
|
// 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;
|
|
overrideMusic = params.overrideMusic ?? false;
|
|
|
|
// Don't do anything else here! Wait until create() when we attach to the camera.
|
|
}
|
|
|
|
/**
|
|
* Called when the PlayState is switched to.
|
|
*/
|
|
public override function create():Void
|
|
{
|
|
if (instance != null)
|
|
{
|
|
// TODO: Do something in this case? IDK.
|
|
trace('WARNING: PlayState instance already exists. This should not happen.');
|
|
}
|
|
instance = this;
|
|
|
|
if (!assertChartExists()) return;
|
|
|
|
if (false)
|
|
{
|
|
// Displays the camera follow point as a sprite for debug purposes.
|
|
cameraFollowPoint = new FlxSprite(0, 0).makeGraphic(8, 8, 0xFF00FF00);
|
|
cameraFollowPoint.visible = false;
|
|
cameraFollowPoint.zIndex = 1000000;
|
|
}
|
|
else
|
|
{
|
|
// Camera follow point is an invisible point in space.
|
|
cameraFollowPoint = new FlxObject(0, 0);
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Prepare the current song's instrumental and vocals to be played.
|
|
if (!overrideMusic && currentChart != null)
|
|
{
|
|
currentChart.cacheInst();
|
|
currentChart.cacheVocals();
|
|
}
|
|
|
|
// Prepare the Conductor.
|
|
Conductor.instance.forceBPM(null);
|
|
|
|
if (currentChart.offsets != null)
|
|
{
|
|
Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset();
|
|
}
|
|
|
|
Conductor.instance.mapTimeChanges(currentChart.timeChanges);
|
|
Conductor.instance.update((Conductor.instance.beatLengthMs * -5) + startTimestamp);
|
|
|
|
// The song is now loaded. We can continue to initialize the play state.
|
|
initCameras();
|
|
initHealthBar();
|
|
if (!isMinimalMode)
|
|
{
|
|
initStage();
|
|
initCharacters();
|
|
}
|
|
else
|
|
{
|
|
initMinimalMode();
|
|
}
|
|
initStrumlines();
|
|
|
|
// Initialize the judgements and combo meter.
|
|
comboPopUps = new PopUpStuff();
|
|
comboPopUps.cameras = [camHUD];
|
|
add(comboPopUps);
|
|
|
|
// The little dial that shows up when you hold the Skip Cutscene key.
|
|
skipTimer = new FlxPieDial(16, 16, 32, FlxColor.WHITE, 36, CIRCLE, true, 24);
|
|
skipTimer.amount = 0;
|
|
skipTimer.zIndex = 1000;
|
|
add(skipTimer);
|
|
// Renders only in video cutscene mode.
|
|
skipTimer.cameras = [camCutscene];
|
|
|
|
#if discord_rpc
|
|
// Initialize Discord Rich Presence.
|
|
initDiscord();
|
|
#end
|
|
|
|
// Read the song's note data and pass it to the strumlines.
|
|
generateSong();
|
|
|
|
// Reset the camera's zoom and force it to focus on the camera follow point.
|
|
resetCamera();
|
|
|
|
initPreciseInputs();
|
|
|
|
FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height);
|
|
|
|
// The song is loaded and in the process of starting.
|
|
// This gets set back to false when the chart actually starts.
|
|
startingSong = true;
|
|
|
|
// TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead.
|
|
if ((currentSong?.id ?? '').toLowerCase() == 'winter-horrorland')
|
|
{
|
|
// VanillaCutscenes will call startCountdown later.
|
|
VanillaCutscenes.playHorrorStartCutscene();
|
|
}
|
|
else
|
|
{
|
|
// 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.
|
|
startCountdown();
|
|
}
|
|
|
|
// Do this last to prevent beatHit from being called before create() is done.
|
|
super.create();
|
|
|
|
leftWatermarkText.cameras = [camHUD];
|
|
rightWatermarkText.cameras = [camHUD];
|
|
|
|
// Initialize some debug stuff.
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
// Display the version number (and git commit hash) in the bottom right corner.
|
|
this.rightWatermarkText.text = Constants.VERSION;
|
|
|
|
FlxG.console.registerObject('playState', this);
|
|
#end
|
|
|
|
initialized = true;
|
|
}
|
|
|
|
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)
|
|
{
|
|
// 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.';
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
public override function update(elapsed:Float):Void
|
|
{
|
|
if (criticalFailure) return;
|
|
|
|
super.update(elapsed);
|
|
|
|
updateHealthBar();
|
|
updateScoreText();
|
|
|
|
// Handle restarting the song when needed (player death or pressing Retry)
|
|
if (needsReset)
|
|
{
|
|
if (!assertChartExists()) return;
|
|
|
|
dispatchEvent(new ScriptEvent(SONG_RETRY));
|
|
|
|
resetCamera();
|
|
|
|
persistentUpdate = true;
|
|
persistentDraw = true;
|
|
|
|
startingSong = true;
|
|
|
|
inputSpitter = [];
|
|
|
|
// Reset music properly.
|
|
|
|
FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instance.instrumentalOffset);
|
|
FlxG.sound.music.pause();
|
|
|
|
if (!overrideMusic)
|
|
{
|
|
// 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.');
|
|
}
|
|
}
|
|
vocals.pause();
|
|
vocals.time = 0;
|
|
|
|
FlxG.sound.music.volume = 1;
|
|
vocals.volume = 1;
|
|
vocals.playerVolume = 1;
|
|
vocals.opponentVolume = 1;
|
|
|
|
if (currentStage != null) currentStage.resetStage();
|
|
|
|
playerStrumline.vwooshNotes();
|
|
opponentStrumline.vwooshNotes();
|
|
|
|
playerStrumline.clean();
|
|
opponentStrumline.clean();
|
|
|
|
// 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;
|
|
songScore = 0;
|
|
Highscore.tallies.combo = 0;
|
|
Countdown.performCountdown(currentStageId.startsWith('school'));
|
|
|
|
needsReset = false;
|
|
}
|
|
|
|
// Update the conductor.
|
|
if (startingSong)
|
|
{
|
|
if (isInCountdown)
|
|
{
|
|
Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000);
|
|
if (Conductor.instance.songPosition >= (startTimestamp)) startSong();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Constants.EXT_SOUND == 'mp3')
|
|
{
|
|
Conductor.instance.formatOffset = Constants.MP3_DELAY_MS;
|
|
}
|
|
else
|
|
{
|
|
Conductor.instance.formatOffset = 0.0;
|
|
}
|
|
|
|
Conductor.instance.update(); // Normal conductor update.
|
|
}
|
|
|
|
var androidPause:Bool = false;
|
|
|
|
#if android
|
|
androidPause = FlxG.android.justPressed.BACK;
|
|
#end
|
|
|
|
// Attempt to pause the game.
|
|
if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame)
|
|
{
|
|
var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000));
|
|
|
|
dispatchEvent(event);
|
|
|
|
if (!event.eventCanceled)
|
|
{
|
|
// 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;
|
|
|
|
// 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)
|
|
{
|
|
FlxG.switchState(() -> new GitarooPause(
|
|
{
|
|
targetSong: currentSong,
|
|
targetDifficulty: currentDifficulty,
|
|
targetVariation: currentVariation,
|
|
}));
|
|
}
|
|
else
|
|
{
|
|
var boyfriendPos:FlxPoint = new FlxPoint(0, 0);
|
|
|
|
// Prevent the game from crashing if Boyfriend isn't present.
|
|
if (currentStage != null && currentStage.getBoyfriend() != null)
|
|
{
|
|
boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
|
|
}
|
|
|
|
var pauseSubState:FlxSubState = new PauseSubState(isChartingMode);
|
|
|
|
FlxTransitionableSubState.skipNextTransIn = true;
|
|
FlxTransitionableSubState.skipNextTransOut = true;
|
|
openSubState(pauseSubState);
|
|
pauseSubState.camera = camHUD;
|
|
// boyfriendPos.put(); // TODO: Why is this here?
|
|
}
|
|
|
|
#if discord_rpc
|
|
DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
|
|
#end
|
|
}
|
|
}
|
|
|
|
// Cap health.
|
|
if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX;
|
|
if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN;
|
|
|
|
// Lerp the camera zoom towards the target level.
|
|
if (subState == null)
|
|
{
|
|
FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
|
|
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
|
|
}
|
|
|
|
if (currentStage != null)
|
|
{
|
|
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
|
|
}
|
|
|
|
// TODO: Add a song event for Handle GF dance speed.
|
|
|
|
// Handle player death.
|
|
if (!isInCutscene && !disableKeys)
|
|
{
|
|
// RESET = Quick Game Over Screen
|
|
if (controls.RESET)
|
|
{
|
|
health = Constants.HEALTH_MIN;
|
|
trace('RESET = True');
|
|
}
|
|
|
|
#if CAN_CHEAT // brandon's a pussy
|
|
if (controls.CHEAT)
|
|
{
|
|
health += 0.25 * Constants.HEALTH_MAX; // +25% health.
|
|
trace('User is cheating!');
|
|
}
|
|
#end
|
|
|
|
if (health <= Constants.HEALTH_MIN && !isPracticeMode)
|
|
{
|
|
vocals.pause();
|
|
FlxG.sound.music.pause();
|
|
|
|
deathCounter += 1;
|
|
|
|
dispatchEvent(new ScriptEvent(GAME_OVER));
|
|
|
|
// Disable updates, preventing animations in the background from playing.
|
|
persistentUpdate = false;
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
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;
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
}
|
|
#end
|
|
|
|
var gameOverSubState = new GameOverSubState(
|
|
{
|
|
isChartingMode: isChartingMode,
|
|
transparent: persistentDraw
|
|
});
|
|
FlxTransitionableSubState.skipNextTransIn = true;
|
|
FlxTransitionableSubState.skipNextTransOut = true;
|
|
openSubState(gameOverSubState);
|
|
|
|
#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
|
|
}
|
|
}
|
|
|
|
// Query and activate song events.
|
|
// TODO: Check that these work even when songPosition is less than 0.
|
|
if (songEvents != null && songEvents.length > 0)
|
|
{
|
|
var songEventsToActivate:Array<SongEventData> = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition);
|
|
|
|
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 5 seconds old, skip it.
|
|
if (event.time - Conductor.instance.songPosition < -5000)
|
|
{
|
|
event.activated = true;
|
|
continue;
|
|
};
|
|
|
|
var eventEvent:SongEventScriptEvent = new SongEventScriptEvent(event);
|
|
dispatchEvent(eventEvent);
|
|
// Calling event.cancelEvent() skips the event. Neat!
|
|
if (!eventEvent.eventCanceled)
|
|
{
|
|
SongEventRegistry.handleEvent(event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle keybinds.
|
|
processInputQueue();
|
|
if (!isInCutscene && !disableKeys) debugKeyShit();
|
|
if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
|
|
|
|
// Moving notes into position is now done by Strumline.update().
|
|
processNotes(elapsed);
|
|
}
|
|
|
|
public override function dispatchEvent(event:ScriptEvent):Void
|
|
{
|
|
// ORDER: Module, Stage, Character, Song, Conversation, Note
|
|
// Modules should get the first chance to cancel the event.
|
|
|
|
// super.dispatchEvent(event) dispatches event to module scripts.
|
|
super.dispatchEvent(event);
|
|
|
|
// Dispatch event to stage script.
|
|
ScriptEventDispatcher.callEvent(currentStage, event);
|
|
|
|
// Dispatch event to character script(s).
|
|
if (currentStage != null) currentStage.dispatchToCharacters(event);
|
|
|
|
// Dispatch event to song script.
|
|
ScriptEventDispatcher.callEvent(currentSong, event);
|
|
|
|
// Dispatch event to conversation script.
|
|
ScriptEventDispatcher.callEvent(currentConversation, event);
|
|
|
|
// TODO: Dispatch event to note scripts
|
|
}
|
|
|
|
/**
|
|
* Function called before opening a new substate.
|
|
* @param subState The substate to open.
|
|
*/
|
|
public override function openSubState(subState:FlxSubState):Void
|
|
{
|
|
// 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));
|
|
|
|
if (shouldPause)
|
|
{
|
|
// Pause the music.
|
|
if (FlxG.sound.music != null)
|
|
{
|
|
musicPausedBySubState = FlxG.sound.music.playing;
|
|
FlxG.sound.music.pause();
|
|
if (vocals != null) vocals.pause();
|
|
}
|
|
|
|
// Pause the countdown.
|
|
Countdown.pauseCountdown();
|
|
}
|
|
|
|
super.openSubState(subState);
|
|
}
|
|
|
|
/**
|
|
* Function called before closing the current substate.
|
|
* @param subState
|
|
*/
|
|
public override function closeSubState():Void
|
|
{
|
|
if (Std.isOfType(subState, PauseSubState))
|
|
{
|
|
var event:ScriptEvent = new ScriptEvent(RESUME, true);
|
|
|
|
dispatchEvent(event);
|
|
|
|
if (event.eventCanceled) return;
|
|
|
|
// Resume
|
|
if (musicPausedBySubState)
|
|
{
|
|
FlxG.sound.music.play(FlxG.sound.music.time);
|
|
}
|
|
|
|
if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals();
|
|
|
|
// Resume the countdown.
|
|
Countdown.resumeCountdown();
|
|
|
|
#if discord_rpc
|
|
if (startTimer.finished)
|
|
{
|
|
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true,
|
|
currentSongLengthMs - Conductor.instance.songPosition);
|
|
}
|
|
else
|
|
{
|
|
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC);
|
|
}
|
|
#end
|
|
}
|
|
else if (Std.isOfType(subState, Transition))
|
|
{
|
|
// Do nothing.
|
|
}
|
|
|
|
super.closeSubState();
|
|
}
|
|
|
|
#if discord_rpc
|
|
/**
|
|
* Function called when the game window gains focus.
|
|
*/
|
|
public override function onFocus():Void
|
|
{
|
|
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause)
|
|
{
|
|
if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
|
|
+ ' ('
|
|
+ storyDifficultyText
|
|
+ ')', iconRPC, true,
|
|
currentSongLengthMs
|
|
- Conductor.instance.songPosition);
|
|
else
|
|
DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
|
|
}
|
|
|
|
super.onFocus();
|
|
}
|
|
|
|
/**
|
|
* Function called when the game window loses focus.
|
|
*/
|
|
public override function onFocusLost():Void
|
|
{
|
|
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText,
|
|
currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
|
|
|
|
super.onFocusLost();
|
|
}
|
|
#end
|
|
|
|
/**
|
|
* Removes any references to the current stage, then clears the stage cache,
|
|
* then reloads all the stages.
|
|
*
|
|
* 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.
|
|
*
|
|
* Call this by pressing F5 on a debug build.
|
|
*/
|
|
override function debug_refreshModules():Void
|
|
{
|
|
// Prevent further gameplay updates, which will try to reference dead objects.
|
|
criticalFailure = true;
|
|
|
|
// 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);
|
|
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();
|
|
}
|
|
|
|
super.debug_refreshModules();
|
|
|
|
var event:ScriptEvent = new ScriptEvent(CREATE, false);
|
|
ScriptEventDispatcher.callEvent(currentSong, event);
|
|
}
|
|
|
|
override function stepHit():Bool
|
|
{
|
|
if (criticalFailure || !initialized) return false;
|
|
|
|
// super.stepHit() returns false if a module cancelled the event.
|
|
if (!super.stepHit()) return false;
|
|
|
|
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))
|
|
{
|
|
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));
|
|
resyncVocals();
|
|
}
|
|
|
|
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
|
|
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
|
|
|
|
return true;
|
|
}
|
|
|
|
override function beatHit():Bool
|
|
{
|
|
if (criticalFailure || !initialized) return false;
|
|
|
|
// super.beatHit() returns false if a module cancelled the event.
|
|
if (!super.beatHit()) return false;
|
|
|
|
if (isGamePaused) return false;
|
|
|
|
if (generatedMusic)
|
|
{
|
|
// TODO: Sort more efficiently, or less often, to improve performance.
|
|
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
// Zoom camera in (1.5%)
|
|
FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom;
|
|
// Hud zooms double (3%)
|
|
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
|
|
}
|
|
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
|
|
|
|
// That combo milestones that got spoiled that one time.
|
|
// Comes with NEAT visual and audio effects.
|
|
|
|
// bruh this var is bonkers i thot it was a function lmfaooo
|
|
|
|
// Break up into individual lines to aid debugging.
|
|
|
|
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)];
|
|
// 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);
|
|
// shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection));
|
|
// }
|
|
|
|
if (shouldShowComboText)
|
|
{
|
|
var animShit:ComboMilestone = new ComboMilestone(-100, 300, Highscore.tallies.combo);
|
|
animShit.scrollFactor.set(0.6, 0.6);
|
|
animShit.cameras = [camHUD];
|
|
add(animShit);
|
|
|
|
var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
|
|
|
|
new FlxTimer().start(((Conductor.instance.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) {
|
|
animShit.forceFinish();
|
|
});
|
|
}
|
|
|
|
if (playerStrumline != null) playerStrumline.onBeatHit();
|
|
if (opponentStrumline != null) opponentStrumline.onBeatHit();
|
|
|
|
// Make the characters dance on the beat
|
|
danceOnBeat();
|
|
|
|
return true;
|
|
}
|
|
|
|
public override function destroy():Void
|
|
{
|
|
if (currentConversation != null)
|
|
{
|
|
remove(currentConversation);
|
|
currentConversation.kill();
|
|
}
|
|
|
|
performCleanup();
|
|
|
|
super.destroy();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
if (currentStage == null) return;
|
|
|
|
// TODO: Add HEY! song events to Tutorial.
|
|
if (Conductor.instance.currentBeat % 16 == 15
|
|
&& currentStage.getDad().characterId == 'gf'
|
|
&& Conductor.instance.currentBeat > 16
|
|
&& Conductor.instance.currentBeat < 48)
|
|
{
|
|
currentStage.getBoyfriend().playAnimation('hey', true);
|
|
currentStage.getDad().playAnimation('cheer', true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes the game and HUD cameras.
|
|
*/
|
|
function initCameras():Void
|
|
{
|
|
camGame = new FunkinCamera();
|
|
camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
|
|
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.
|
|
|
|
FlxG.cameras.reset(camGame);
|
|
FlxG.cameras.add(camHUD, false);
|
|
FlxG.cameras.add(camCutscene, false);
|
|
|
|
// Configure camera follow point.
|
|
if (previousCameraFollowPoint != null)
|
|
{
|
|
cameraFollowPoint.setPosition(previousCameraFollowPoint.x, previousCameraFollowPoint.y);
|
|
previousCameraFollowPoint = null;
|
|
}
|
|
add(cameraFollowPoint);
|
|
}
|
|
|
|
/**
|
|
* Initializes the health bar on the HUD.
|
|
*/
|
|
function initHealthBar():Void
|
|
{
|
|
var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
|
|
healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
|
|
healthBarBG.screenCenter(X);
|
|
healthBarBG.scrollFactor.set(0, 0);
|
|
add(healthBarBG);
|
|
|
|
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);
|
|
add(healthBar);
|
|
|
|
// 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();
|
|
add(scoreText);
|
|
|
|
// Move the health bar to the HUD camera.
|
|
healthBar.cameras = [camHUD];
|
|
healthBarBG.cameras = [camHUD];
|
|
scoreText.cameras = [camHUD];
|
|
}
|
|
|
|
/**
|
|
* Generates the stage and all its props.
|
|
*/
|
|
function initStage():Void
|
|
{
|
|
loadStage(currentStageId);
|
|
}
|
|
|
|
function initMinimalMode():Void
|
|
{
|
|
// Create the green background.
|
|
var menuBG = new FlxSprite().loadGraphic(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);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
|
|
if (currentStage != null)
|
|
{
|
|
// Actually create and position the sprites.
|
|
var event:ScriptEvent = new ScriptEvent(CREATE, false);
|
|
ScriptEventDispatcher.callEvent(currentStage, event);
|
|
|
|
// Apply camera zoom level from stage data.
|
|
defaultCameraZoom = currentStage.camZoom;
|
|
|
|
// Add the stage to the scene.
|
|
this.add(currentStage);
|
|
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
FlxG.console.registerObject('stage', currentStage);
|
|
#end
|
|
}
|
|
else
|
|
{
|
|
// lolol
|
|
lime.app.Application.current.window.alert('Unable to load stage ${id}, is its data corrupted?.', 'Stage Error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.');
|
|
}
|
|
|
|
var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the variation we are playing on by manipulating targetVariation.
|
|
|
|
//
|
|
// GIRLFRIEND
|
|
//
|
|
var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.girlfriend);
|
|
|
|
if (girlfriend != null)
|
|
{
|
|
girlfriend.characterType = CharacterType.GF;
|
|
}
|
|
else if (currentCharacterData.girlfriend != '')
|
|
{
|
|
trace('WARNING: Could not load girlfriend character with ID ${currentCharacterData.girlfriend}, skipping...');
|
|
}
|
|
else
|
|
{
|
|
// Chosen GF was '' so we don't load one.
|
|
}
|
|
|
|
//
|
|
// DAD
|
|
//
|
|
var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.opponent);
|
|
|
|
if (dad != null)
|
|
{
|
|
dad.characterType = CharacterType.DAD;
|
|
}
|
|
|
|
//
|
|
// OPPONENT HEALTH ICON
|
|
//
|
|
iconP2 = new HealthIcon('dad', 1);
|
|
iconP2.y = healthBar.y - (iconP2.height / 2);
|
|
dad.initHealthIcon(true); // Apply the character ID here
|
|
add(iconP2);
|
|
iconP2.cameras = [camHUD];
|
|
|
|
//
|
|
// BOYFRIEND
|
|
//
|
|
var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.player);
|
|
|
|
if (boyfriend != null)
|
|
{
|
|
boyfriend.characterType = CharacterType.BF;
|
|
}
|
|
|
|
//
|
|
// PLAYER HEALTH ICON
|
|
//
|
|
iconP1 = new HealthIcon('bf', 0);
|
|
iconP1.y = healthBar.y - (iconP1.height / 2);
|
|
boyfriend.initHealthIcon(false); // Apply the character ID here
|
|
add(iconP1);
|
|
iconP1.cameras = [camHUD];
|
|
|
|
//
|
|
// ADD CHARACTERS TO SCENE
|
|
//
|
|
|
|
if (currentStage != null)
|
|
{
|
|
// Characters get added to the stage, not the main scene.
|
|
if (girlfriend != null)
|
|
{
|
|
currentStage.addCharacter(girlfriend, GF);
|
|
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
FlxG.console.registerObject('gf', girlfriend);
|
|
#end
|
|
}
|
|
|
|
if (boyfriend != null)
|
|
{
|
|
currentStage.addCharacter(boyfriend, BF);
|
|
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
FlxG.console.registerObject('bf', boyfriend);
|
|
#end
|
|
}
|
|
|
|
if (dad != null)
|
|
{
|
|
currentStage.addCharacter(dad, DAD);
|
|
// Camera starts at dad.
|
|
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
|
|
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
FlxG.console.registerObject('dad', dad);
|
|
#end
|
|
}
|
|
|
|
// Rearrange by z-indexes.
|
|
currentStage.refresh();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs the strumlines for each player.
|
|
*/
|
|
function initStrumlines():Void
|
|
{
|
|
var noteStyleId:String = switch (currentStageId)
|
|
{
|
|
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();
|
|
|
|
playerStrumline = new Strumline(noteStyle, true);
|
|
opponentStrumline = new Strumline(noteStyle, false);
|
|
add(playerStrumline);
|
|
add(opponentStrumline);
|
|
|
|
// 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 = 200;
|
|
playerStrumline.cameras = [camHUD];
|
|
|
|
// Position the opponent strumline on the left half of the screen
|
|
opponentStrumline.x = Constants.STRUMLINE_X_OFFSET;
|
|
opponentStrumline.y = Preferences.downscroll ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
|
|
opponentStrumline.zIndex = 100;
|
|
opponentStrumline.cameras = [camHUD];
|
|
|
|
if (!PlayStatePlaylist.isStoryMode)
|
|
{
|
|
playerStrumline.fadeInArrows();
|
|
opponentStrumline.fadeInArrows();
|
|
}
|
|
|
|
this.refresh();
|
|
}
|
|
|
|
/**
|
|
* Initializes the Discord Rich Presence.
|
|
*/
|
|
function initDiscord():Void
|
|
{
|
|
#if discord_rpc
|
|
storyDifficultyText = difficultyString();
|
|
iconRPC = currentSong.player2;
|
|
|
|
// To avoid having duplicate images in Discord assets
|
|
switch (iconRPC)
|
|
{
|
|
case 'senpai-angry':
|
|
iconRPC = 'senpai';
|
|
case 'monster-christmas':
|
|
iconRPC = 'monster';
|
|
case 'mom-car':
|
|
iconRPC = 'mom';
|
|
}
|
|
|
|
// 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';
|
|
|
|
// Updating Discord Rich Presence.
|
|
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC);
|
|
#end
|
|
}
|
|
|
|
function initPreciseInputs():Void
|
|
{
|
|
FlxG.keys.preventDefaultKeys = [];
|
|
PreciseInputManager.instance.onInputPressed.add(onKeyPress);
|
|
PreciseInputManager.instance.onInputReleased.add(onKeyRelease);
|
|
}
|
|
|
|
/**
|
|
* Initializes the song (applying the chart, generating the notes, etc.)
|
|
* Should be done before the countdown starts.
|
|
*/
|
|
function generateSong():Void
|
|
{
|
|
if (currentChart == null)
|
|
{
|
|
trace('Song difficulty could not be loaded.');
|
|
}
|
|
|
|
// Conductor.instance.forceBPM(currentChart.getStartingBPM());
|
|
|
|
if (!overrideMusic)
|
|
{
|
|
// 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.');
|
|
}
|
|
}
|
|
|
|
regenNoteData();
|
|
|
|
var event:ScriptEvent = new ScriptEvent(CREATE, false);
|
|
ScriptEventDispatcher.callEvent(currentSong, event);
|
|
|
|
generatedMusic = true;
|
|
}
|
|
|
|
/**
|
|
* Read note data from the chart and generate the notes.
|
|
*/
|
|
function regenNoteData(startTime:Float = 0):Void
|
|
{
|
|
Highscore.tallies.combo = 0;
|
|
Highscore.tallies = new Tallies();
|
|
|
|
// Reset song events.
|
|
songEvents = currentChart.getEvents();
|
|
SongEventRegistry.resetEvents(songEvents);
|
|
|
|
// Reset the notes on each strumline.
|
|
var playerNoteData:Array<SongNoteData> = [];
|
|
var opponentNoteData:Array<SongNoteData> = [];
|
|
|
|
for (songNote in currentChart.notes)
|
|
{
|
|
var strumTime:Float = songNote.time;
|
|
if (strumTime < startTime) continue; // Skip notes that are before the start time.
|
|
|
|
var noteData:Int = songNote.getDirection();
|
|
|
|
var playerNote:Bool = true;
|
|
|
|
if (noteData > 3) playerNote = false;
|
|
|
|
switch (songNote.getStrumlineIndex())
|
|
{
|
|
case 0:
|
|
playerNoteData.push(songNote);
|
|
case 1:
|
|
opponentNoteData.push(songNote);
|
|
}
|
|
}
|
|
|
|
playerStrumline.applyNoteData(playerNoteData);
|
|
opponentStrumline.applyNoteData(opponentNoteData);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
// If Countdown.performCountdown returns false, then the countdown was canceled by a script.
|
|
var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school'));
|
|
if (!result) return;
|
|
|
|
isInCutscene = false;
|
|
camCutscene.visible = false;
|
|
camHUD.visible = true;
|
|
}
|
|
|
|
/**
|
|
* Displays a dialogue cutscene with the given ID.
|
|
* This is used by song scripts to display dialogue.
|
|
*/
|
|
public function startConversation(conversationId:String):Void
|
|
{
|
|
isInCutscene = true;
|
|
|
|
currentConversation = ConversationRegistry.instance.fetchEntry(conversationId);
|
|
if (currentConversation == null) return;
|
|
|
|
currentConversation.completeCallback = onConversationComplete;
|
|
currentConversation.cameras = [camCutscene];
|
|
currentConversation.zIndex = 1000;
|
|
add(currentConversation);
|
|
refresh();
|
|
|
|
var event:ScriptEvent = new ScriptEvent(CREATE, false);
|
|
ScriptEventDispatcher.callEvent(currentConversation, event);
|
|
}
|
|
|
|
/**
|
|
* Handler function called when a conversation ends.
|
|
*/
|
|
function onConversationComplete():Void
|
|
{
|
|
isInCutscene = false;
|
|
remove(currentConversation);
|
|
currentConversation = null;
|
|
|
|
if (startingSong && !isInCountdown)
|
|
{
|
|
startCountdown();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts playing the song after the countdown has completed.
|
|
*/
|
|
function startSong():Void
|
|
{
|
|
dispatchEvent(new ScriptEvent(SONG_START));
|
|
|
|
startingSong = false;
|
|
|
|
if (!overrideMusic && !isGamePaused && currentChart != null)
|
|
{
|
|
currentChart.playInst(1.0, false);
|
|
}
|
|
|
|
FlxG.sound.music.onComplete = endSong;
|
|
// 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.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
|
|
|
trace('Playing vocals...');
|
|
add(vocals);
|
|
|
|
FlxG.sound.music.play(FlxG.sound.music.time);
|
|
vocals.play();
|
|
resyncVocals();
|
|
|
|
#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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resyncronize the vocal tracks if they have become offset from the instrumental.
|
|
*/
|
|
function resyncVocals():Void
|
|
{
|
|
if (vocals == null) return;
|
|
|
|
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
|
|
if (!FlxG.sound.music.playing) return;
|
|
|
|
vocals.pause();
|
|
|
|
FlxG.sound.music.play(FlxG.sound.music.time);
|
|
|
|
vocals.time = FlxG.sound.music.time;
|
|
vocals.play(false, FlxG.sound.music.time);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Updates the values of the health bar.
|
|
*/
|
|
function updateHealthBar():Void
|
|
{
|
|
healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
|
|
}
|
|
|
|
/**
|
|
* Callback executed when one of the note keys is pressed.
|
|
*/
|
|
function onKeyPress(event:PreciseInputEvent):Void
|
|
{
|
|
if (isGamePaused) return;
|
|
|
|
// Do the minimal possible work here.
|
|
inputPressQueue.push(event);
|
|
}
|
|
|
|
/**
|
|
* Callback executed when one of the note keys is released.
|
|
*/
|
|
function onKeyRelease(event:PreciseInputEvent):Void
|
|
{
|
|
if (isGamePaused) return;
|
|
|
|
// Do the minimal possible work here.
|
|
inputReleaseQueue.push(event);
|
|
}
|
|
|
|
/**
|
|
* Handles opponent note hits and player note misses.
|
|
*/
|
|
function processNotes(elapsed:Float):Void
|
|
{
|
|
if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return;
|
|
|
|
// Process notes on the opponent's side.
|
|
for (note in opponentStrumline.notes.members)
|
|
{
|
|
if (note == null) continue;
|
|
|
|
// TODO: Does this properly account for offsets?
|
|
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
|
|
var hitWindowCenter = note.strumTime;
|
|
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
|
|
|
|
if (Conductor.instance.songPosition > hitWindowEnd)
|
|
{
|
|
if (note.hasMissed) continue;
|
|
|
|
note.tooEarly = false;
|
|
note.mayHit = false;
|
|
note.hasMissed = true;
|
|
|
|
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
|
|
}
|
|
else if (Conductor.instance.songPosition > hitWindowCenter)
|
|
{
|
|
if (note.hasBeenHit) continue;
|
|
|
|
// 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);
|
|
dispatchEvent(event);
|
|
|
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
|
if (event.eventCanceled) continue;
|
|
|
|
// 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);
|
|
|
|
if (note.holdNoteSprite != null)
|
|
{
|
|
opponentStrumline.playNoteHoldCover(note.holdNoteSprite);
|
|
}
|
|
}
|
|
else if (Conductor.instance.songPosition > hitWindowStart)
|
|
{
|
|
if (note.hasBeenHit || note.hasMissed) continue;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
{
|
|
currentStage.getDad().holdTimer = 0;
|
|
}
|
|
}
|
|
|
|
if (holdNote.missedNote && !holdNote.handledMiss)
|
|
{
|
|
// When the opponent drops a hold note.
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
|
|
var hitWindowCenter = note.strumTime;
|
|
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
|
|
|
|
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;
|
|
}
|
|
|
|
// This becomes true when the note leaves the hit window.
|
|
// It might still be on screen.
|
|
if (note.hasMissed && !note.handledMiss)
|
|
{
|
|
// 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, 0, true);
|
|
dispatchEvent(event);
|
|
|
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
|
if (event.eventCanceled) continue;
|
|
|
|
// Judge the miss.
|
|
// NOTE: This is what handles the scoring.
|
|
trace('Missed note! ${note.noteData}');
|
|
onNoteMiss(note);
|
|
|
|
note.handledMiss = true;
|
|
}
|
|
}
|
|
|
|
// Process hold notes on the player's side.
|
|
// This handles scoring so we don't need it on the opponent's side.
|
|
for (holdNote in playerStrumline.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)
|
|
{
|
|
// Grant the player health.
|
|
health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
|
|
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
playerStrumline.handleSkippedNotes();
|
|
opponentStrumline.handleSkippedNotes();
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
inputPressQueue = [];
|
|
inputReleaseQueue = [];
|
|
return;
|
|
}
|
|
|
|
// Generate a list of notes within range.
|
|
var notesInRange:Array<NoteSprite> = playerStrumline.getNotesMayHit();
|
|
var holdNotesInRange:Array<SustainTrail> = playerStrumline.getHoldNotesHitOrMissed();
|
|
|
|
// 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)
|
|
{
|
|
var input:PreciseInputEvent = inputPressQueue.shift();
|
|
|
|
playerStrumline.pressKey(input.noteDirection);
|
|
|
|
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).
|
|
ghostNoteMiss(input.noteDirection, notesInRange.length > 0);
|
|
|
|
// Play the strumline animation.
|
|
playerStrumline.playPress(input.noteDirection);
|
|
}
|
|
else if (notesInDirection.length > 0)
|
|
{
|
|
// 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}');
|
|
goodNoteHit(targetNote, input);
|
|
|
|
notesInDirection.remove(targetNote);
|
|
|
|
// Play the strumline animation.
|
|
playerStrumline.playConfirm(input.noteDirection);
|
|
}
|
|
else
|
|
{
|
|
// Play the strumline animation.
|
|
playerStrumline.playPress(input.noteDirection);
|
|
}
|
|
}
|
|
|
|
while (inputReleaseQueue.length > 0)
|
|
{
|
|
var input:PreciseInputEvent = inputReleaseQueue.shift();
|
|
|
|
// Play the strumline animation.
|
|
playerStrumline.playStatic(input.noteDirection);
|
|
|
|
playerStrumline.releaseKey(input.noteDirection);
|
|
}
|
|
}
|
|
|
|
function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void
|
|
{
|
|
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, Highscore.tallies.combo + 1, true);
|
|
dispatchEvent(event);
|
|
|
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
|
if (event.eventCanceled) return;
|
|
|
|
popUpScore(note, input);
|
|
|
|
if (note.isHoldNote && note.holdNoteSprite != null)
|
|
{
|
|
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
|
|
}
|
|
|
|
vocals.playerVolume = 1;
|
|
}
|
|
|
|
/**
|
|
* Called when a note leaves the screen and is considered missed by the player.
|
|
* @param note
|
|
*/
|
|
function onNoteMiss(note:NoteSprite):Void
|
|
{
|
|
// a MISS is when you let a note scroll past you!!
|
|
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true);
|
|
dispatchEvent(event);
|
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
|
if (event.eventCanceled) return;
|
|
|
|
health -= Constants.HEALTH_MISS_PENALTY;
|
|
songScore -= 10;
|
|
|
|
if (!isPracticeMode)
|
|
{
|
|
// 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
|
|
];
|
|
|
|
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
|
|
});
|
|
}
|
|
}
|
|
vocals.playerVolume = 0;
|
|
|
|
Highscore.tallies.missed++;
|
|
|
|
if (Highscore.tallies.combo != 0)
|
|
{
|
|
// Break the combo.
|
|
Highscore.tallies.combo = comboPopUps.displayCombo(0);
|
|
}
|
|
|
|
if (event.playSound)
|
|
{
|
|
vocals.playerVolume = 0;
|
|
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param direction
|
|
* @param hasPossibleNotes
|
|
*/
|
|
function ghostNoteMiss(direction:NoteDirection, hasPossibleNotes:Bool = true):Void
|
|
{
|
|
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).
|
|
- 10 // Amount of score to add (negative).
|
|
);
|
|
dispatchEvent(event);
|
|
|
|
// Calling event.cancelEvent() skips animations and penalties. Neat!
|
|
if (event.eventCanceled) return;
|
|
|
|
health += event.healthChange;
|
|
songScore += event.scoreChange;
|
|
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
if (event.playSound)
|
|
{
|
|
vocals.playerVolume = 0;
|
|
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debug keys. Disabled while in cutscenes.
|
|
*/
|
|
function debugKeyShit():Void
|
|
{
|
|
#if !debug
|
|
perfectMode = false;
|
|
#else
|
|
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
|
|
#end
|
|
|
|
// Eject button
|
|
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
|
|
|
|
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
|
|
|
// Open the stage editor overlaying the current state.
|
|
if (controls.DEBUG_STAGE)
|
|
{
|
|
// 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,
|
|
}));
|
|
}
|
|
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
// 1: End the song immediately.
|
|
if (FlxG.keys.justPressed.ONE) endSong();
|
|
|
|
// 2: Gain 10% health.
|
|
if (FlxG.keys.justPressed.TWO) health += 0.1 * Constants.HEALTH_MAX;
|
|
|
|
// 3: Lose 5% health.
|
|
if (FlxG.keys.justPressed.THREE) health -= 0.05 * Constants.HEALTH_MAX;
|
|
#end
|
|
|
|
// 9: Toggle the old icon.
|
|
if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon();
|
|
|
|
#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);
|
|
#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, input:PreciseInputEvent):Void
|
|
{
|
|
vocals.playerVolume = 1;
|
|
|
|
// 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 - daNote.noteData.time - inputLatencyMs);
|
|
|
|
var score = Scoring.scoreNote(noteDiff, PBOT1);
|
|
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
|
|
|
|
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;
|
|
switch (daRating)
|
|
{
|
|
case 'sick':
|
|
Highscore.tallies.sick += 1;
|
|
health += Constants.HEALTH_SICK_BONUS;
|
|
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
|
|
case 'good':
|
|
Highscore.tallies.good += 1;
|
|
health += Constants.HEALTH_GOOD_BONUS;
|
|
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
|
|
case 'bad':
|
|
Highscore.tallies.bad += 1;
|
|
health += Constants.HEALTH_BAD_BONUS;
|
|
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
|
|
case 'shit':
|
|
Highscore.tallies.shit += 1;
|
|
health += Constants.HEALTH_SHIT_BONUS;
|
|
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
|
|
}
|
|
|
|
if (isComboBreak)
|
|
{
|
|
// Break the combo, but don't increment tallies.misses.
|
|
Highscore.tallies.combo = comboPopUps.displayCombo(0);
|
|
}
|
|
else
|
|
{
|
|
Highscore.tallies.combo++;
|
|
Highscore.tallies.totalNotesHit++;
|
|
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
|
|
}
|
|
|
|
playerStrumline.hitNote(daNote, !isComboBreak);
|
|
|
|
if (daRating == "sick")
|
|
{
|
|
playerStrumline.playNoteSplash(daNote.noteData.getDirection());
|
|
}
|
|
|
|
songScore += score;
|
|
|
|
if (!isPracticeMode)
|
|
{
|
|
// TODO: Input splitter uses old input system, make it pull from the precise input queue directly.
|
|
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
|
|
});
|
|
}
|
|
}
|
|
comboPopUps.displayRating(daRating);
|
|
if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
if (currentConversation != null)
|
|
{
|
|
if (controls.CUTSCENE_ADVANCE) currentConversation?.advanceConversation();
|
|
|
|
if (controls.CUTSCENE_SKIP)
|
|
{
|
|
currentConversation?.trySkipConversation(elapsed);
|
|
}
|
|
else
|
|
{
|
|
currentConversation?.trySkipConversation(-1);
|
|
}
|
|
}
|
|
else if (VideoCutscene.isPlaying())
|
|
{
|
|
// This is a video cutscene.
|
|
|
|
if (controls.CUTSCENE_SKIP)
|
|
{
|
|
trySkipVideoCutscene(elapsed);
|
|
}
|
|
else
|
|
{
|
|
trySkipVideoCutscene(-1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle logic for the skip timer.
|
|
* If the skip button is being held, pass the amount of time elapsed since last game update.
|
|
* If the skip button has been released, pass a negative number.
|
|
*/
|
|
function trySkipVideoCutscene(elapsed:Float):Void
|
|
{
|
|
if (skipTimer == null || skipTimer.animation == null) return;
|
|
|
|
if (elapsed < 0)
|
|
{
|
|
skipHeldTimer = 0.0;
|
|
}
|
|
else
|
|
{
|
|
skipHeldTimer += elapsed;
|
|
}
|
|
|
|
skipTimer.visible = skipHeldTimer >= 0.05;
|
|
skipTimer.amount = Math.min(skipHeldTimer / 1.5, 1.0);
|
|
|
|
if (skipHeldTimer >= 1.5)
|
|
{
|
|
VideoCutscene.finishVideo();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* End the song. Handle saving high scores and transitioning to the results screen.
|
|
*/
|
|
function endSong():Void
|
|
{
|
|
dispatchEvent(new ScriptEvent(SONG_END));
|
|
|
|
#if sys
|
|
// spitter for ravy, teehee!!
|
|
var writer = new json2object.JsonWriter<Array<ScoreInput>>();
|
|
var output = writer.write(inputSpitter, ' ');
|
|
sys.io.File.saveContent("./scores.json", output);
|
|
#end
|
|
|
|
deathCounter = 0;
|
|
mayPauseGame = false;
|
|
FlxG.sound.music.volume = 0;
|
|
vocals.volume = 0;
|
|
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: Highscore.tallies.totalNotes,
|
|
},
|
|
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
|
|
};
|
|
|
|
if (Save.get().isSongHighScore(currentSong.id, currentDifficulty, data))
|
|
{
|
|
Save.get().setSongScore(currentSong.id, currentDifficulty, data);
|
|
#if newgrounds
|
|
NGio.postScore(score, currentSong.id);
|
|
#end
|
|
}
|
|
}
|
|
|
|
if (PlayStatePlaylist.isStoryMode)
|
|
{
|
|
PlayStatePlaylist.campaignScore += songScore;
|
|
|
|
// Pop the next song ID from the list.
|
|
// Returns null if the list is empty.
|
|
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
|
|
|
|
if (targetSongId == null)
|
|
{
|
|
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
|
|
|
|
// transIn = FlxTransitionableState.defaultTransIn;
|
|
// transOut = FlxTransitionableState.defaultTransOut;
|
|
|
|
// TODO: Rework week unlock logic.
|
|
// StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true;
|
|
|
|
if (currentSong.validScore)
|
|
{
|
|
NGio.unlockMedal(60961);
|
|
|
|
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 / Highscore.tallies.totalNotes,
|
|
};
|
|
|
|
if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
|
|
{
|
|
Save.get().setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
|
|
#if newgrounds
|
|
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
|
|
#end
|
|
}
|
|
}
|
|
|
|
if (isSubState)
|
|
{
|
|
this.close();
|
|
}
|
|
else
|
|
{
|
|
moveToResultsScreen();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var difficulty:String = '';
|
|
|
|
trace('Loading next song ($targetSongId : $difficulty)');
|
|
|
|
FlxTransitionableState.skipNextTransIn = true;
|
|
FlxTransitionableState.skipNextTransOut = true;
|
|
|
|
FlxG.sound.music.stop();
|
|
vocals.stop();
|
|
|
|
// TODO: Softcode this cutscene.
|
|
if (currentSong.id == 'eggnog')
|
|
{
|
|
var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom,
|
|
-FlxG.height * FlxG.camera.zoom).makeGraphic(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
|
|
blackShit.scrollFactor.set();
|
|
add(blackShit);
|
|
camHUD.visible = false;
|
|
isInCutscene = true;
|
|
|
|
FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() {
|
|
// no camFollow so it centers on horror tree
|
|
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
|
|
// Load and cache the song's charts.
|
|
// TODO: Do this in the loading state.
|
|
targetSong.cacheCharts(true);
|
|
|
|
LoadingState.loadAndSwitchState(() -> {
|
|
var nextPlayState:PlayState = new PlayState(
|
|
{
|
|
targetSong: targetSong,
|
|
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
|
|
targetVariation: currentVariation,
|
|
});
|
|
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
|
|
return nextPlayState;
|
|
});
|
|
});
|
|
}
|
|
else
|
|
{
|
|
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
|
|
// Load and cache the song's charts.
|
|
// TODO: Do this in the loading state.
|
|
targetSong.cacheCharts(true);
|
|
LoadingState.loadAndSwitchState(() -> {
|
|
var nextPlayState:PlayState = new PlayState(
|
|
{
|
|
targetSong: targetSong,
|
|
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
|
|
targetVariation: currentVariation,
|
|
});
|
|
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
|
|
return nextPlayState;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (isSubState)
|
|
{
|
|
this.close();
|
|
}
|
|
else
|
|
{
|
|
moveToResultsScreen();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override function close():Void
|
|
{
|
|
criticalFailure = true; // Stop game updates.
|
|
performCleanup();
|
|
super.close();
|
|
}
|
|
|
|
/**
|
|
* Perform necessary cleanup before leaving the PlayState.
|
|
*/
|
|
function performCleanup():Void
|
|
{
|
|
if (currentChart != null)
|
|
{
|
|
// TODO: Uncache the song.
|
|
}
|
|
|
|
if (!overrideMusic)
|
|
{
|
|
// Stop the music.
|
|
FlxG.sound.music.pause();
|
|
if (vocals != null) vocals.stop();
|
|
}
|
|
else
|
|
{
|
|
FlxG.sound.music.pause();
|
|
if (vocals != null)
|
|
{
|
|
vocals.pause();
|
|
remove(vocals);
|
|
}
|
|
}
|
|
|
|
// Remove reference to stage and remove sprites from it to save memory.
|
|
if (currentStage != null)
|
|
{
|
|
remove(currentStage);
|
|
currentStage.kill();
|
|
dispatchEvent(new ScriptEvent(DESTROY, false));
|
|
currentStage = null;
|
|
}
|
|
|
|
GameOverSubState.reset();
|
|
|
|
// Clear the static reference to this state.
|
|
instance = null;
|
|
}
|
|
|
|
/**
|
|
* Play the camera zoom animation and move to the results screen.
|
|
*/
|
|
function moveToResultsScreen():Void
|
|
{
|
|
trace('WENT TO RESULTS SCREEN!');
|
|
|
|
// Stop camera zooming on beat.
|
|
cameraZoomRate = 0;
|
|
|
|
// 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;
|
|
|
|
if (targetBF)
|
|
{
|
|
FlxG.camera.follow(currentStage.getBoyfriend(), null, 0.05);
|
|
FlxG.camera.targetOffset.y -= 350;
|
|
FlxG.camera.targetOffset.x += 20;
|
|
}
|
|
else if (targetDad)
|
|
{
|
|
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;
|
|
}
|
|
|
|
FlxTween.tween(camHUD, {alpha: 0}, 0.6);
|
|
|
|
// 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(_) {
|
|
persistentUpdate = false;
|
|
vocals.stop();
|
|
camHUD.alpha = 1;
|
|
var res:ResultState = new ResultState(
|
|
{
|
|
storyMode: PlayStatePlaylist.isStoryMode,
|
|
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
|
|
tallies: Highscore.tallies,
|
|
});
|
|
res.camera = camHUD;
|
|
openSubState(res);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pauses music and vocals easily.
|
|
*/
|
|
public function pauseMusic():Void
|
|
{
|
|
FlxG.sound.music.pause();
|
|
vocals.pause();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
|
|
}
|
|
|
|
#if (debug || FORCE_DEBUG_VERSION)
|
|
/**
|
|
* 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.
|
|
*/
|
|
function changeSection(sections:Int):Void
|
|
{
|
|
FlxG.sound.music.pause();
|
|
|
|
var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections);
|
|
var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
|
|
|
|
FlxG.sound.music.time = targetTimeMs;
|
|
|
|
handleSkippedNotes();
|
|
// regenNoteData(FlxG.sound.music.time);
|
|
|
|
Conductor.instance.update(FlxG.sound.music.time);
|
|
|
|
resyncVocals();
|
|
}
|
|
#end
|
|
}
|