2022-03-08 08:13:53 +00:00
package funkin . play ;
2020-10-03 06:50:15 +00:00
2025-06-22 20:08:36 +00:00
import funkin . play . PauseSubState . PauseMode ;
2023-05-25 22:34:26 +00:00
import flixel . addons . transition . FlxTransitionableState ;
2024-02-12 21:50:29 +00:00
import flixel . addons . transition . Transition ;
2025-01-06 22:20:02 +00:00
import funkin . ui . FullScreenScaleMode ;
2020-10-25 20:51:06 +00:00
import flixel . FlxCamera ;
2020-10-04 06:42:58 +00:00
import flixel . FlxObject ;
2020-10-09 21:24:20 +00:00
import flixel . FlxSubState ;
2020-10-05 09:48:30 +00:00
import flixel . math . FlxMath ;
2023-06-02 19:35:01 +00:00
import flixel . math . FlxPoint ;
2025-04-14 00:55:46 +00:00
import flixel . sound . FlxSound ;
2020-10-03 06:50:15 +00:00
import flixel . text . FlxText ;
2020-10-03 19:32:15 +00:00
import flixel . tweens . FlxTween ;
2025-06-08 03:09:40 +00:00
import flixel . tweens . FlxEase ;
2020-10-05 20:32:41 +00:00
import flixel . ui . FlxBar ;
2020-10-04 06:42:58 +00:00
import flixel . util . FlxColor ;
2024-07-10 18:23:50 +00:00
import flixel . util . FlxStringUtil ;
2024-09-19 14:03:16 +00:00
import flixel . util . FlxTimer ;
2024-03-23 21:50:48 +00:00
import funkin . audio . FunkinSound ;
2023-09-17 11:45:10 +00:00
import funkin . audio . VoicesGroup ;
2025-08-20 18:26:48 +00:00
import funkin . data . dialogue . ConversationRegistry ;
2024-02-12 21:50:29 +00:00
import funkin . data . event . SongEventRegistry ;
2023-09-17 11:45:10 +00:00
import funkin . data . notestyle . NoteStyleRegistry ;
2024-02-12 21:50:29 +00:00
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 ;
2024-03-23 21:50:48 +00:00
import funkin . graphics . FunkinCamera ;
import funkin . graphics . FunkinSprite ;
2024-02-12 21:50:29 +00:00
import funkin . Highscore . Tallies ;
2023-06-22 05:41:01 +00:00
import funkin . input . PreciseInputManager ;
2022-03-11 06:30:01 +00:00
import funkin . modding . events . ScriptEvent ;
2025-03-27 06:55:45 +00:00
import funkin . api . newgrounds . Events ;
2022-03-11 06:30:01 +00:00
import funkin . modding . events . ScriptEventDispatcher ;
2022-04-19 00:39:41 +00:00
import funkin . play . character . BaseCharacter ;
2023-05-25 22:34:26 +00:00
import funkin . play . character . CharacterData . CharacterDataParser ;
2024-02-12 21:50:29 +00:00
import funkin . play . components . HealthIcon ;
import funkin . play . components . PopUpStuff ;
import funkin . play . cutscene . dialogue . Conversation ;
2023-02-22 01:58:15 +00:00
import funkin . play . cutscene . VanillaCutscenes ;
2023-06-02 19:35:01 +00:00
import funkin . play . cutscene . VideoCutscene ;
2023-06-22 05:41:01 +00:00
import funkin . play . notes . NoteDirection ;
2024-09-19 14:03:16 +00:00
import funkin . play . notes . notekind . NoteKindManager ;
2025-08-01 06:05:11 +00:00
import funkin . play . notes . notekind . NoteKind ;
2023-09-17 11:45:10 +00:00
import funkin . play . notes . NoteSprite ;
2024-02-12 21:50:29 +00:00
import funkin . play . notes . notestyle . NoteStyle ;
2023-06-22 05:41:01 +00:00
import funkin . play . notes . Strumline ;
2023-06-27 22:06:33 +00:00
import funkin . play . notes . SustainTrail ;
2024-12-14 01:28:39 +00:00
import funkin . play . notes . NoteVibrationsHandler ;
2022-07-07 01:49:42 +00:00
import funkin . play . scoring . Scoring ;
2022-09-22 10:34:03 +00:00
import funkin . play . song . Song ;
2022-03-08 08:13:53 +00:00
import funkin . play . stage . Stage ;
2024-02-12 21:50:29 +00:00
import funkin . save . Save ;
import funkin . ui . debug . charting . ChartEditorState ;
2023-11-07 09:04:22 +00:00
import funkin . ui . debug . stage . StageOffsetSubState ;
2024-02-12 21:50:29 +00:00
import funkin . ui . mainmenu . MainMenuState ;
import funkin . ui . MusicBeatSubState ;
import funkin . ui . transition . LoadingState ;
2023-03-18 19:47:23 +00:00
import funkin . util . SerializerUtil ;
2025-04-15 21:22:56 +00:00
import funkin . util . HapticUtil ;
2025-06-18 03:23:09 +00:00
import funkin . util . GRhythmUtil ;
2024-02-12 21:50:29 +00:00
import haxe . Int64 ;
2025-04-14 15:20:15 +00:00
#if mobile
2024-12-15 14:53:02 +00:00
import funkin . util . TouchUtil ;
2025-04-14 15:20:15 +00:00
import funkin . mobile . ui . FunkinHitbox ;
2025-06-29 00:17:45 +00:00
import funkin . mobile . input . ControlsHandler ;
2025-04-15 19:54:00 +00:00
import funkin . mobile . ui . FunkinHitbox . FunkinHitboxControlSchemes ;
2025-04-30 04:31:31 +00:00
#if FEATURE_MOBILE_ADVERTISEMENTS
2025-05-01 23:55:44 +00:00
import funkin . mobile . util . AdMobUtil ;
2025-04-14 16:39:43 +00:00
#end
2025-04-14 15:20:15 +00:00
#end
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2024-09-19 14:03:16 +00:00
import funkin . api . discord . DiscordClient ;
2021-03-21 20:29:47 +00:00
#end
2024-10-07 03:51:50 +00:00
#if FEATURE_NEWGROUNDS
import funkin . api . newgrounds . Medals ;
import funkin . api . newgrounds . Leaderboards ;
#end
2021-03-21 20:29:47 +00:00
2023-06-02 19:35:01 +00:00
/ * *
* 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 ,
/ * *
2024-02-06 02:35:58 +00:00
* The variation to play on .
2024-02-29 23:49:20 +00:00
* @ default ` Constants . DEFAULT_VARIATION `
2023-06-02 19:35:01 +00:00
* /
2024-02-06 02:35:58 +00:00
? targetVariation : String ,
2024-02-05 18:35:30 +00:00
/ * *
* The instrumental to play with .
* Significant if t h e ` t a r g e t S o n g ` s u p p o r t s a l t e r n a t e i n s t r u m e n t a l s .
* @ default ` null `
* /
? targetInstrumental : String ,
2023-07-26 20:52:58 +00:00
/ * *
* Whether the song should start in Practice Mode .
* @ default ` false `
* /
? practiceMode : Bool ,
2024-03-06 03:27:07 +00:00
/ * *
* Whether the song should start in Bot Play Mode .
* @ default ` false `
* /
? botPlayMode : Bool ,
2023-07-26 20:52:58 +00:00
/ * *
* Whether the song should be in minimal mode .
* @ default ` false `
* /
? minimalMode : Bool ,
2023-07-27 00:03:31 +00:00
/ * *
* If specified , the game will jump to the specified timestamp after the countdown ends .
2024-02-29 23:49:20 +00:00
* @ default ` 0.0 `
2023-07-27 00:03:31 +00:00
* /
? startTimestamp : Float ,
2024-02-29 23:49:20 +00:00
/ * *
* If specified , the game will play the song with the given speed .
* @ default ` 1.0 ` for 1 0 0 % s p e e d .
* /
? playbackRate : Float ,
2023-08-04 20:15:07 +00:00
/ * *
* If specified , the game will not load the instrumental or vocal tracks ,
* and must be loaded externally .
* /
? overrideMusic : Bool ,
2024-02-17 04:48:43 +00:00
/ * *
* The initial camera follow point .
* Used to persist the position of the ` cameraFollowPosition ` between levels .
* /
? cameraFollowPoint : FlxPoint ,
2023-06-02 19:35:01 +00:00
}
2023-02-22 01:58:15 +00:00
/ * *
* The gameplay state , where all the rhythm gaming happens .
2023-07-26 20:52:58 +00:00
* SubState so it can be loaded as a child of the chart editor .
2023-02-22 01:58:15 +00:00
* /
2025-04-23 03:31:58 +00:00
@ : nullSafety
2023-07-26 20:52:58 +00:00
class PlayState extends MusicBeatSubState
2020-10-03 06:50:15 +00:00
{
2023-01-23 00:55:30 +00:00
/ * *
* STATIC VARIABLES
* Static variables should be used for i n f o r m a t i o n t h a t m u s t b e p e r s i s t e d b e t w e e n s t a t e s o r b e t w e e n r e s e t s ,
* such as the active song or song playlist .
* /
/ * *
* The currently active PlayState .
2023-06-02 19:35:01 +00:00
* There should be only one PlayState in existance at a time , we can use a singleton .
2023-01-23 00:55:30 +00:00
* /
2025-04-23 03:31:58 +00:00
public static var instance: Null < PlayState > ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
/ * *
* This sucks . We need this because FlxG . resetState ( ) ; assumes the constructor has no arguments .
* @ see https : //github.com/HaxeFlixel/flixel/issues/2541
* /
2025-04-23 03:31:58 +00:00
static var lastParams: Null < PlayStateParams > ;
2023-06-22 05:41:01 +00:00
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* PUBLIC INSTANCE VARIABLES
* Public instance variables should be used for i n f o r m a t i o n t h a t m u s t b e r e s e t o r d e r e f e r e n c e d
* every time the state is changed , but may need to be accessed externally .
2023-01-23 00:55:30 +00:00
* /
/ * *
2023-06-02 19:35:01 +00:00
* The currently selected stage .
2023-01-23 00:55:30 +00:00
* /
2025-04-23 03:31:58 +00:00
public var currentSong: Song ;
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* The currently selected difficulty .
2023-01-23 00:55:30 +00:00
* /
2023-06-02 19:35:01 +00:00
public var currentDifficulty: String = Constants . DEFAULT_DIFFICULTY ;
2023-01-23 00:55:30 +00:00
/ * *
2024-02-06 02:35:58 +00:00
* The currently selected variation .
2023-01-23 00:55:30 +00:00
* /
2024-02-06 02:35:58 +00:00
public var currentVariation: String = Constants . DEFAULT_VARIATION ;
2023-01-23 00:55:30 +00:00
2024-06-07 01:38:00 +00:00
/ * *
* The currently selected instrumental ID .
* @ default ` ' ' `
* /
public var currentInstrumental: String = ' ' ;
2023-01-27 07:38:37 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* The currently active Stage . This is the object containing all the props .
2023-01-27 07:38:37 +00:00
* /
2025-04-23 03:31:58 +00:00
public var currentStage: Null < Stage > = null ;
2023-01-27 07:38:37 +00:00
2023-01-23 00:55:30 +00:00
/ * *
* Gets set to true when the PlayState needs to reset ( player o p t e d t o r e s t a r t o r d i e d ) .
* Gets disabled once resetting happens .
* /
2023-06-02 19:35:01 +00:00
public var needsReset: Bool = false ;
2023-01-23 00:55:30 +00:00
2025-04-26 15:54:37 +00:00
/ * *
* A timer that gets active once resetting happens . Used to vwoosh in notes .
* /
public var vwooshTimer: FlxTimer = new FlxTimer ( ) ;
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* The current ' B l u e b a l l C o u n t e r ' to display in the pause menu .
2023-01-23 00:55:30 +00:00
* Resets when you beat a song or go back to the main menu .
* /
2023-06-02 19:35:01 +00:00
public var deathCounter: Int = 0 ;
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* The player ' s c u r r e n t h e a l t h .
2023-01-23 00:55:30 +00:00
* /
2023-06-27 17:43:42 +00:00
public var health: Float = Constants . HEALTH_STARTING ;
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* The player ' s c u r r e n t s c o r e .
2023-06-22 05:41:01 +00:00
* TODO : Move this to its own class .
2023-01-23 00:55:30 +00:00
* /
2023-06-02 19:35:01 +00:00
public var songScore: Int = 0 ;
2023-01-23 00:55:30 +00:00
2023-12-05 07:44:57 +00:00
/ * *
* Start at this point in the song once the countdown is done .
* For example , if ` s t a r t T i m e s t a m p ` i s ` 3 0 0 0 0 ` , t h e s o n g w i l l s t a r t a t t h e 3 0 s e c o n d m a r k .
* Used for c h a r t p l a y t e s t i n g o r p r a c t i c e .
* /
public var startTimestamp: Float = 0.0 ;
2024-02-29 23:49:20 +00:00
/ * *
* Play back the song at this speed .
* @ default ` 1.0 ` for n o r m a l s p e e d .
* /
public var playbackRate: Float = 1.0 ;
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* An empty FlxObject contained in the scene .
2023-06-22 05:41:01 +00:00
* The current gameplay camera will always follow this object . Tween its position to move the camera smoothly .
2023-06-09 19:34:56 +00:00
*
2023-06-22 05:41:01 +00:00
* It needs to be an object in the scene for t h e c a m e r a t o b e c o n f i g u r e d t o f o l l o w i t .
2024-02-22 23:55:24 +00:00
* We optionally make this a sprite so we can draw a debug graphic with it .
2023-01-23 00:55:30 +00:00
* /
2023-06-22 05:41:01 +00:00
public var cameraFollowPoint: FlxObject ;
2023-06-02 19:35:01 +00:00
2024-02-28 06:29:40 +00:00
/ * *
* An FlxTween that tweens the camera to the follow point .
* Only used when tweening the camera manually , rather than tweening via follow .
* /
2025-04-23 03:31:58 +00:00
public var cameraFollowTween: Null < FlxTween > ;
2024-02-28 06:29:40 +00:00
2024-03-10 23:35:41 +00:00
/ * *
* An FlxTween that zooms the camera to the desired amount .
* /
2025-04-23 03:31:58 +00:00
public var cameraZoomTween: Null < FlxTween > ;
2024-03-10 23:35:41 +00:00
2024-05-09 16:51:03 +00:00
/ * *
* An FlxTween that changes the additive speed to the desired amount .
* /
2024-05-10 20:23:35 +00:00
public var scrollSpeedTweens: Array < FlxTween > = [ ] ;
2024-05-09 16:51:03 +00:00
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* The camera follow point from the last stage .
* Used to persist the position of the ` cameraFollowPosition ` between levels .
2023-01-23 00:55:30 +00:00
* /
2025-04-23 03:31:58 +00:00
public var previousCameraFollowPoint: Null < FlxPoint > ;
2023-01-23 00:55:30 +00:00
2023-06-02 19:35:01 +00:00
/ * *
2024-03-16 15:38:10 +00:00
* The current camera zoom level without any modifiers applied .
* /
2024-03-28 18:05:38 +00:00
public var currentCameraZoom: Float = FlxCamera . defaultZoom ;
2024-03-16 15:38:10 +00:00
/ * *
2024-03-28 18:05:38 +00:00
* Multiplier for c u r r e n t C a m e r a Z o o m f o r c a m e r a b o p s .
* Lerped back to 1.0 x every frame .
2023-06-02 19:35:01 +00:00
* /
2024-03-28 18:05:38 +00:00
public var cameraBopMultiplier: Float = 1.0 ;
2023-01-23 00:55:30 +00:00
2024-03-16 15:38:10 +00:00
/ * *
2024-03-28 18:05:38 +00:00
* Default camera zoom for t h e c u r r e n t s t a g e .
* If we aren ' t i n a s t a g e , j u s t u s e t h e d e f a u l t z o o m ( 1 . 0 5 x ) .
2024-03-16 15:38:10 +00:00
* /
2024-03-28 18:05:38 +00:00
public var stageZoom( get , never ) : Float ;
function get_stageZoom ( ) : Float
{
if ( currentStage != null ) return currentStage . camZoom ;
e lse
return FlxCamera . defaultZoom * 1.05 ;
}
2024-03-16 15:38:10 +00:00
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* The current HUD camera zoom level .
2023-06-09 19:34:56 +00:00
*
2023-06-02 19:35:01 +00:00
* The camera zoom is increased every beat , and lerped back to this value every frame , creating a smooth ' z o o m - i n ' effect .
2023-01-23 00:55:30 +00:00
* /
2023-06-02 19:35:01 +00:00
public var defaultHUDCameraZoom: Float = FlxCamera . defaultZoom * 1.0 ;
2023-01-23 00:55:30 +00:00
/ * *
2024-03-28 18:05:38 +00:00
* Camera bop intensity multiplier .
* Applied to cameraBopMultiplier on camera bops ( usually e v e r y b e a t ) .
* @ default ` 101.5 % `
2023-01-23 00:55:30 +00:00
* /
2024-03-28 18:05:38 +00:00
public var cameraBopIntensity: Float = Constants . DEFAULT_BOP_INTENSITY ;
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* Intensity of the HUD camera zoom .
2024-03-28 18:05:38 +00:00
* Need to make this a multiplier later . Just shoving in 0.015 for n o w s o i t d o e s n ' t b r e a k .
2023-06-02 19:35:01 +00:00
* @ default ` 3.0 % `
2023-01-23 00:55:30 +00:00
* /
2024-03-28 18:05:38 +00:00
public var hudCameraZoomIntensity: Float = 0.015 * 2.0 ;
2023-01-23 00:55:30 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* How many beats ( quarter n o t e s ) between camera zooms .
* @ default One camera zoom per measure ( four b e a t s ) .
2023-01-23 00:55:30 +00:00
* /
2023-06-02 19:35:01 +00:00
public var cameraZoomRate: Int = Constants . DEFAULT_ZOOM_RATE ;
2024-10-04 17:51:19 +00:00
/ * *
* How many beats ( quarter n o t e s ) the zoom rate is offset .
* For if y o u w a n t t h e z o o m t o h a p p e n o f f - b e a t .
* @ default Zero beats ( on - beat ) .
* /
public var cameraZoomRateOffset: Int = Constants . DEFAULT_ZOOM_OFFSET ;
2023-06-02 19:35:01 +00:00
/ * *
* 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 .
2025-06-06 04:11:55 +00:00
* If true , player will not gain or lose score from notes .
2023-06-02 19:35:01 +00:00
* /
public var isPracticeMode: Bool = false ;
2024-03-06 03:27:07 +00:00
/ * *
* Whether the game is currently in Bot Play Mode .
2025-06-06 04:11:55 +00:00
* If true , player will not gain or lose score from notes .
2024-03-06 03:27:07 +00:00
* /
public var isBotPlayMode: Bool = false ;
2024-03-04 21:37:42 +00:00
/ * *
* Whether the player has dropped below zero health ,
* and we are just waiting for a n a n i m a t i o n t o p l a y o u t b e f o r e t r a n s i t i o n i n g .
* /
public var isPlayerDying: Bool = false ;
2023-07-26 20:52:58 +00:00
/ * *
* In Minimal Mode , the stage and characters are not loaded and a standard background is used .
* /
public var isMinimalMode: Bool = false ;
2023-06-02 19:35:01 +00:00
/ * *
* Whether the game is currently in an animated cutscene , and gameplay should be stopped .
* /
public var isInCutscene: Bool = false ;
/ * *
2025-03-19 07:12:55 +00:00
* Whether the inputs should be disabled for w h a t e v e r r e a s o n . . .
* Used after the song ends , and in the Stage Editor .
2023-06-02 19:35:01 +00:00
* /
public var disableKeys: Bool = false ;
2023-01-23 00:55:30 +00:00
2025-01-17 20:37:00 +00:00
/ * *
* The previous difficulty the player was playing on .
* /
public var previousDifficulty: String = Constants . DEFAULT_DIFFICULTY ;
2023-08-31 22:47:23 +00:00
public var isSubState( get , never ) : Bool ;
2023-07-26 20:52:58 +00:00
function get_isSubState ( ) : Bool
{
return this . _parentState != null ;
}
2023-08-31 22:47:23 +00:00
public var isChartingMode( get , never ) : Bool ;
2023-07-26 20:52:58 +00:00
function get_isChartingMode ( ) : Bool
{
return this . _parentState != null && Std . isOfType ( this . _parentState , ChartEditorState ) ;
}
2023-06-16 21:37:56 +00:00
/ * *
* The current dialogue .
* /
2025-04-23 03:31:58 +00:00
public var currentConversation: Null < Conversation > ;
2023-06-16 21:37:56 +00:00
2023-06-22 05:41:01 +00:00
/ * *
* Key press inputs which have been received but not yet processed .
2025-03-19 07:12:55 +00:00
* These are encoded with an OS timestamp , so we can account for i n p u t l a t e n c y .
2023-06-22 05:41:01 +00:00
* * /
var inputPressQueue: Array < PreciseInputEvent > = [ ] ;
/ * *
* Key release inputs which have been received but not yet processed .
2025-03-19 07:12:55 +00:00
* These are encoded with an OS timestamp , so we can account for i n p u t l a t e n c y .
2023-06-22 05:41:01 +00:00
* * /
var inputReleaseQueue: Array < PreciseInputEvent > = [ ] ;
2024-02-28 05:19:08 +00:00
/ * *
* If we just unpaused the game , we shouldn ' t b e a b l e t o p a u s e a g a i n f o r o n e f r a m e .
* /
var justUnpaused: Bool = false ;
2025-04-23 03:31:58 +00:00
/ * *
* The current note style used by the song .
* /
var noteStyle: NoteStyle ;
2023-01-23 00:55:30 +00:00
/ * *
* PRIVATE INSTANCE VARIABLES
* Private instance variables should be used for i n f o r m a t i o n t h a t m u s t b e r e s e t o r d e r e f e r e n c e d
* every time the state is reset , but should not be accessed externally .
* /
2023-06-02 19:35:01 +00:00
/ * *
* The Array containing the upcoming song events .
* The ` update ( ) ` function regularly s h i f t s t h e s e o u t t o t r i g g e r e v e n t s .
* /
2025-04-23 03:31:58 +00:00
var songEvents: Array < SongEventData > = [ ] ;
2023-01-23 00:55:30 +00:00
/ * *
* If true , the player is allowed to pause the game .
* Disabled during the ending of a song .
* /
2023-01-23 03:25:45 +00:00
var mayPauseGame: Bool = true ;
2023-01-23 00:55:30 +00:00
/ * *
* The displayed value of the player ' s h e a l t h .
* Used to provide smooth animations based on linear interpolation of the player ' s h e a l t h .
* /
2023-06-27 17:43:42 +00:00
var healthLerp: Float = Constants . HEALTH_STARTING ;
2023-01-23 00:55:30 +00:00
2023-06-02 19:35:01 +00:00
/ * *
* How long the user has held the " S k i p V i d e o C u t s c e n e " button for .
* /
var skipHeldTimer: Float = 0 ;
2023-12-05 07:44:57 +00:00
/ * *
* 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-23 00:55:30 +00:00
/ * *
* Forcibly disables all update logic while t h e g a m e m o v e s b a c k t o t h e M e n u s t a t e .
2023-06-02 19:35:01 +00:00
* This is used only when a critical error occurs and the game absolutely cannot continue .
2023-01-23 00:55:30 +00:00
* /
2023-01-23 03:25:45 +00:00
var criticalFailure: Bool = false ;
2023-01-23 00:55:30 +00:00
2023-02-22 01:58:15 +00:00
/ * *
2023-06-02 19:35:01 +00:00
* False as long as the countdown has not finished yet .
* /
var startingSong: Bool = false ;
2024-02-16 12:54:27 +00:00
/ * *
2024-03-10 23:35:41 +00:00
* Track if w e c u r r e n t l y h a v e t h e m u s i c p a u s e d f o r a P a u s e s u b s t a t e , s o w e c a n u n p a u s e i t w h e n w e r e t u r n .
2024-02-16 12:54:27 +00:00
* /
var musicPausedBySubState: Bool = false ;
2024-03-15 08:52:22 +00:00
/ * *
* Track any camera tweens we ' v e p a u s e d f o r a P a u s e s u b s t a t e , s o w e c a n u n p a u s e t h e m w h e n w e r e t u r n .
* /
var cameraTweensPausedBySubState: List < FlxTween > = new List < FlxTween > ( ) ;
2024-03-10 23:35:41 +00:00
2025-04-14 00:55:46 +00:00
/ * *
* Track any sounds we ' v e p a u s e d f o r a P a u s e s u b s t a t e , s o w e c a n u n p a u s e t h e m w h e n w e r e t u r n .
* /
var soundsPausedBySubState: List < FlxSound > = new List < FlxSound > ( ) ;
2023-08-09 04:28:09 +00:00
/ * *
* False until ` create ( ) ` has completed .
* /
var initialized: Bool = false ;
2023-06-02 19:35:01 +00:00
/ * *
* A group of audio tracks , used to play the song ' s v o c a l s .
2023-02-22 01:58:15 +00:00
* /
2025-04-23 03:31:58 +00:00
public var vocals: Null < VoicesGroup > ;
2023-02-22 01:58:15 +00:00
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2023-06-22 05:41:01 +00:00
// Discord RPC variables
2024-09-25 07:58:43 +00:00
var discordRPCAlbum: String = ' ' ;
2024-09-19 14:03:16 +00:00
var discordRPCIcon: String = ' ' ;
2023-06-22 05:41:01 +00:00
#end
2023-01-23 00:55:30 +00:00
/ * *
* RENDER OBJECTS
* /
/ * *
* The FlxText which displays the current score .
* /
2023-01-23 03:25:45 +00:00
var scoreText: FlxText ;
2023-01-23 00:55:30 +00:00
/ * *
* The bar which displays the player ' s h e a l t h .
* Dynamically updated based on the value of ` healthLerp ` ( which i s b a s e d o n ` h e a l t h ` ) .
* /
public var healthBar: FlxBar ;
/ * *
* The background image used for t h e h e a l t h b a r .
* Emma says the image is slightly skewed so I ' m l e a v i n g i t a s a n i m a g e i n s t e a d o f a ` c r e a t e G r a p h i c ` .
* /
2024-02-22 23:55:24 +00:00
public var healthBarBG: FunkinSprite ;
2023-01-23 00:55:30 +00:00
/ * *
* The health icon representing the player .
* /
2025-04-23 03:31:58 +00:00
public var iconP1: Null < HealthIcon > ;
2023-01-23 00:55:30 +00:00
/ * *
* The health icon representing the opponent .
* /
2025-04-23 03:31:58 +00:00
public var iconP2: Null < HealthIcon > ;
2023-01-23 00:55:30 +00:00
/ * *
* The sprite group containing active player ' s s t r u m l i n e n o t e s .
* /
public var playerStrumline: Strumline ;
/ * *
* The sprite group containing opponent ' s s t r u m l i n e n o t e s .
* /
2023-06-22 05:41:01 +00:00
public var opponentStrumline: Strumline ;
2023-01-23 00:55:30 +00:00
/ * *
* The camera which contains , and controls visibility of , the user interface e l e m e n t s .
* /
public var camHUD: FlxCamera ;
/ * *
* The camera which contains , and controls visibility of , the stage and characters .
* /
2024-02-13 06:38:11 +00:00
public var camGame: FlxCamera ;
2023-01-23 00:55:30 +00:00
2023-02-22 01:58:15 +00:00
/ * *
2024-09-05 05:22:45 +00:00
* Simple helper debug variable , to be able to move the camera around for d e b u g p u r p o s e s
* without worrying about the camera tweening back to the follow point .
* /
public var debugUnbindCameraZoom: Bool = false ;
2023-02-22 01:58:15 +00:00
/ * *
2024-05-20 00:56:57 +00:00
* The camera which contains , and controls visibility of , a video cutscene , dialogue , pause menu and sticker transition .
2023-02-22 01:58:15 +00:00
* /
public var camCutscene: FlxCamera ;
2024-12-23 15:03:01 +00:00
/ * *
* The camera which contains , and controls visibility of menus when there are fake cutouts added .
* /
public var camCutouts: FlxCamera ;
2023-06-22 05:41:01 +00:00
/ * *
* The combo popups . Includes the real - time combo counter and the rating .
* /
2024-04-09 02:56:28 +00:00
public var comboPopUps: PopUpStuff ;
2023-06-22 05:41:01 +00:00
2025-07-16 17:27:37 +00:00
public var isSongEnd: Bool = false ;
2025-04-14 15:20:15 +00:00
#if mobile
/ * *
* The pause button for t h e g a m e , o n l y a p p e a r s i n M o b i l e t a r g e t s .
* /
var pauseButton: FunkinSprite ;
2025-06-08 03:09:40 +00:00
/ * *
* The pause circle for t h e g a m e , o n l y a p p e a r s i n M o b i l e t a r g e t s .
* /
var pauseCircle: FunkinSprite ;
2025-04-14 15:20:15 +00:00
#end
2023-01-23 00:55:30 +00: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 .
* /
2023-01-23 03:25:45 +00:00
var isGamePaused( get , never ) : Bool ;
2023-01-23 00:55:30 +00: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 06:38:11 +00: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 05:19:08 +00:00
return ! pauseSubState . allowInput ;
2024-02-13 06:38:11 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
* Data for t h e c u r r e n t d i f f i c u l t y f o r t h e c u r r e n t s o n g .
* Includes chart data , scroll speed , and other information .
* /
2025-04-23 03:31:58 +00:00
public var currentChart( get , never ) : Null < SongDifficulty > ;
2023-01-23 00:55:30 +00:00
2025-04-23 03:31:58 +00:00
function get_currentChart ( ) : Null < SongDifficulty >
2023-06-22 05:41:01 +00:00
{
if ( currentSong == null || currentDifficulty == null ) return null ;
2024-02-06 02:35:58 +00:00
return currentSong . getDifficulty ( currentDifficulty , currentVariation ) ;
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
/ * *
* The internal ID of the currently active Stage .
* Used to retrieve the data required to build the ` currentStage ` .
* /
2023-08-31 22:47:23 +00:00
public var currentStageId( get , never ) : String ;
2023-06-22 05:41:01 +00:00
function get_currentStageId ( ) : String
{
2025-04-23 03:31:58 +00:00
var stage: String = currentChart ? . stage ? ? ' ' ;
return stage == ' ' ? Constants . DEFAULT_STAGE : stage ;
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2023-06-02 19:35:01 +00:00
/ * *
2023-06-22 05:41:01 +00:00
* The length of the current song , i n milliseconds .
2023-06-02 19:35:01 +00:00
* /
2023-06-22 05:41:01 +00:00
var currentSongLengthMs( get , never ) : Float ;
function get_currentSongLengthMs ( ) : Float
{
2025-04-23 03:31:58 +00:00
return FlxG . sound . music ? . length ? ? 0 ;
2023-06-22 05:41:01 +00:00
}
2025-06-28 07:50:59 +00:00
/ * *
* The threshold for r e s y n c i n g t h e s o n g .
* If the vocals deviate from the instrumental by more than this amount , then ` resyncVocals ( ) ` will be called .
* /
2025-07-09 11:09:14 +00:00
static final RESYNC_THRESHOLD : Float = 40 ;
2025-06-28 07:50:59 +00:00
2025-08-16 11:28:34 +00:00
/ * *
* The threshold for h o w m u c h t h e c o n d u c t o r l e r p c a n d r i f t f r o m t h e m u s i c .
* If the conductor song position deviate from the music by more than this amount , then a normal conductor update is triggered .
* /
static final CONDUCTOR_DRIFT_THRESHOLD : Float = 65 ;
2025-08-01 20:37:51 +00:00
/ * *
* The ratio for e a s i n g t h e s o n g p o s i t o n f o r s m o o t h e r n o t e s s c r o l l i n g .
* /
2025-08-16 11:28:34 +00:00
static final MUSIC_EASE_RATIO : Float = 42 ;
2025-08-01 20:37:51 +00:00
2023-06-22 05:41:01 +00:00
// TODO: Refactor or document
var generatedMusic: Bool = false ;
2023-06-02 19:35:01 +00:00
2024-07-28 05:42:09 +00:00
var skipEndingTransition: Bool = false ;
2023-06-02 19:35:01 +00:00
2024-04-30 11:21:45 +00:00
static final BACKGROUND_COLOR : FlxColor = FlxColor . BLACK ;
2023-11-29 01:36:59 +00:00
2023-06-22 05:41:01 +00:00
/ * *
* Instantiate a new PlayState .
* @ param params The parameters used to initialize the PlayState .
* Includes information about what song to play and more .
* /
2025-04-23 03:31:58 +00:00
public function n e w ( ? params : PlayStateParams )
2023-01-23 00:55:30 +00:00
{
2023-06-02 19:35:01 +00:00
super ( ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Validate parameters.
2025-04-23 03:31:58 +00:00
var params: PlayStateParams = params ? ?
{
trace ( ' W A R N I N G : P l a y S t a t e c o n s t r u c t o r c a l l e d w i t h n o p a r a m e t e r s . R e u s i n g p r e v i o u s p a r a m e t e r s . ' ) ;
lastParams ? ? throw ' P l a y S t a t e c o n s t r u c t o r c a l l e d w i t h n o a v a i l a b l e p a r a m e t e r s . ' ;
}
lastParams = params ;
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
// Apply parameters.
2025-04-23 03:31:58 +00:00
currentSong = params . targetSong ? ? throw " t a r g e t S o n g s h o u l d n o t b e n u l l " ;
2023-06-02 19:35:01 +00:00
if ( params . targetDifficulty != null ) currentDifficulty = params . targetDifficulty ;
2025-01-17 20:37:00 +00:00
previousDifficulty = currentDifficulty ;
2024-02-06 02:35:58 +00:00
if ( params . targetVariation != null ) currentVariation = params . targetVariation ;
2024-06-07 01:38:00 +00:00
if ( params . targetInstrumental != null ) currentInstrumental = params . targetInstrumental ;
2023-07-26 20:52:58 +00:00
isPracticeMode = params . practiceMode ? ? false ;
2024-03-06 03:27:07 +00:00
isBotPlayMode = params . botPlayMode ? ? false ;
2023-07-26 20:52:58 +00:00
isMinimalMode = params . minimalMode ? ? false ;
2023-07-27 00:03:31 +00:00
startTimestamp = params . startTimestamp ? ? 0.0 ;
2024-02-29 23:49:20 +00:00
playbackRate = params . playbackRate ? ? 1.0 ;
2023-08-04 20:15:07 +00:00
overrideMusic = params . overrideMusic ? ? false ;
2024-02-17 04:48:43 +00:00
previousCameraFollowPoint = params . cameraFollowPoint ;
2023-06-22 05:41:01 +00:00
2025-04-23 03:31:58 +00:00
// Basic object initialization
// TODO: Add something to toggle this on!
if ( false )
{
// Displays the camera follow point as a sprite for debug purposes.
var cameraFollowPoint = new FunkinSprite ( 0 , 0 ) ;
cameraFollowPoint . makeSolidColor ( 8 , 8 , 0xFF00FF00 ) ;
cameraFollowPoint . visible = false ;
cameraFollowPoint . zIndex = 1000000 ;
this . cameraFollowPoint = cameraFollowPoint ;
}
e lse
{
// Camera follow point is an invisible point in space.
cameraFollowPoint = new FlxObject ( 0 , 0 ) ;
}
// Cameras
camGame = new FunkinCamera ( ' p l a y S t a t e C a m G a m e ' ) ;
camHUD = new FlxCamera ( ) ;
camCutscene = new FlxCamera ( ) ;
camCutouts = new FlxCamera ( ) ;
var currentChart = currentSong . getDifficulty ( currentDifficulty , currentVariation ) ;
var noteStyleId: Null < String > = currentChart ? . noteStyle ;
var nulNoteStyle: Null < NoteStyle > = NoteStyleRegistry . instance . fetchEntry ( noteStyleId ? ? Constants . DEFAULT_NOTE_STYLE ) ;
if ( nulNoteStyle == null ) throw " F a i l e d t o r e t r i e v e b o t h n o t e s t y l e a n d d e f a u l t n o t e s t y l e . T h i s s h o u l d n ' t h a p p e n ! " ;
noteStyle = nulNoteStyle ;
// Strumlines
playerStrumline = new Strumline ( noteStyle , ! isBotPlayMode , currentChart ? . scrollSpeed ) ;
opponentStrumline = new Strumline ( noteStyle , false , currentChart ? . scrollSpeed ) ;
// Healthbar
healthBarBG = FunkinSprite . create ( 0 , 0 , ' h e a l t h B a r ' ) ;
healthBar = new FlxBar ( 0 , 0 , RIGHT_TO_LEFT , Std . int ( healthBarBG . width - 8 ) , Std . int ( healthBarBG . height - 8 ) , null , 0 , 2 ) ;
scoreText = new FlxText ( 0 , 0 , 0 , ' ' , 20 ) ;
// Combo & Pop Up
comboPopUps = new PopUpStuff ( noteStyle ) ;
// Pause sprites
#if mobile
pauseButton = FunkinSprite . createSparrow ( 0 , 0 , " p a u s e B u t t o n " ) ;
pauseCircle = FunkinSprite . create ( 0 , 0 , ' p a u s e C i r c l e ' ) ;
#end
2023-06-22 05:41:01 +00:00
// Don't do anything else here! Wait until create() when we attach to the camera.
2023-06-02 19:35:01 +00:00
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
/ * *
* Called when the PlayState is switched to .
* /
2025-04-23 03:31:58 +00:00
@ : nullSafety ( Off )
2023-06-02 19:35:01 +00:00
public override function create ( ) : Void
{
if ( instance != null )
{
2023-06-22 05:41:01 +00:00
// TODO: Do something in this case? IDK.
2023-06-02 19:35:01 +00:00
trace ( ' W A R N I N G : P l a y S t a t e i n s t a n c e a l r e a d y e x i s t s . T h i s s h o u l d n o t h a p p e n . ' ) ;
}
2023-01-23 00:55:30 +00:00
instance = this ;
2025-06-22 20:08:36 +00:00
#if ! mobile
// TODO: Figure out how to do the flair for charting mode!! I can't figure it out for the love of god. -Zack
if ( ! isChartingMode ) FlxG . autoPause = false ;
#end
2023-01-23 00:55:30 +00:00
2023-10-09 18:13:14 +00:00
if ( ! assertChartExists ( ) ) return ;
2023-01-23 00:55:30 +00:00
2025-04-14 15:20:15 +00:00
#if mobile
// Force allowScreenTimeout to be disabled
lime . system . System . allowScreenTimeout = false ;
#end
2023-01-23 00:55:30 +00:00
// 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.
2025-04-23 03:31:58 +00:00
if ( ! overrideMusic )
2023-01-23 00:55:30 +00:00
{
2025-04-23 03:31:58 +00:00
if ( FlxG . sound . music != null ) FlxG . sound . music . stop ( ) ;
// Prepare the current song's instrumental and vocals to be played.
2024-07-23 00:52:03 +00:00
currentChart . cacheInst ( currentInstrumental ) ;
2023-09-26 03:24:07 +00:00
currentChart . cacheVocals ( ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
// Prepare the Conductor.
2023-12-14 21:56:20 +00:00
Conductor . instance . forceBPM ( null ) ;
2023-12-08 06:57:46 +00:00
if ( currentChart . offsets != null )
{
2024-07-23 00:52:03 +00:00
Conductor . instance . instrumentalOffset = currentChart . offsets . getInstrumentalOffset ( currentInstrumental ) ;
2023-12-08 06:57:46 +00:00
}
2023-12-14 21:56:20 +00:00
Conductor . instance . mapTimeChanges ( currentChart . timeChanges ) ;
2025-05-02 05:24:18 +00:00
var pre: Float = ( Conductor . instance . beatLengthMs * - 5 ) + startTimestamp ;
2024-09-19 08:20:16 +00:00
trace ( ' A t t e m p t i n g t o s t a r t a t ' + pre ) ;
Conductor . instance . update ( pre ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// The song is now loaded. We can continue to initialize the play state.
initCameras ( ) ;
initHealthBar ( ) ;
2023-07-26 20:52:58 +00:00
if ( ! isMinimalMode )
{
initStage ( ) ;
initCharacters ( ) ;
}
e lse
{
initMinimalMode ( ) ;
}
2023-06-22 05:41:01 +00:00
initStrumlines ( ) ;
2024-07-28 21:10:32 +00:00
initPopups ( ) ;
2023-01-23 00:55:30 +00:00
2025-04-14 15:20:15 +00:00
#if mobile
2025-06-29 00:17:45 +00:00
if ( ! ControlsHandler . usingExternalInputDevice )
2025-04-15 19:54:00 +00:00
{
2025-06-29 00:17:45 +00:00
// Initialize the hitbox for mobile controls
addHitbox ( false ) ;
2025-04-23 03:31:58 +00:00
if ( hitbox != null )
2025-04-15 19:54:00 +00:00
{
2025-04-23 03:31:58 +00:00
hitbox . isPixel = currentChart . noteStyle == " p i x e l " ;
if ( Preferences . controlsScheme == FunkinHitboxControlSchemes . Arrows )
2025-06-29 00:17:45 +00:00
{
2025-04-23 03:31:58 +00:00
for ( direction in Strumline . DIRECTIONS )
{
hitbox . getFirstHintByDirection ( direction ) . follow ( playerStrumline . getByDirection ( direction ) ) ;
}
2025-06-29 00:17:45 +00:00
}
2025-04-15 19:54:00 +00:00
}
}
2025-06-29 00:17:45 +00:00
e lse
{
// The camera is still needed for the pause button!
camControls = new FunkinCamera ( ' c a m C o n t r o l s ' ) ;
FlxG . cameras . add ( camControls , false ) ;
camControls . bgColor = 0x0 ;
}
2025-04-14 15:20:15 +00:00
#end
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2023-06-22 05:41:01 +00:00
// Initialize Discord Rich Presence.
initDiscord ( ) ;
#end
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Read the song's note data and pass it to the strumlines.
2023-06-02 19:35:01 +00:00
generateSong ( ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Reset the camera's zoom and force it to focus on the camera follow point.
2023-01-23 00:55:30 +00:00
resetCamera ( ) ;
2023-06-22 05:41:01 +00:00
initPreciseInputs ( ) ;
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
FlxG . worldBounds . set ( 0 , 0 , FlxG . width , FlxG . height ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// The song is loaded and in the process of starting.
// This gets set back to false when the chart actually starts.
2023-01-23 00:55:30 +00:00
startingSong = true ;
2023-06-22 05:41:01 +00:00
// TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead.
2025-04-23 03:31:58 +00:00
if ( ( currentSong . id ? ? ' ' ) . t o L o w e r C a s e ( ) = = ' w i n t e r - h o r r o r l a n d ' )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
// VanillaCutscenes will call startCountdown later.
VanillaCutscenes . playHorrorStartCutscene ( ) ;
2023-01-23 00:55:30 +00:00
}
e lse
{
2023-06-22 05:41:01 +00: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-23 00:55:30 +00:00
startCountdown ( ) ;
}
2025-04-14 15:20:15 +00:00
// Create the pause button.
#if mobile
2025-04-23 03:31:58 +00:00
initPauseSprites ( ) ;
2025-04-14 15:20:15 +00:00
#end
2023-08-02 22:08:49 +00:00
// Do this last to prevent beatHit from being called before create() is done.
super . create ( ) ;
2023-06-22 05:41:01 +00:00
leftWatermarkText . cameras = [ camHUD ] ;
rightWatermarkText . cameras = [ camHUD ] ;
// Initialize some debug stuff.
2024-08-26 22:01:36 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-06-22 05:41:01 +00:00
// Display the version number (and git commit hash) in the bottom right corner.
2023-01-23 00:55:30 +00:00
this . rightWatermarkText . text = Constants . VERSION ;
2023-02-22 01:58:15 +00:00
FlxG . console . registerObject ( ' p l a y S t a t e ' , this ) ;
#end
2023-08-09 04:28:09 +00:00
initialized = true ;
2024-04-01 17:05:16 +00:00
// This step ensures z-indexes are applied properly,
// and it's important to call it last so all elements get affected.
refresh ( ) ;
2023-01-23 00:55:30 +00:00
}
2025-06-12 01:06:56 +00:00
public function togglePauseButton ( visible : Bool = false ) : Void
{
#if mobile
pauseCircle . alpha = visible ? 0.1 : 0 ;
pauseButton . alpha = visible ? 1 : 0 ;
#end
}
2023-10-09 18:13:14 +00:00
function assertChartExists ( ) : Bool
{
// Returns null if the song failed to load or doesn't have the selected difficulty.
2025-04-23 03:31:58 +00:00
if ( currentSong == null || currentChart == null || currentChart ? . notes == null )
2023-10-09 18:13:14 +00:00
{
// We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic.
criticalFailure = true ;
// Choose an error message.
var message: String = ' T h e r e w a s a c r i t i c a l e r r o r . C l i c k O K t o r e t u r n t o t h e m a i n m e n u . ' ;
if ( currentSong == null )
{
2024-05-20 14:42:17 +00:00
message = ' T h e r e w a s a c r i t i c a l e r r o r l o a d i n g t h i s s o n g \' s c h a r t . C l i c k O K t o r e t u r n t o t h e m a i n m e n u . ' ;
2023-10-09 18:13:14 +00:00
}
e lse if ( currentDifficulty == null )
{
2024-05-20 14:42:17 +00:00
message = ' T h e r e w a s a c r i t i c a l e r r o r s e l e c t i n g a d i f f i c u l t y f o r t h i s s o n g . C l i c k O K t o r e t u r n t o t h e m a i n m e n u . ' ;
2023-10-09 18:13:14 +00:00
}
2024-02-06 00:46:11 +00:00
e lse if ( currentChart == null )
2023-10-09 18:13:14 +00:00
{
2024-05-20 14:42:17 +00:00
message = ' T h e r e w a s a c r i t i c a l e r r o r r e t r i e v i n g d a t a f o r t h i s s o n g o n " $ currentDifficulty " d i f f i c u l t y w i t h v a r i a t i o n " $ currentVariation " . C l i c k O K t o r e t u r n t o t h e m a i n m e n u . ' ;
2023-10-09 18:13:14 +00:00
}
2025-04-23 03:31:58 +00:00
e lse if ( currentChart ? . notes == null )
2024-02-17 03:01:47 +00:00
{
2024-05-20 14:42:17 +00:00
message = ' T h e r e w a s a c r i t i c a l e r r o r r e t r i e v i n g n o t e d a t a f o r t h i s s o n g o n " $ currentDifficulty " d i f f i c u l t y w i t h v a r i a t i o n " $ currentVariation " . C l i c k O K t o r e t u r n t o t h e m a i n m e n u . ' ;
2024-02-17 03:01:47 +00:00
}
2023-10-09 18:13:14 +00:00
// Display a popup. This blocks the application until the user clicks OK.
2025-08-03 09:50:54 +00:00
funkin . util . WindowUtil . showError ( ' E r r o r l o a d i n g P l a y S t a t e ' , message ) ;
2023-10-09 18:13:14 +00:00
// Force the user back to the main menu.
if ( isSubState )
{
this . close ( ) ;
}
e lse
{
2025-04-23 03:31:58 +00:00
if ( currentStage != null ) this . remove ( currentStage ) ;
2024-02-06 00:46:11 +00:00
FlxG . switchState ( ( ) - > new MainMenuState ( ) ) ;
2023-10-09 18:13:14 +00:00
}
return false ;
}
return true ;
}
2023-06-22 05:41:01 +00:00
public override function update ( elapsed : Float ) : Void
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
if ( criticalFailure ) return ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
super . update ( elapsed ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
updateHealthBar ( ) ;
updateScoreText ( ) ;
// Handle restarting the song when needed (player death or pressing Retry)
if ( needsReset )
2023-01-23 00:55:30 +00:00
{
2023-10-09 18:13:14 +00:00
if ( ! assertChartExists ( ) ) return ;
2024-05-15 16:24:04 +00:00
prevScrollTargets = [ ] ;
2025-01-17 20:37:00 +00:00
var retryEvent = new SongRetryEvent ( currentDifficulty ) ;
previousDifficulty = currentDifficulty ;
dispatchEvent ( retryEvent ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
resetCamera ( ) ;
2023-01-23 00:55:30 +00:00
2024-05-09 05:10:53 +00:00
var fromDeathState = isPlayerDying ;
2023-06-22 05:41:01 +00:00
persistentUpdate = true ;
persistentDraw = true ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
startingSong = true ;
2024-03-04 21:37:42 +00:00
isPlayerDying = false ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Reset music properly.
2024-04-07 01:42:18 +00:00
if ( FlxG . sound . music != null )
{
FlxG . sound . music . pause ( ) ;
2025-05-03 04:48:21 +00:00
FlxG . sound . music . time = startTimestamp ;
2024-10-19 08:21:50 +00:00
FlxG . sound . music . pitch = playbackRate ;
2024-04-07 01:42:18 +00:00
}
2023-10-09 18:19:52 +00:00
2025-04-23 03:31:58 +00:00
if ( ! overrideMusic && vocals != null )
2023-10-18 05:02:10 +00:00
{
2023-11-09 22:00:46 +00:00
// Stop the vocals if they already exist.
2025-04-23 03:31:58 +00:00
vocals . stop ( ) ;
vocals = currentChart ? . buildVocals ( currentInstrumental ) ;
2023-10-18 05:02:10 +00:00
2025-04-23 03:31:58 +00:00
if ( vocals ? . members ? . length == 0 )
2023-10-18 05:02:10 +00:00
{
trace ( ' W A R N I N G : N o v o c a l s f o u n d f o r t h i s s o n g . ' ) ;
}
}
2023-01-23 00:55:30 +00:00
2024-04-07 01:42:18 +00:00
if ( FlxG . sound . music != null ) FlxG . sound . music . volume = 1 ;
2023-01-23 00:55:30 +00:00
2025-04-23 03:31:58 +00:00
if ( vocals != null )
{
vocals . pause ( ) ;
vocals . time = startTimestamp - Conductor . instance . instrumentalOffset ;
vocals . volume = 1 ;
vocals . playerVolume = 1 ;
vocals . opponentVolume = 1 ;
}
currentStage ? . resetStage ( ) ;
2023-06-22 05:41:01 +00:00
2024-05-09 05:10:53 +00:00
if ( ! fromDeathState )
{
playerStrumline . vwooshNotes ( ) ;
opponentStrumline . vwooshNotes ( ) ;
}
2023-07-31 17:42:13 +00:00
2023-07-26 01:39:19 +00:00
playerStrumline . clean ( ) ;
opponentStrumline . clean ( ) ;
2023-06-22 05:41:01 +00:00
// Delete all notes and reset the arrays.
regenNoteData ( ) ;
// Reset camera zooming
2024-03-28 18:05:38 +00:00
cameraBopIntensity = Constants . DEFAULT_BOP_INTENSITY ;
2024-04-05 06:44:44 +00:00
hudCameraZoomIntensity = ( cameraBopIntensity - 1.0 ) * 2.0 ;
2023-06-22 05:41:01 +00:00
cameraZoomRate = Constants . DEFAULT_ZOOM_RATE ;
2023-06-27 17:43:42 +00:00
health = Constants . HEALTH_STARTING ;
2023-06-22 05:41:01 +00:00
songScore = 0 ;
Highscore . tallies . combo = 0 ;
2025-04-29 20:13:13 +00:00
// so the song doesn't start too early :D
var vwooshDelay: Float = 0.5 ;
Conductor . instance . update ( - vwooshDelay * 1000 + startTimestamp + Conductor . instance . beatLengthMs * - 5 ) ;
2025-04-22 16:34:16 +00:00
// timer for vwoosh
2025-04-29 20:13:13 +00:00
vwooshTimer . start ( vwooshDelay , function ( _ ) {
2025-04-22 16:34:16 +00:00
if ( playerStrumline . notes . length == 0 ) playerStrumline . updateNotes ( ) ;
if ( opponentStrumline . notes . length == 0 ) opponentStrumline . updateNotes ( ) ;
playerStrumline . vwooshInNotes ( ) ;
opponentStrumline . vwooshInNotes ( ) ;
Countdown . performCountdown ( ) ;
} ) ;
2023-06-22 05:41:01 +00:00
2025-04-29 20:13:13 +00:00
// Stops any existing countdown.
Countdown . stopCountdown ( ) ;
2025-03-07 04:57:05 +00:00
// Reset the health icons.
2025-04-23 02:58:00 +00:00
currentStage ? . getBoyfriend ( ) ? . initHealthIcon ( false ) ;
currentStage ? . getDad ( ) ? . initHealthIcon ( true ) ;
2025-03-07 04:57:05 +00:00
2023-06-22 05:41:01 +00:00
needsReset = false ;
}
// Update the conductor.
if ( startingSong )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
if ( isInCountdown )
{
2024-04-11 01:31:49 +00:00
// Do NOT apply offsets at this point, because they already got applied the previous frame!
Conductor . instance . update ( Conductor . instance . songPosition + elapsed * 1000 , false ) ;
2024-10-19 08:21:50 +00:00
if ( Conductor . instance . songPosition >= ( startTimestamp + Conductor . instance . combinedOffset ) )
2024-09-19 07:02:59 +00:00
{
trace ( " s t a r t e d s o n g a t " + Conductor . instance . songPosition ) ;
startSong ( ) ;
}
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
e lse
{
2023-12-05 07:44:57 +00:00
if ( Constants . EXT_SOUND == ' m p 3 ' )
{
2023-12-14 21:56:20 +00:00
Conductor . instance . formatOffset = Constants . MP3_DELAY_MS ;
2023-12-05 07:44:57 +00:00
}
e lse
{
2023-12-14 21:56:20 +00:00
Conductor . instance . formatOffset = 0.0 ;
2023-12-05 07:44:57 +00:00
}
2023-01-23 00:55:30 +00:00
2025-08-01 17:16:39 +00:00
// Lime has some precision loss when getting the sound current position
// Since the notes scrolling is dependant on the sound time that caused it to appear "stuttery" for some people
// As a workaround for that, we lerp the conductor position to the music time to fill the gap in this lost precision making the scrolling smoother
// The previous method where it "guessed" the song position based on the elapsed time had some flaws
// Somtimes the songPosition would exceed the music length causing issues in other places
// And it was frame dependant which we don't like!!
2025-08-13 03:47:47 +00:00
if ( FlxG . sound . music . playing )
{
2025-08-15 14:44:28 +00:00
final audioDiff : Float = Math . round ( Math . abs ( FlxG . sound . music . time - ( Conductor . instance . songPosition - Conductor . instance . combinedOffset ) ) ) ;
2025-08-16 11:28:34 +00:00
if ( audioDiff <= CONDUCTOR_DRIFT_THRESHOLD )
2025-08-13 21:13:53 +00:00
{
// Only do neat & smooth lerps as long as the lerp doesn't fuck up and go WAY behind the music time triggering false resyncs
2025-08-17 17:34:46 +00:00
final easeRatio : Float = 1.0 - Math . exp ( - ( MUSIC_EASE_RATIO * playbackRate ) * elapsed ) ;
2025-08-13 21:13:53 +00:00
Conductor . instance . update ( FlxMath . lerp ( Conductor . instance . songPosition , FlxG . sound . music . time + Conductor . instance . combinedOffset , easeRatio ) , false ) ;
}
e lse
{
// Fallback to properly update the conductor incase the lerp messed up
// Shouldn't be fallen back to unless you're lagging alot
2025-08-16 11:28:34 +00:00
trace ( ' [ W A R N I N G ] N o r m a l C o n d u c t o r U p d a t e ! ! a r e y o u l a g g i n g ? ' ) ;
2025-08-13 21:13:53 +00:00
Conductor . instance . update ( ) ;
}
2025-08-13 03:47:47 +00:00
}
2023-01-23 00:55:30 +00:00
}
2025-04-14 15:20:15 +00:00
var pauseButtonCheck: Bool = false ;
2023-06-22 05:41:01 +00:00
var androidPause: Bool = false ;
2025-04-14 15:20:15 +00:00
// So the player wouldn't miss when pressing the pause utton
#if mobile
2025-05-09 10:28:31 +00:00
pauseButtonCheck = TouchUtil . pressAction ( pauseButton ) ;
2025-04-14 15:20:15 +00:00
#end
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
#if android
2025-04-14 15:20:15 +00:00
androidPause = FlxG . android . justReleased . BACK ;
2023-06-22 05:41:01 +00:00
#end
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Attempt to pause the game.
2025-06-04 14:17:53 +00:00
if ( ( controls . PAUSE || androidPause || pauseButtonCheck ) ) pause ( ) ;
2023-01-23 00:55:30 +00:00
2025-04-14 15:20:15 +00:00
#if mobile
2024-11-23 13:16:34 +00:00
if ( justUnpaused )
{
2025-06-08 03:09:40 +00:00
// pauseButton.alpha = 1;
// pauseCircle.alpha = 0.1;
FlxTween . cancelTweensOf ( pauseButton ) ;
FlxTween . cancelTweensOf ( pauseCircle ) ;
FlxTween . tween ( pauseButton , { alpha : 1 } , 0.25 , { ease : FlxEase . quartOut } ) ;
FlxTween . tween ( pauseCircle , { alpha : 0.1 } , 0.25 , { ease : FlxEase . quartOut } ) ;
2024-11-23 13:16:34 +00:00
2025-06-29 00:17:45 +00:00
if ( ! startingSong && hitbox != null ) hitbox . visible = true ;
2024-11-23 13:16:34 +00:00
}
2025-04-14 15:20:15 +00:00
#end
2023-06-22 05:41:01 +00:00
// Cap health.
2023-06-27 17:43:42 +00:00
if ( health > Constants . HEALTH_MAX ) health = Constants . HEALTH_MAX ;
if ( health < Constants . HEALTH_MIN ) health = Constants . HEALTH_MIN ;
2023-01-23 00:55:30 +00:00
2024-03-28 18:05:38 +00:00
// Apply camera zoom + multipliers.
2024-04-20 06:11:04 +00:00
if ( subState == null && cameraZoomRate > 0.0 ) // && !isInCutscene)
2023-01-23 00:55:30 +00:00
{
2024-03-28 18:05:38 +00:00
cameraBopMultiplier = FlxMath . lerp ( 1.0 , cameraBopMultiplier , 0.95 ) ; // Lerp bop multiplier back to 1.0x
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier ; // Apply camera bop multiplier.
2024-09-05 05:22:45 +00:00
if ( ! debugUnbindCameraZoom ) FlxG . camera . zoom = zoomPlusBop ; // Actually apply the zoom to the camera.
2024-03-16 15:38:10 +00:00
2023-06-22 05:41:01 +00:00
camHUD . zoom = FlxMath . lerp ( defaultHUDCameraZoom , camHUD . zoom , 0.95 ) ;
2023-01-23 00:55:30 +00:00
}
2024-03-01 13:13:06 +00:00
if ( currentStage != null && currentStage . getBoyfriend ( ) != null )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
FlxG . watch . addQuick ( ' b f A n i m ' , currentStage . getBoyfriend ( ) . getCurrentAnimation ( ) ) ;
2023-06-02 19:35:01 +00:00
}
2024-02-15 22:25:28 +00:00
FlxG . watch . addQuick ( ' h e a l t h ' , health ) ;
2024-04-05 06:44:44 +00:00
FlxG . watch . addQuick ( ' c a m e r a B o p I n t e n s i t y ' , cameraBopIntensity ) ;
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
// TODO: Add a song event for Handle GF dance speed.
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
// Handle player death.
2024-02-13 07:09:25 +00:00
if ( ! isInCutscene && ! disableKeys )
2023-06-22 05:41:01 +00:00
{
// RESET = Quick Game Over Screen
if ( controls . RESET )
2023-01-23 00:55:30 +00:00
{
2023-06-27 17:43:42 +00:00
health = Constants . HEALTH_MIN ;
2023-06-22 05:41:01 +00:00
trace ( ' R E S E T = T r u e ' ) ;
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
#if CAN_CHEAT // brandon's a pussy
if ( controls . CHEAT )
{
2023-06-27 17:43:42 +00:00
health += 0.25 * Constants . HEALTH_MAX ; // +25% health.
2023-06-22 05:41:01 +00:00
trace ( ' U s e r i s c h e a t i n g ! ' ) ;
}
#end
2023-01-23 00:55:30 +00:00
2024-03-04 21:37:42 +00:00
if ( health <= Constants . HEALTH_MIN && ! isPracticeMode && ! isPlayerDying )
2023-06-22 05:41:01 +00:00
{
2025-04-23 03:31:58 +00:00
vocals ? . pause ( ) ;
2024-05-01 03:24:43 +00:00
if ( FlxG . sound . music != null ) FlxG . sound . music . pause ( ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
deathCounter += 1 ;
2025-03-27 06:55:45 +00:00
#if FEATURE_NEWGROUNDS
Events . logFailSong ( currentSong . id , currentVariation ) ;
#end
2023-01-23 00:55:30 +00:00
2023-10-26 09:46:22 +00:00
dispatchEvent ( new ScriptEvent ( GAME_OVER ) ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Disable updates, preventing animations in the background from playing.
persistentUpdate = false ;
2024-08-26 22:01:36 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-06-22 05:41:01 +00: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 ;
}
e lse
{
#end
persistentDraw = false ;
2024-08-26 22:01:36 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-06-22 05:41:01 +00:00
}
#end
2023-02-22 01:58:15 +00:00
2024-03-04 21:37:42 +00:00
isPlayerDying = true ;
2025-06-07 14:26:38 +00:00
#if FEATURE_MOBILE_ADVERTISEMENTS
2025-06-10 12:29:08 +00:00
if ( AdMobUtil . PLAYING_COUNTER < AdMobUtil . MAX_BEFORE_AD ) AdMobUtil . PLAYING_COUNTER ++ ;
2025-06-07 14:26:38 +00:00
#end
2024-03-04 21:37:42 +00:00
var deathPreTransitionDelay = currentStage ? . getBoyfriend ( ) ? . getDeathPreTransitionDelay ( ) ? ? 0.0 ;
if ( deathPreTransitionDelay > 0 )
{
new FlxTimer ( ) . start ( deathPreTransitionDelay , function ( _ ) {
moveToGameOver ( ) ;
2023-12-19 06:27:58 +00:00
} ) ;
2024-03-04 21:37:42 +00:00
}
e lse
{
// Transition immediately.
moveToGameOver ( ) ;
}
2023-06-22 05:41:01 +00:00
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2024-09-19 14:03:16 +00:00
DiscordClient . instance . setPresence (
{
2024-09-19 15:13:05 +00:00
details : ' G a m e O v e r - ${ buildDiscordRPCDetails ( ) } ' ,
state : buildDiscordRPCState ( ) ,
2024-09-25 07:58:43 +00:00
largeImageKey : discordRPCAlbum ,
2024-09-19 14:03:16 +00:00
smallImageKey : discordRPCIcon
} ) ;
2023-06-22 05:41:01 +00:00
#end
}
2024-03-04 21:37:42 +00:00
e lse if ( isPlayerDying )
{
// Wait up.
}
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
2024-02-27 00:03:04 +00:00
processSongEvents ( ) ;
// Handle keybinds.
processInputQueue ( ) ;
if ( ! isInCutscene && ! disableKeys ) debugKeyShit ( ) ;
if ( isInCutscene && ! disableKeys ) handleCutsceneKeys ( elapsed ) ;
// Moving notes into position is now done by Strumline.update().
2024-03-06 22:22:11 +00:00
if ( ! isInCutscene ) processNotes ( elapsed ) ;
2024-02-28 08:04:56 +00:00
2025-04-14 15:20:15 +00:00
#if mobile
if ( ( VideoCutscene . isPlaying ( ) || isInCutscene ) && ! pauseButton . visible ) pauseButton . visible = true ;
2025-06-08 03:09:40 +00:00
pauseCircle . visible = pauseButton . visible ;
2025-04-14 15:20:15 +00:00
#end
2024-02-28 08:04:56 +00:00
justUnpaused = false ;
2025-06-22 20:08:36 +00:00
#if ! mobile
if ( Preferences . autoPause ) FlxG . autoPause = ! mayPauseGame ;
#end
2024-02-27 00:03:04 +00:00
}
2025-04-23 03:31:58 +00:00
function pause ( mode : PauseMode = Standard ) : Void
2025-06-04 14:17:53 +00:00
{
2025-08-02 23:31:44 +00:00
if ( ! mayPauseGame || justUnpaused || isGamePaused || isPlayerDying ) return ;
2025-06-22 20:08:36 +00:00
switch ( mode )
2025-06-04 14:17:53 +00:00
{
2025-06-22 20:08:36 +00:00
c ase Conversation :
preparePauseUI ( ) ;
2025-04-23 03:31:58 +00:00
openPauseSubState ( Conversation , FullScreenScaleMode . hasFakeCutouts ? camCutouts : camCutscene , ( ) - > currentConversation ? . pauseMusic ( ) ) ;
2025-06-22 20:08:36 +00:00
c ase Cutscene :
preparePauseUI ( ) ;
2025-07-25 14:36:34 +00:00
openPauseSubState ( Cutscene , FullScreenScaleMode . hasFakeCutouts ? camCutouts : camCutscene , ( ) - > VideoCutscene . pauseVideo ( ) ) ;
2025-06-22 20:08:36 +00:00
d efault : // also known as standard
if ( ! isInCountdown || isInCutscene ) return ;
Countdown . pauseCountdown ( ) ;
preparePauseUI ( ) ;
final event = new PauseScriptEvent ( FlxG . random . bool ( 1 / 1000 * 100 ) ) ;
dispatchEvent ( event ) ;
if ( ! event . eventCanceled )
{
persistentUpdate = false ;
persistentDraw = true ;
if ( ! isSubState && event . gitaroo )
{
2025-04-23 03:31:58 +00:00
if ( currentStage != null ) this . remove ( currentStage ) ;
2025-06-22 20:08:36 +00:00
FlxG . switchState ( ( ) - > new GitarooPause ( lastParams ) ) ;
}
e lse
{
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 ( ) ;
}
openPauseSubState ( isChartingMode ? Charting : Standard , camCutscene ) ;
}
#if FEATURE_DISCORD_RPC
DiscordClient . instance . setPresence (
{
details : ' P a u s e d - ${ buildDiscordRPCDetails ( ) } ' ,
state : buildDiscordRPCState ( ) ,
largeImageKey : discordRPCAlbum ,
smallImageKey : discordRPCIcon
} ) ;
#end
}
2025-06-04 14:17:53 +00:00
}
2025-06-22 20:08:36 +00:00
}
2025-06-04 15:12:54 +00:00
2025-06-22 20:08:36 +00:00
function preparePauseUI ( ) : Void
{
2025-06-04 14:17:53 +00:00
#if mobile
2025-06-08 03:09:40 +00:00
FlxTween . cancelTweensOf ( pauseButton ) ;
FlxTween . cancelTweensOf ( pauseCircle ) ;
2025-06-04 14:17:53 +00:00
pauseButton . alpha = 0 ;
2025-06-08 03:09:40 +00:00
pauseCircle . alpha = 0 ;
2025-06-29 00:17:45 +00:00
if ( hitbox != null ) hitbox . visible = false ;
2025-06-04 14:17:53 +00:00
#end
2025-06-22 20:08:36 +00:00
}
2025-06-04 14:17:53 +00:00
2025-07-25 14:36:34 +00:00
function openPauseSubState ( mode : PauseMode , cam : FlxCamera , ? onPause : Void -> Void ) : Void
2025-06-22 20:08:36 +00:00
{
2025-07-25 14:36:34 +00:00
final pauseSubState = new PauseSubState ( { mode : mode } , onPause ) ;
2025-06-22 20:08:36 +00:00
FlxTransitionableState . skipNextTransIn = true ;
FlxTransitionableState . skipNextTransOut = true ;
pauseSubState . camera = cam ;
persistentUpdate = false ;
openSubState ( pauseSubState ) ;
2025-06-04 14:17:53 +00:00
}
2024-03-04 21:37:42 +00:00
function moveToGameOver ( ) : Void
{
2024-05-09 05:10:53 +00:00
// Reset and update a bunch of values in advance for the transition back from the game over substate.
playerStrumline . clean ( ) ;
opponentStrumline . clean ( ) ;
2025-04-29 20:13:13 +00:00
vwooshTimer . cancel ( ) ;
2024-05-09 05:10:53 +00:00
songScore = 0 ;
updateScoreText ( ) ;
health = Constants . HEALTH_STARTING ;
healthLerp = health ;
healthBar . value = healthLerp ;
2024-05-15 20:50:34 +00:00
if ( ! isMinimalMode )
{
2025-04-23 03:31:58 +00:00
iconP1 ? . updatePosition ( ) ;
iconP2 ? . updatePosition ( ) ;
2024-05-15 20:50:34 +00:00
}
2024-05-09 05:10:53 +00:00
// Transition to the game over substate.
2024-03-04 21:37:42 +00:00
var gameOverSubState = new GameOverSubState (
{
isChartingMode : isChartingMode ,
transparent : persistentDraw
} ) ;
2024-03-17 02:20:22 +00:00
FlxTransitionableState . skipNextTransIn = true ;
FlxTransitionableState . skipNextTransOut = true ;
2024-03-04 21:37:42 +00:00
openSubState ( gameOverSubState ) ;
}
2024-02-27 00:03:04 +00:00
function processSongEvents ( ) : Void
{
2023-06-22 05:41:01 +00:00
// Query and activate song events.
2024-02-27 00:03:04 +00:00
// TODO: Check that these work appropriately even when songPosition is less than 0, to play events during countdown.
2025-04-23 03:31:58 +00:00
if ( songEvents . length > 0 )
2023-06-02 19:35:01 +00:00
{
2024-01-06 01:11:38 +00:00
var songEventsToActivate: Array < SongEventData > = SongEventRegistry . queryEvents ( songEvents , Conductor . instance . songPosition ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( songEventsToActivate . length > 0 )
{
trace ( ' F o u n d ${ songEventsToActivate . length } e v e n t ( s ) t o a c t i v a t e . ' ) ;
for ( event in songEventsToActivate )
{
2024-02-27 00:03:04 +00:00
// 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 )
2023-07-27 01:34:38 +00:00
{
event . activated = true ;
continue ;
} ;
2023-06-22 05:41:01 +00:00
var eventEvent: SongEventScriptEvent = new SongEventScriptEvent ( event ) ;
dispatchEvent ( eventEvent ) ;
// Calling event.cancelEvent() skips the event. Neat!
if ( ! eventEvent . eventCanceled )
{
2024-01-04 02:10:14 +00:00
SongEventRegistry . handleEvent ( event ) ;
2023-06-22 05:41:01 +00:00
}
}
}
2023-01-23 00:55:30 +00:00
}
}
2023-06-22 05:41:01 +00:00
public override function dispatchEvent ( event : ScriptEvent ) : Void
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
// ORDER: Module, Stage, Character, Song, Conversation, Note
// Modules should get the first chance to cancel the event.
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// super.dispatchEvent(event) dispatches event to module scripts.
super . dispatchEvent ( event ) ;
2023-01-23 00:55:30 +00:00
2024-07-13 01:40:46 +00:00
// Dispatch event to note kind scripts
NoteKindManager . callEvent ( event ) ;
2023-06-22 05:41:01 +00:00
// Dispatch event to stage script.
ScriptEventDispatcher . callEvent ( currentStage , event ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Dispatch event to character script(s).
if ( currentStage != null ) currentStage . dispatchToCharacters ( event ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Dispatch event to song script.
ScriptEventDispatcher . callEvent ( currentSong , event ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Dispatch event to conversation script.
ScriptEventDispatcher . callEvent ( currentConversation , event ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Function called before opening a new substate .
* @ param subState The substate to open .
* /
2023-06-22 05:41:01 +00:00
public override function openSubState ( subState : FlxSubState ) : Void
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
// If there is a substate which requires the game to continue,
// then make this a condition.
2025-03-03 18:03:01 +00:00
var shouldPause: Bool = ( Std . isOfType ( subState , PauseSubState ) || Std . isOfType ( subState , GameOverSubState ) ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( shouldPause )
2023-06-02 19:35:01 +00:00
{
2023-06-22 05:41:01 +00:00
// Pause the music.
if ( FlxG . sound . music != null )
{
2024-03-10 23:35:41 +00:00
if ( FlxG . sound . music . playing )
2024-02-28 08:01:20 +00:00
{
FlxG . sound . music . pause ( ) ;
2024-03-10 23:35:41 +00:00
musicPausedBySubState = true ;
2024-02-28 08:01:20 +00:00
}
2024-03-10 23:35:41 +00:00
2025-06-11 02:12:16 +00:00
// Pause any sounds that are playing and keep track of them.
2025-04-14 00:55:46 +00:00
// Vocals are also paused here but are not included as they are handled separately.
if ( Std . isOfType ( subState , PauseSubState ) )
{
FlxG . sound . list . forEachAlive ( function ( sound : FlxSound ) {
2025-06-11 02:08:49 +00:00
if ( ! sound . active || sound == FlxG . sound . music ) return ;
// In case it's a scheduled sound
2025-07-11 21:53:47 +00:00
if ( Std . isOfType ( sound , FunkinSound ) )
{
var funkinSound: FunkinSound = cast sound ;
if ( funkinSound != null && ! funkinSound . isPlaying ) return ;
}
2025-06-11 02:08:49 +00:00
if ( ! sound . playing && sound . time >= 0 ) return ;
2025-04-14 00:55:46 +00:00
sound . pause ( ) ;
soundsPausedBySubState . add ( sound ) ;
} ) ;
vocals ? . forEach ( function ( voice : FunkinSound ) {
soundsPausedBySubState . remove ( voice ) ;
} ) ;
}
e lse
{
vocals ? . pause ( ) ;
}
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2025-07-21 22:24:47 +00:00
if ( ! vwooshTimer . finished ) vwooshTimer . active = false ;
2024-03-15 08:52:22 +00:00
// Pause camera tweening, and keep track of which tweens we pause.
2024-03-10 23:35:41 +00:00
if ( cameraFollowTween != null && cameraFollowTween . active )
{
cameraFollowTween . active = false ;
2024-03-15 08:52:22 +00:00
cameraTweensPausedBySubState . add ( cameraFollowTween ) ;
}
if ( cameraZoomTween != null && cameraZoomTween . active )
{
cameraZoomTween . active = false ;
cameraTweensPausedBySubState . add ( cameraZoomTween ) ;
2024-03-10 23:35:41 +00:00
}
2024-06-08 00:26:17 +00:00
// Pause camera follow
FlxG . camera . followLerp = 0 ;
2024-05-10 20:23:35 +00:00
for ( tween in scrollSpeedTweens )
2024-05-09 16:51:03 +00:00
{
2024-05-10 20:23:35 +00:00
if ( tween != null && tween . active )
{
tween . active = false ;
cameraTweensPausedBySubState . add ( tween ) ;
}
2024-05-09 16:51:03 +00:00
}
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
super . openSubState ( subState ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Function called before closing the current substate .
* @ param subState
* /
2023-06-22 05:41:01 +00:00
public override function closeSubState ( ) : Void
2023-01-23 00:55:30 +00:00
{
2023-08-04 20:15:07 +00:00
if ( Std . isOfType ( subState , PauseSubState ) )
2023-06-22 05:41:01 +00:00
{
2023-10-26 09:46:22 +00:00
var event: ScriptEvent = new ScriptEvent ( RESUME , true ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
dispatchEvent ( event ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( event . eventCanceled ) return ;
2023-01-23 00:55:30 +00:00
2025-04-29 20:13:13 +00:00
// Resume vwooshTimer
if ( ! vwooshTimer . finished ) vwooshTimer . active = true ;
2024-03-10 23:35:41 +00:00
// Resume music if we paused it.
2024-02-16 12:54:27 +00:00
if ( musicPausedBySubState )
{
2024-02-28 08:01:20 +00:00
FlxG . sound . music . play ( ) ;
2024-03-10 23:35:41 +00:00
musicPausedBySubState = false ;
}
2025-07-20 01:19:46 +00:00
// The logic here is that if this sound doesn't auto-destroy
// then it's gonna be reused somewhere, so we just stop it instead.
forEachPausedSound ( s - > needsReset ? ( s . autoDestroy ? s . destroy ( ) : s . stop ( ) ) : s . resume ( ) ) ;
2025-04-14 00:55:46 +00:00
2024-03-15 08:52:22 +00:00
// Resume camera tweens if we paused any.
for ( camTween in cameraTweensPausedBySubState )
2024-03-10 23:35:41 +00:00
{
2024-03-15 08:52:22 +00:00
camTween . active = true ;
2024-02-16 12:54:27 +00:00
}
2024-03-15 08:52:22 +00:00
cameraTweensPausedBySubState . clear ( ) ;
2023-08-02 22:08:49 +00:00
2024-06-08 00:26:17 +00:00
// Resume camera follow
FlxG . camera . followLerp = Constants . DEFAULT_CAMERA_FOLLOW_RATE ;
2024-02-28 19:51:39 +00:00
if ( currentConversation != null )
{
currentConversation . resumeMusic ( ) ;
2024-02-16 12:54:27 +00:00
}
2023-08-02 22:08:49 +00:00
2024-03-10 23:35:41 +00:00
// Re-sync vocals.
2023-06-22 05:41:01 +00:00
if ( FlxG . sound . music != null && ! startingSong && ! isInCutscene ) resyncVocals ( ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Resume the countdown.
Countdown . resumeCountdown ( ) ;
2023-02-22 01:58:15 +00:00
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2024-09-19 14:03:16 +00:00
if ( Conductor . instance . songPosition > 0 )
2023-01-23 00:55:30 +00:00
{
2024-09-19 14:03:16 +00:00
// DiscordClient.changePresence(detailsText, '${currentChart.songName} ($discordRPCDifficulty)', discordRPCIcon, true,
// currentSongLengthMs - Conductor.instance.songPosition);
DiscordClient . instance . setPresence (
{
2024-09-19 15:13:05 +00:00
state : buildDiscordRPCState ( ) ,
2024-09-25 07:58:43 +00:00
details : buildDiscordRPCDetails ( ) ,
largeImageKey : discordRPCAlbum ,
2024-09-19 14:03:16 +00:00
smallImageKey : discordRPCIcon
} ) ;
2023-01-23 00:55:30 +00:00
}
e lse
{
2024-09-19 14:03:16 +00:00
DiscordClient . instance . setPresence (
{
2024-09-19 15:13:05 +00:00
state : buildDiscordRPCState ( ) ,
2024-09-25 07:58:43 +00:00
details : buildDiscordRPCDetails ( ) ,
largeImageKey : discordRPCAlbum ,
2024-09-19 14:03:16 +00:00
smallImageKey : discordRPCIcon
} ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
#end
2024-02-28 05:19:08 +00:00
justUnpaused = true ;
2023-01-23 00:55:30 +00:00
}
2023-08-04 20:15:07 +00:00
e lse if ( Std . isOfType ( subState , Transition ) )
{
// Do nothing.
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
super . closeSubState ( ) ;
2023-01-23 00:55:30 +00:00
}
2023-02-22 01:58:15 +00:00
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Function called when the game window gains focus .
* /
2023-06-22 05:41:01 +00:00
public override function onFocus ( ) : Void
2023-01-23 00:55:30 +00:00
{
2025-06-22 20:10:20 +00:00
if ( VideoCutscene . isPlaying ( ) && Preferences . autoPause && isGamePaused ) VideoCutscene . pauseVideo ( ) ;
2024-06-24 08:55:59 +00:00
#if html5
2025-06-22 20:10:20 +00:00
e lse if ( Preferences . autoPause ) VideoCutscene . resumeVideo ( ) ;
2024-06-24 08:55:59 +00:00
#end
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2025-06-22 20:10:20 +00:00
if ( health > Constants . HEALTH_MIN && ! isGamePaused && Preferences . autoPause )
2023-01-23 00:55:30 +00:00
{
2024-09-19 14:03:16 +00:00
if ( Conductor . instance . songPosition > 0.0 )
{
DiscordClient . instance . setPresence (
{
2024-09-19 15:13:05 +00:00
state : buildDiscordRPCState ( ) ,
details : buildDiscordRPCDetails ( ) ,
2024-09-25 07:58:43 +00:00
largeImageKey : discordRPCAlbum ,
2024-09-19 14:03:16 +00:00
smallImageKey : discordRPCIcon
} ) ;
}
2023-01-23 00:55:30 +00:00
e lse
2024-09-19 14:03:16 +00:00
{
DiscordClient . instance . setPresence (
{
2024-09-19 15:13:05 +00:00
state : buildDiscordRPCState ( ) ,
details : buildDiscordRPCDetails ( ) ,
2024-09-25 07:58:43 +00:00
largeImageKey : discordRPCAlbum ,
2024-09-19 14:03:16 +00:00
smallImageKey : discordRPCIcon
} ) ;
// DiscordClient.changePresence(detailsText, '${currentChart.songName} ($discordRPCDifficulty)', discordRPCIcon, true,
// currentSongLengthMs - Conductor.instance.songPosition);
}
2023-01-23 00:55:30 +00:00
}
2024-06-24 08:55:59 +00:00
#end
2023-01-23 00:55:30 +00:00
super . onFocus ( ) ;
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Function called when the game window loses focus .
* /
2023-06-22 05:41:01 +00:00
public override function onFocusLost ( ) : Void
2023-01-23 00:55:30 +00:00
{
2024-06-24 08:55:59 +00:00
#if html5
2025-06-22 20:10:20 +00:00
if ( Preferences . autoPause ) VideoCutscene . pauseVideo ( ) ;
2024-06-24 08:55:59 +00:00
#end
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2025-06-22 20:10:20 +00:00
if ( health > Constants . HEALTH_MIN && ! isGamePaused && Preferences . autoPause )
2024-09-19 14:03:16 +00:00
{
DiscordClient . instance . setPresence (
{
2024-09-19 15:13:05 +00:00
state : buildDiscordRPCState ( ) ,
details : buildDiscordRPCDetails ( ) ,
2024-09-25 07:58:43 +00:00
largeImageKey : discordRPCAlbum ,
2024-09-19 14:03:16 +00:00
smallImageKey : discordRPCIcon
} ) ;
}
2024-06-24 08:55:59 +00:00
#end
2025-06-22 20:21:15 +00:00
// if else if else if else if else if else AAAAAAAAAAAAAAAAAAAAAAA
if ( ! isGamePaused && Preferences . autoPause )
2025-06-04 14:17:53 +00:00
{
2025-06-22 20:21:15 +00:00
if ( currentConversation != null )
{
pause ( Conversation ) ;
}
e lse if ( VideoCutscene . isPlaying ( ) )
{
pause ( Cutscene ) ;
}
e lse
{
pause ( ) ;
}
2025-06-04 14:17:53 +00:00
}
2023-01-23 00:55:30 +00:00
super . onFocusLost ( ) ;
}
/ * *
2024-08-26 22:01:36 +00:00
* Call this by pressing F5 on a debug build .
* /
2024-07-09 22:23:06 +00:00
override function reloadAssets ( ) : Void
2023-01-23 00:55:30 +00:00
{
2025-04-21 06:44:23 +00:00
performCleanup ( ) ;
2025-05-11 20:13:27 +00:00
// `performCleanup()` clears the static reference to this state
2025-05-11 20:23:33 +00:00
// scripts might still need it, so we set it back to `this`
2025-05-11 20:13:27 +00:00
instance = this ;
2024-07-09 22:23:06 +00:00
funkin . modding . PolymodHandler . forceReloadAssets ( ) ;
2025-04-23 03:31:58 +00:00
if ( lastParams == null )
{
throw " N o l a s t P a r a m s t o r e f e r t o " ;
}
2025-05-27 07:20:50 +00:00
lastParams . targetSong = SongRegistry . instance . fetchEntry ( currentSong . id ,
{ variation : currentVariation } ) ? ? throw " C o u l d n o t l o a d c u r r e n t s o n g f r o m I D . T h i s s h o u l d n ' t h a p p e n ! " ;
2024-07-09 22:23:06 +00:00
LoadingState . loadPlayState ( lastParams ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
override function stepHit ( ) : Bool
2023-01-23 00:55:30 +00:00
{
2023-08-09 04:28:09 +00:00
if ( criticalFailure || ! initialized ) return false ;
2023-02-22 01:58:15 +00:00
2023-06-22 05:41:01 +00:00
// super.stepHit() returns false if a module cancelled the event.
if ( ! super . stepHit ( ) ) return false ;
2023-01-23 00:55:30 +00:00
2023-08-03 15:40:19 +00:00
if ( isGamePaused ) return false ;
2025-04-23 03:31:58 +00:00
iconP1 ? . onStepHit ( Std . int ( Conductor . instance . currentStep ) ) ;
iconP2 ? . onStepHit ( Std . int ( Conductor . instance . currentStep ) ) ;
2023-01-23 00:55:30 +00:00
2025-05-10 13:11:44 +00:00
// Try to call hold note haptics each step hit. Works if atleast one note status is NoteStatus.isHoldNotePressed.
playerStrumline . noteVibrations . tryHoldNoteVibration ( ) ;
2024-12-21 20:54:47 +00:00
2023-06-22 05:41:01 +00:00
return true ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
override function beatHit ( ) : Bool
2023-01-23 00:55:30 +00:00
{
2023-08-09 04:28:09 +00:00
if ( criticalFailure || ! initialized ) return false ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// super.beatHit() returns false if a module cancelled the event.
if ( ! super . beatHit ( ) ) return false ;
2023-01-23 00:55:30 +00:00
2023-08-03 15:40:19 +00:00
if ( isGamePaused ) return false ;
2023-06-22 05:41:01 +00:00
if ( generatedMusic )
{
// TODO: Sort more efficiently, or less often, to improve performance.
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
2023-01-23 00:55:30 +00:00
}
2024-09-19 08:20:16 +00:00
if ( FlxG . sound . music != null )
2024-07-23 18:02:09 +00:00
{
2024-09-21 10:33:47 +00:00
var correctSync: Float = Math . min ( FlxG . sound . music . length , Math . max ( 0 , Conductor . instance . songPosition - Conductor . instance . combinedOffset ) ) ;
2024-10-03 11:28:01 +00:00
var playerVoicesError: Float = 0 ;
var opponentVoicesError: Float = 0 ;
2025-03-13 16:57:32 +00:00
if ( vocals != null && vocals . playing )
2024-10-03 11:28:01 +00:00
{
2025-04-23 03:31:58 +00:00
@ : nullSafety ( Off )
2024-10-03 11:28:01 +00:00
@ : privateAccess // todo: maybe make the groups public :thinking:
{
2025-04-23 03:31:58 +00:00
vocals . playerVoices ? . forEachAlive ( function ( voice : FunkinSound ) {
2024-10-03 11:28:01 +00:00
var currentRawVoiceTime: Float = voice . time + vocals . playerVoicesOffset ;
if ( Math . abs ( currentRawVoiceTime - correctSync ) > Math . abs ( playerVoicesError ) ) playerVoicesError = currentRawVoiceTime - correctSync ;
} ) ;
2025-04-23 03:31:58 +00:00
vocals . opponentVoices ? . forEachAlive ( function ( voice : FunkinSound ) {
2024-10-03 11:28:01 +00:00
var currentRawVoiceTime: Float = voice . time + vocals . opponentVoicesOffset ;
if ( Math . abs ( currentRawVoiceTime - correctSync ) > Math . abs ( opponentVoicesError ) ) opponentVoicesError = currentRawVoiceTime - correctSync ;
} ) ;
}
}
if ( ! startingSong
2025-06-28 07:50:59 +00:00
& & ( Math . abs ( FlxG . sound . music . time - correctSync ) > RESYNC_THRESHOLD
| | Math . abs ( playerVoicesError ) > RESYNC_THRESHOLD
| | Math . abs ( opponentVoicesError ) > RESYNC_THRESHOLD ) )
2024-09-19 08:20:16 +00:00
{
trace ( " V O C A L S N E E D R E S Y N C " ) ;
2024-10-03 11:28:01 +00:00
if ( vocals != null )
{
trace ( playerVoicesError ) ;
trace ( opponentVoicesError ) ;
}
2024-09-19 08:20:16 +00:00
trace ( FlxG . sound . music . time ) ;
trace ( correctSync ) ;
resyncVocals ( ) ;
}
2024-07-23 18:02:09 +00:00
}
2024-03-28 18:05:38 +00:00
// Only bop camera if zoom level is below 135%
2024-03-24 03:52:08 +00:00
if ( Preferences . zoomCamera
2024-03-28 18:05:38 +00:00
& & FlxG . camera . zoom < ( 1.35 * FlxCamera . defaultZoom )
2024-03-24 03:52:08 +00:00
& & cameraZoomRate > 0
2024-10-04 17:51:19 +00:00
& & ( Conductor . instance . currentBeat + cameraZoomRateOffset ) % cameraZoomRate == 0 )
2023-01-23 00:55:30 +00:00
{
2024-03-28 18:05:38 +00:00
// Set zoom multiplier for camera bop.
cameraBopMultiplier = cameraBopIntensity ;
// HUD camera zoom still uses old system. To change. (+3%)
2023-06-22 05:41:01 +00:00
camHUD . zoom += hudCameraZoomIntensity * defaultHUDCameraZoom ;
2023-01-23 00:55:30 +00:00
}
2023-12-14 21:56:20 +00:00
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( playerStrumline != null ) playerStrumline . onBeatHit ( ) ;
if ( opponentStrumline != null ) opponentStrumline . onBeatHit ( ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
return true ;
2023-01-23 00:55:30 +00:00
}
2023-12-16 02:09:01 +00:00
public override function destroy ( ) : Void
2023-01-23 00:55:30 +00:00
{
2023-12-16 02:09:01 +00:00
performCleanup ( ) ;
2024-12-21 15:26:32 +00:00
#if mobile
// Syncing allowScreenTimeout with Preferences option.
lime . system . System . allowScreenTimeout = Preferences . screenTimeout ;
#end
2025-06-22 20:10:20 +00:00
#if ! mobile
FlxG . autoPause = Preferences . autoPause ;
#end
2023-06-22 05:41:01 +00:00
super . destroy ( ) ;
2023-01-23 00:55:30 +00:00
}
2024-09-05 05:22:45 +00:00
public override function initConsoleHelpers ( ) : Void
2023-01-23 00:55:30 +00:00
{
2024-09-05 05:22:45 +00:00
FlxG . console . registerFunction ( " d e b u g U n b i n d C a m e r a Z o o m " , ( ) - > {
debugUnbindCameraZoom = ! debugUnbindCameraZoom ;
} ) ;
} ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Initializes the game and HUD cameras .
* /
2023-06-22 05:41:01 +00:00
function initCameras ( ) : Void
2023-01-23 00:55:30 +00:00
{
2023-11-29 01:36:59 +00:00
camGame . bgColor = BACKGROUND_COLOR ; // Show a pink background behind the stage.
2023-06-22 05:41:01 +00:00
camHUD . bgColor . alpha = 0 ; // Show the game scene behind the camera.
camCutscene . bgColor . alpha = 0 ; // Show the game scene behind the camera.
2025-04-23 03:31:58 +00:00
camCutouts . setPosition ( ( FlxG . width - FlxG . initialWidth ) / 2 , ( FlxG . height - FlxG . initialHeight ) / 2 ) ;
camCutouts . setSize ( FlxG . initialWidth , FlxG . initialHeight ) ;
2024-12-23 15:03:01 +00:00
camCutouts . bgColor . alpha = 0 ; // Show the game scene behind the camera.
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
FlxG . cameras . reset ( camGame ) ;
FlxG . cameras . add ( camHUD , false ) ;
FlxG . cameras . add ( camCutscene , false ) ;
2024-12-23 15:03:01 +00:00
FlxG . cameras . add ( camCutouts , false ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Configure camera follow point.
if ( previousCameraFollowPoint != null )
{
cameraFollowPoint . setPosition ( previousCameraFollowPoint . x , previousCameraFollowPoint . y ) ;
previousCameraFollowPoint = null ;
}
add ( cameraFollowPoint ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Initializes the health bar on the HUD .
* /
2023-06-22 05:41:01 +00:00
function initHealthBar ( ) : Void
2023-01-23 00:55:30 +00:00
{
2023-10-17 04:38:28 +00:00
var healthBarYPos: Float = Preferences . downscroll ? FlxG . height * 0.1 : FlxG . height * 0.9 ;
2025-04-17 18:33:58 +00:00
#if mobile
2025-06-29 00:17:45 +00:00
if ( Preferences . controlsScheme == FunkinHitboxControlSchemes . Arrows
& & ! ControlsHandler . usingExternalInputDevice ) healthBarYPos = FlxG . height * 0.1 ;
2025-04-17 18:33:58 +00:00
#end
2025-04-23 03:31:58 +00:00
healthBarBG . y = healthBarYPos ;
2023-06-22 05:41:01 +00:00
healthBarBG . screenCenter ( X ) ;
healthBarBG . scrollFactor . set ( 0 , 0 ) ;
2024-02-29 02:19:21 +00:00
healthBarBG . zIndex = 800 ;
2023-06-22 05:41:01 +00:00
add ( healthBarBG ) ;
2023-01-23 00:55:30 +00:00
2025-04-23 03:31:58 +00:00
healthBar . x = healthBarBG . x + 4 ;
healthBar . y = healthBarBG . y + 4 ;
healthBar . parent = this ;
healthBar . parentVariable = ' h e a l t h L e r p ' ;
2023-06-22 05:41:01 +00:00
healthBar . scrollFactor . set ( ) ;
healthBar . createFilledBar ( Constants . COLOR_HEALTH_BAR_RED , Constants . COLOR_HEALTH_BAR_GREEN ) ;
2024-02-29 02:19:21 +00:00
healthBar . zIndex = 801 ;
2023-06-22 05:41:01 +00:00
add ( healthBar ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// The score text below the health bar.
2025-04-23 03:31:58 +00:00
scoreText . x = healthBarBG . x + healthBarBG . width - 190 ;
scoreText . y = healthBarBG . y + 30 ;
2023-06-22 05:41:01 +00:00
scoreText . setFormat ( Paths . font ( ' v c r . t t f ' ) , 16 , FlxColor . WHITE , RIGHT , FlxTextBorderStyle . OUTLINE , FlxColor . BLACK ) ;
scoreText . scrollFactor . set ( ) ;
2024-02-29 02:19:21 +00:00
scoreText . zIndex = 802 ;
2023-06-22 05:41:01 +00:00
add ( scoreText ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Move the health bar to the HUD camera.
healthBar . cameras = [ camHUD ] ;
healthBarBG . cameras = [ camHUD ] ;
scoreText . cameras = [ camHUD ] ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Generates the stage and all its props .
* /
2023-06-22 05:41:01 +00:00
function initStage ( ) : Void
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
loadStage ( currentStageId ) ;
}
2023-01-23 00:55:30 +00:00
2023-07-26 20:52:58 +00:00
function initMinimalMode ( ) : Void
{
// Create the green background.
2024-03-13 01:34:50 +00:00
var menuBG = FunkinSprite . create ( ' m e n u D e s a t ' ) ;
2023-07-26 20:52:58 +00:00
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 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Loads stage data from cache , assembles the props ,
* and adds it to the state .
* @ param id
* /
2023-06-22 05:41:01 +00:00
function loadStage ( id : String ) : Void
{
2024-01-16 21:49:15 +00:00
currentStage = StageRegistry . instance . fetchEntry ( id ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( currentStage != null )
2023-01-23 00:55:30 +00:00
{
2024-04-07 02:38:32 +00:00
currentStage . revive ( ) ; // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
2023-06-22 05:41:01 +00:00
// Actually create and position the sprites.
2023-10-26 09:46:22 +00:00
var event: ScriptEvent = new ScriptEvent ( CREATE , false ) ;
2023-06-22 05:41:01 +00:00
ScriptEventDispatcher . callEvent ( currentStage , event ) ;
2023-01-23 00:55:30 +00:00
2024-02-15 22:25:28 +00:00
resetCameraZoom ( ) ;
2023-02-22 19:46:46 +00:00
2023-06-22 05:41:01 +00:00
// Add the stage to the scene.
this . add ( currentStage ) ;
2023-02-22 20:01:07 +00:00
2024-08-26 22:01:36 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-06-22 05:41:01 +00:00
FlxG . console . registerObject ( ' s t a g e ' , currentStage ) ;
#end
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
e lse
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
// lolol
2025-08-21 19:23:11 +00:00
funkin . util . WindowUtil . showError ( ' S t a g e E r r o r ' , ' U n a b l e t o l o a d s t a g e $ id , i s i t s d a t a c o r r u p t e d ? . ' ) ;
2023-06-22 05:41:01 +00:00
}
}
2023-02-22 01:58:15 +00:00
2024-02-15 22:25:28 +00:00
public function resetCameraZoom ( ) : Void
{
2025-04-23 03:31:58 +00:00
if ( isMinimalMode ) return ;
2024-02-15 22:25:28 +00:00
// Apply camera zoom level from stage data.
2024-03-28 18:05:38 +00:00
currentCameraZoom = stageZoom ;
2024-03-16 15:38:10 +00:00
FlxG . camera . zoom = currentCameraZoom ;
2024-03-28 18:05:38 +00:00
// Reset bop multiplier.
cameraBopMultiplier = 1.0 ;
2024-02-15 22:25:28 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Generates the character sprites and adds them to the stage .
* /
2023-06-22 05:41:01 +00:00
function initCharacters ( ) : Void
{
if ( currentSong == null || currentChart == null )
{
2025-04-23 03:31:58 +00:00
throw ' S o n g d i f f i c u l t y c o u l d n o t b e l o a d e d . ' ;
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2025-04-23 03:31:58 +00:00
var currentCharacterData: Null < SongCharacterData > = currentChart ? . characters ;
if ( currentCharacterData == null )
{
trace ( ' C a n n o t r e t r i e v e c h a r a c t e r d a t a ' ) ;
return ;
}
// Switch the variation we are playing on by manipulating targetVariation.
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
//
// GIRLFRIEND
//
2025-04-23 03:31:58 +00:00
var girlfriend: Null < BaseCharacter > = CharacterDataParser . fetchCharacter ( currentCharacterData . girlfriend ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( girlfriend != null )
{
2024-09-18 09:24:57 +00:00
// Don't need to do anything.
2023-01-23 00:55:30 +00:00
}
2023-09-26 03:24:07 +00:00
e lse if ( currentCharacterData . girlfriend != ' ' )
2023-01-23 00:55:30 +00:00
{
2023-09-26 03:24:07 +00:00
trace ( ' W A R N I N G : C o u l d n o t l o a d g i r l f r i e n d c h a r a c t e r w i t h I D ${ currentCharacterData . girlfriend } , s k i p p i n g . . . ' ) ;
2023-01-23 00:55:30 +00:00
}
e lse
{
2023-06-22 05:41:01 +00:00
// Chosen GF was '' so we don't load one.
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
//
// DAD
//
2025-04-23 03:31:58 +00:00
var dad: Null < BaseCharacter > = CharacterDataParser . fetchCharacter ( currentCharacterData . opponent ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( dad != null )
2023-01-23 00:55:30 +00:00
{
2024-03-01 13:13:06 +00:00
//
// OPPONENT HEALTH ICON
//
iconP2 = new HealthIcon ( ' d a d ' , 1 ) ;
iconP2 . y = healthBar . y - ( iconP2 . height / 2 ) ;
dad . initHealthIcon ( true ) ; // Apply the character ID here
iconP2 . zIndex = 850 ;
add ( iconP2 ) ;
iconP2 . cameras = [ camHUD ] ;
2024-09-19 14:03:16 +00:00
#if FEATURE_DISCORD_RPC
2025-04-23 03:31:58 +00:00
discordRPCAlbum = ' a l b u m - ${ currentChart ? . album } ' ;
2024-09-25 07:58:43 +00:00
discordRPCIcon = ' i c o n - ${ currentCharacterData . opponent } ' ;
2024-09-19 14:03:16 +00:00
#end
2024-03-01 13:13:06 +00:00
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
//
// BOYFRIEND
//
2025-04-23 03:31:58 +00:00
var boyfriend: Null < BaseCharacter > = CharacterDataParser . fetchCharacter ( currentCharacterData . player ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( boyfriend != null )
2023-01-23 00:55:30 +00:00
{
2024-03-01 13:13:06 +00:00
//
// PLAYER HEALTH ICON
//
iconP1 = new HealthIcon ( ' b f ' , 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-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
//
// ADD CHARACTERS TO SCENE
//
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( currentStage != null )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
// Characters get added to the stage, not the main scene.
if ( girlfriend != null )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
currentStage . addCharacter ( girlfriend , GF ) ;
2023-01-23 00:55:30 +00:00
2024-08-26 22:01:36 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-06-22 05:41:01 +00:00
FlxG . console . registerObject ( ' g f ' , girlfriend ) ;
#end
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
if ( boyfriend != null )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
currentStage . addCharacter ( boyfriend , BF ) ;
2023-01-23 00:55:30 +00:00
2024-08-26 22:01:36 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-06-22 05:41:01 +00:00
FlxG . console . registerObject ( ' b f ' , boyfriend ) ;
2023-01-23 00:55:30 +00:00
#end
2023-06-16 21:37:56 +00:00
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( dad != null )
2023-06-16 21:37:56 +00:00
{
2023-06-22 05:41:01 +00:00
currentStage . addCharacter ( dad , DAD ) ;
// Camera starts at dad.
cameraFollowPoint . setPosition ( dad . cameraFocusPoint . x , dad . cameraFocusPoint . y ) ;
2023-01-23 00:55:30 +00:00
2024-08-26 22:01:36 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-06-22 05:41:01 +00:00
FlxG . console . registerObject ( ' d a d ' , dad ) ;
2023-01-23 00:55:30 +00:00
#end
}
2023-06-22 05:41:01 +00:00
// Rearrange by z-indexes.
currentStage . refresh ( ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-16 21:37:56 +00:00
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Constructs the strumlines for e a c h p l a y e r .
* /
2023-06-22 05:41:01 +00:00
function initStrumlines ( ) : Void
2023-02-22 01:58:15 +00:00
{
2024-03-06 02:48:04 +00:00
playerStrumline . onNoteIncoming . add ( onStrumlineNoteIncoming ) ;
opponentStrumline . onNoteIncoming . add ( onStrumlineNoteIncoming ) ;
2023-06-22 05:41:01 +00:00
add ( playerStrumline ) ;
add ( opponentStrumline ) ;
2023-01-23 00:55:30 +00:00
2025-06-13 01:21:21 +00:00
final cutoutSize = FullScreenScaleMode . gameCutoutSize . x / 2.5 ;
2023-07-08 05:03:46 +00:00
// Position the player strumline on the right half of the screen
2024-11-03 18:57:43 +00:00
playerStrumline . x = ( FlxG . width / 2 + Constants . STRUMLINE_X_OFFSET ) + ( cutoutSize / 2.0 ) ; // Classic style
2023-07-08 05:03:46 +00:00
// playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; // Centered style
2025-05-08 17:28:39 +00:00
2025-06-24 19:33:05 +00:00
playerStrumline . y = Preferences . downscroll ? FlxG . height - playerStrumline . height - Constants . STRUMLINE_Y_OFFSET - noteStyle . getStrumlineOffsets ( ) [ 1 ] : Constants . STRUMLINE_Y_OFFSET ;
2025-04-15 19:54:00 +00:00
2024-02-29 02:19:21 +00:00
playerStrumline . zIndex = 1001 ;
2023-06-22 05:41:01 +00:00
playerStrumline . cameras = [ camHUD ] ;
2023-01-23 00:55:30 +00:00
2023-07-08 05:03:46 +00:00
// Position the opponent strumline on the left half of the screen
2025-06-02 00:12:48 +00:00
opponentStrumline . x = Constants . STRUMLINE_X_OFFSET + cutoutSize ;
2025-06-24 19:33:05 +00:00
opponentStrumline . y = Preferences . downscroll ? FlxG . height - opponentStrumline . height - Constants . STRUMLINE_Y_OFFSET - noteStyle . getStrumlineOffsets ( ) [ 1 ] : Constants . STRUMLINE_Y_OFFSET ;
2025-04-15 19:54:00 +00:00
2024-02-29 02:19:21 +00:00
opponentStrumline . zIndex = 1000 ;
2023-06-22 05:41:01 +00:00
opponentStrumline . cameras = [ camHUD ] ;
2023-01-23 00:55:30 +00:00
2025-04-15 19:54:00 +00:00
#if mobile
2025-06-29 00:17:45 +00:00
if ( Preferences . controlsScheme == FunkinHitboxControlSchemes . Arrows && ! ControlsHandler . usingExternalInputDevice )
2025-04-15 19:54:00 +00:00
{
initNoteHitbox ( ) ;
}
#end
2024-07-04 19:55:19 +00:00
playerStrumline . fadeInArrows ( ) ;
opponentStrumline . fadeInArrows ( ) ;
2023-01-23 00:55:30 +00:00
}
2025-04-15 19:54:00 +00:00
/ * *
2025-04-16 11:35:28 +00:00
* Configures the position of strumline for t h e d e f a u l t c o n t r o l s c h e m e
2025-04-15 19:54:00 +00:00
* /
#if mobile
function initNoteHitbox ( )
{
2025-01-05 23:17:50 +00:00
final amplification : Float = ( FlxG . width / FlxG . height ) / ( FlxG . initialWidth / FlxG . initialHeight ) ;
2025-01-24 10:00:51 +00:00
final playerStrumlineScale : Float = ( ( FlxG . height / FlxG . width ) * 1.95 ) * amplification ;
2025-01-05 23:17:50 +00:00
final playerNoteSpacing : Float = ( ( FlxG . height / FlxG . width ) * 2.8 ) * amplification ;
2025-04-16 10:52:00 +00:00
playerStrumline . strumlineScale . set ( playerStrumlineScale , playerStrumlineScale ) ;
playerStrumline . setNoteSpacing ( playerNoteSpacing ) ;
2025-04-23 03:31:58 +00:00
@ : nullSafety ( Off ) // Who thought it'd be a good idea to make the iterator nullable?
2024-12-17 16:05:49 +00:00
for ( strum in playerStrumline )
{
strum . width *= 2 ;
}
2025-01-12 11:07:38 +00:00
opponentStrumline . enterMiniMode ( 0.4 * amplification ) ;
2025-04-15 19:54:00 +00:00
playerStrumline . x = ( FlxG . width - playerStrumline . width ) / 2 + Constants . STRUMLINE_X_OFFSET ;
2025-04-17 18:33:58 +00:00
playerStrumline . y = ( FlxG . height - playerStrumline . height ) * 0.95 - Constants . STRUMLINE_Y_OFFSET ;
2025-04-23 03:31:58 +00:00
if ( currentChart ? . noteStyle != " p i x e l " )
2025-01-03 05:19:39 +00:00
{
#if android playerStrumline . y += 10 ; #end
}
2025-01-01 18:50:05 +00:00
e lse
2025-01-03 05:19:39 +00:00
{
2025-01-01 18:50:05 +00:00
playerStrumline . y -= 10 ;
2025-01-03 05:19:39 +00:00
}
2025-04-17 18:33:58 +00:00
opponentStrumline . y = Constants . STRUMLINE_Y_OFFSET * 0.3 ;
2025-01-12 11:07:59 +00:00
opponentStrumline . x -= 30 ;
2025-04-15 19:54:00 +00:00
}
2025-04-23 03:31:58 +00:00
function initPauseSprites ( )
{
pauseButton . animation . addByIndices ( ' i d l e ' , ' b a c k ' , [ 0 ] , " " , 24 , false ) ;
pauseButton . animation . addByIndices ( ' h o l d ' , ' b a c k ' , [ 5 ] , " " , 24 , false ) ;
pauseButton . animation . addByIndices ( ' c o n f i r m ' , ' b a c k ' , [
6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 , 32
] , " " , 24 , false ) ;
pauseButton . scale . set ( 0.8 , 0.8 ) ;
pauseButton . updateHitbox ( ) ;
pauseButton . animation . play ( " i d l e " ) ;
pauseButton . setPosition ( ( FlxG . width - pauseButton . width ) - 35 , 35 ) ;
@ : nullSafety ( Off ) // AAAAAAA why did camControls have to be nullable AAAAAAAAAA
pauseButton . cameras = [ camControls ] ;
pauseCircle . scale . set ( 0.84 , 0.8 ) ;
pauseCircle . updateHitbox ( ) ;
@ : nullSafety ( Off )
pauseCircle . cameras = [ camControls ] ;
pauseCircle . x = ( ( pauseButton . x + ( pauseButton . width / 2 ) ) - ( pauseCircle . width / 2 ) ) ;
pauseCircle . y = ( ( pauseButton . y + ( pauseButton . height / 2 ) ) - ( pauseCircle . height / 2 ) ) ;
pauseCircle . alpha = 0.1 ;
add ( pauseCircle ) ;
add ( pauseButton ) ;
hitbox ? . forEachAlive ( function ( hint : FunkinHint ) {
hint . deadZones . push ( pauseButton ) ;
} ) ;
}
2025-04-15 19:54:00 +00:00
#end
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Configures the judgement and combo popups .
* /
2024-07-28 21:10:32 +00:00
function initPopups ( ) : Void
{
// Initialize the judgements and combo meter.
comboPopUps . zIndex = 900 ;
add ( comboPopUps ) ;
comboPopUps . cameras = [ camHUD ] ;
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Initializes the Discord Rich Presence .
* /
2023-06-22 05:41:01 +00:00
function initDiscord ( ) : Void
2023-01-23 00:55:30 +00:00
{
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2024-09-19 14:03:16 +00:00
// Determine the details strings once and reuse them.
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Updating Discord Rich Presence.
2024-09-19 14:03:16 +00:00
DiscordClient . instance . setPresence (
{
2024-09-19 15:13:05 +00:00
state : buildDiscordRPCState ( ) ,
details : buildDiscordRPCDetails ( ) ,
2024-09-25 07:58:43 +00:00
largeImageKey : discordRPCAlbum ,
2024-09-19 14:03:16 +00:00
smallImageKey : discordRPCIcon
} ) ;
2023-06-22 05:41:01 +00:00
#end
2024-10-03 21:10:38 +00:00
#if FEATURE_DISCORD_RPC
// Updating Discord Rich Presence.
DiscordClient . instance . setPresence (
{
state : buildDiscordRPCState ( ) ,
details : buildDiscordRPCDetails ( ) ,
largeImageKey : discordRPCAlbum ,
smallImageKey : discordRPCIcon
} ) ;
#end
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2024-09-19 15:13:05 +00:00
function buildDiscordRPCDetails ( ) : String
{
if ( PlayStatePlaylist . isStoryMode )
2023-01-23 00:55:30 +00:00
{
2024-09-19 15:13:05 +00:00
return ' S t o r y M o d e : ${ PlayStatePlaylist . campaignTitle } ' ;
2023-01-23 00:55:30 +00:00
}
2024-09-19 15:13:05 +00:00
e lse
{
if ( isChartingMode )
{
return ' C h a r t E d i t o r [ P l a y t e s t ] ' ;
}
e lse if ( isPracticeMode )
{
return ' F r e e p l a y [ P r a c t i c e ] ' ;
}
e lse if ( isBotPlayMode )
{
return ' F r e e p l a y [ B o t P l a y ] ' ;
}
e lse
{
return ' F r e e p l a y ' ;
}
}
}
2023-01-23 00:55:30 +00:00
2024-09-19 15:13:05 +00:00
function buildDiscordRPCState ( ) : String
{
2025-04-23 03:31:58 +00:00
if ( currentChart == null )
{
trace ( " W A R N I N G : D i f f i c u l t y d a t a f o r R P C i s n u l l . " ) ;
}
var discordRPCDifficulty = PlayState . instance ? . currentDifficulty ? . replace ( ' - ' , ' ' ) ? . toTitleCase ( ) ? ? ' ? ? ? ' ;
return ' ${ currentChart ? . songName ? ? ' ? ? ? ' } [ $ { d i s c o r d R P C D i f f i c u l t y } ] ' ;
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
function initPreciseInputs ( ) : Void
{
PreciseInputManager . instance . onInputPressed . add ( onKeyPress ) ;
PreciseInputManager . instance . onInputReleased . add ( onKeyRelease ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Initializes the song ( applying t h e c h a r t , generating t h e n o t e s , etc . )
* Should be done before the countdown starts .
* /
2023-06-22 05:41:01 +00:00
function generateSong ( ) : Void
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
if ( currentChart == null )
2023-06-02 19:35:01 +00:00
{
2025-04-23 03:31:58 +00:00
throw ' S o n g d i f f i c u l t y c o u l d n o t b e l o a d e d . ' ;
2023-06-02 19:35:01 +00:00
}
2023-01-23 00:55:30 +00:00
2023-12-14 21:56:20 +00:00
// Conductor.instance.forceBPM(currentChart.getStartingBPM());
2023-01-23 00:55:30 +00:00
2023-08-04 20:15:07 +00:00
if ( ! overrideMusic )
2023-01-23 00:55:30 +00:00
{
2023-11-09 22:00:46 +00:00
// Stop the vocals if they already exist.
2025-04-23 03:31:58 +00:00
vocals ? . stop ( ) ;
vocals = currentChart ? . buildVocals ( currentInstrumental ) ;
2023-08-04 20:15:07 +00:00
2025-04-23 03:31:58 +00:00
if ( vocals ? . members ? . length == 0 )
2023-08-04 20:15:07 +00:00
{
trace ( ' W A R N I N G : N o v o c a l s f o u n d f o r t h i s s o n g . ' ) ;
}
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
regenNoteData ( ) ;
2023-01-23 00:55:30 +00:00
2023-11-06 22:51:56 +00:00
var event: ScriptEvent = new ScriptEvent ( CREATE , false ) ;
ScriptEventDispatcher . callEvent ( currentSong , event ) ;
2023-06-22 05:41:01 +00:00
generatedMusic = true ;
2023-01-23 00:55:30 +00:00
}
/ * *
2024-08-26 22:01:36 +00:00
* Read note data from the chart and generate the notes .
* /
2023-07-27 00:03:31 +00:00
function regenNoteData ( startTime : Float = 0 ) : Void
2023-01-23 00:55:30 +00:00
{
2025-04-23 03:31:58 +00:00
if ( currentChart == null )
{
trace ( ' C a n n o t r e g e n e r a t e n o t e d a t a f o r n u l l c h a r t ' ) ;
return ;
}
2023-06-22 05:41:01 +00:00
Highscore . tallies . combo = 0 ;
Highscore . tallies = new Tallies ( ) ;
2023-01-23 00:55:30 +00:00
2025-04-23 03:31:58 +00:00
@ : nullSafety ( Off )
2024-05-02 08:11:27 +00:00
var event: SongLoadScriptEvent = new SongLoadScriptEvent ( currentChart . song . id , currentChart . difficulty , currentChart . notes . copy ( ) , currentChart . getEvents ( ) ) ;
dispatchEvent ( event ) ;
var builtNoteData = event . notes ;
var builtEventData = event . events ;
songEvents = builtEventData ;
2024-01-04 02:10:14 +00:00
SongEventRegistry . resetEvents ( songEvents ) ;
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
// Reset the notes on each strumline.
var playerNoteData: Array < SongNoteData > = [ ] ;
var opponentNoteData: Array < SongNoteData > = [ ] ;
2024-05-02 08:11:27 +00:00
for ( songNote in builtNoteData )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
var strumTime: Float = songNote . time ;
2023-07-27 00:03:31 +00:00
if ( strumTime < startTime ) continue ; // Skip notes that are before the start time.
2025-08-01 06:05:11 +00:00
var scoreable = true ;
if ( songNote . kind != null )
{
2025-04-23 03:31:58 +00:00
var noteKind: Null < NoteKind > = NoteKindManager . getNoteKind ( songNote . kind ? ? ' ' ) ;
2025-08-01 06:05:11 +00:00
if ( noteKind != null ) scoreable = noteKind . scoreable ;
}
2023-06-22 05:41:01 +00:00
var noteData: Int = songNote . getDirection ( ) ;
var playerNote: Bool = true ;
if ( noteData > 3 ) playerNote = false ;
switch ( songNote . getStrumlineIndex ( ) )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
c ase 0 :
playerNoteData . push ( songNote ) ;
2024-03-05 01:47:23 +00:00
// increment totalNotes for total possible notes able to be hit by the player
2025-08-01 06:05:11 +00:00
if ( scoreable ) Highscore . tallies . totalNotes ++ ;
2023-06-22 05:41:01 +00:00
c ase 1 :
opponentNoteData . push ( songNote ) ;
2023-01-23 00:55:30 +00:00
}
}
2023-06-22 05:41:01 +00:00
playerStrumline . applyNoteData ( playerNoteData ) ;
opponentStrumline . applyNoteData ( opponentNoteData ) ;
2023-02-22 01:58:15 +00:00
}
2024-03-06 02:48:04 +00:00
function onStrumlineNoteIncoming ( noteSprite : NoteSprite ) : Void
{
var event: NoteScriptEvent = new NoteScriptEvent ( NOTE_INCOMING , noteSprite , 0 , false ) ;
dispatchEvent ( event ) ;
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00: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 .
* /
2023-06-22 05:41:01 +00:00
public function startCountdown ( ) : Void
2023-07-02 20:46:40 +00:00
{
2023-06-22 05:41:01 +00:00
// If Countdown.performCountdown returns false, then the countdown was canceled by a script.
2024-07-13 19:45:58 +00:00
var result: Bool = Countdown . performCountdown ( ) ;
2023-06-22 05:41:01 +00:00
if ( ! result ) return ;
2023-07-02 20:46:40 +00:00
2023-06-22 05:41:01 +00:00
isInCutscene = false ;
2024-04-20 06:11:04 +00:00
// TODO: Maybe tween in the camera after any cutscenes.
2023-06-22 05:41:01 +00:00
camHUD . visible = true ;
2023-07-02 20:46:40 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Displays a dialogue cutscene with the given ID .
* This is used by song scripts to display dialogue .
* /
2023-06-22 05:41:01 +00:00
public function startConversation ( conversationId : String ) : Void
2023-02-22 01:58:15 +00:00
{
2023-06-22 05:41:01 +00:00
isInCutscene = true ;
2023-06-02 19:35:01 +00:00
2024-02-07 14:21:44 +00:00
currentConversation = ConversationRegistry . instance . fetchEntry ( conversationId ) ;
2023-06-22 05:41:01 +00:00
if ( currentConversation == null ) return ;
2024-02-28 08:01:20 +00:00
if ( ! currentConversation . alive ) currentConversation . revive ( ) ;
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
currentConversation . completeCallback = onConversationComplete ;
currentConversation . cameras = [ camCutscene ] ;
currentConversation . zIndex = 1000 ;
add ( currentConversation ) ;
refresh ( ) ;
2023-06-02 19:35:01 +00:00
2023-10-26 09:46:22 +00:00
var event: ScriptEvent = new ScriptEvent ( CREATE , false ) ;
2023-06-22 05:41:01 +00:00
ScriptEventDispatcher . callEvent ( currentConversation , event ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Handler function called w h e n a c o n v e r s a t i o n e n d s .
* /
2023-06-22 05:41:01 +00:00
function onConversationComplete ( ) : Void
2023-01-23 00:55:30 +00:00
{
2023-10-18 01:26:32 +00:00
isInCutscene = false ;
2024-02-28 08:01:20 +00:00
if ( currentConversation != null )
{
currentConversation . kill ( ) ;
remove ( currentConversation ) ;
currentConversation = null ;
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( startingSong && ! isInCountdown )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
startCountdown ( ) ;
2023-01-23 00:55:30 +00:00
}
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Starts playing the song after the countdown has completed .
* /
2023-06-22 05:41:01 +00:00
function startSong ( ) : Void
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
startingSong = false ;
2023-01-23 00:55:30 +00:00
2025-04-14 15:20:15 +00:00
#if mobile
2025-06-29 00:17:45 +00:00
if ( hitbox != null ) hitbox . visible = true ;
2025-04-14 15:20:15 +00:00
#end
2023-08-04 20:15:07 +00:00
if ( ! overrideMusic && ! isGamePaused && currentChart != null )
2023-01-23 00:55:30 +00:00
{
2025-04-23 03:31:58 +00:00
currentChart ? . playInst ( 1.0 , currentInstrumental , false ) ;
2023-01-23 00:55:30 +00:00
}
2024-03-23 21:50:48 +00:00
if ( FlxG . sound . music == null )
{
FlxG . log . error ( ' P l a y S t a t e f a i l e d t o i n i t i a l i z e i n s t r u m e n t a l ! ' ) ;
return ;
}
2024-07-28 05:42:09 +00:00
FlxG . sound . music . onComplete = function ( ) {
2025-03-16 06:38:46 +00:00
if ( mayPauseGame ) endSong ( skipEndingTransition ) ;
2024-07-28 05:42:09 +00:00
} ;
2025-05-03 04:48:21 +00:00
2024-10-19 08:21:50 +00:00
FlxG . sound . music . pause ( ) ;
2025-05-03 04:48:21 +00:00
FlxG . sound . music . time = startTimestamp ;
2024-02-29 23:49:20 +00:00
FlxG . sound . music . pitch = playbackRate ;
2024-02-28 08:01:20 +00:00
2024-03-27 03:24:59 +00:00
// Prevent the volume from being wrong.
2024-02-28 08:01:20 +00:00
FlxG . sound . music . volume = 1.0 ;
2024-03-08 05:21:51 +00:00
if ( FlxG . sound . music . fadeTween != null ) FlxG . sound . music . fadeTween . cancel ( ) ;
2023-12-05 07:44:57 +00:00
2025-04-23 03:31:58 +00:00
if ( vocals != null )
{
trace ( ' P l a y i n g v o c a l s . . . ' ) ;
add ( vocals ) ;
vocals . time = startTimestamp - Conductor . instance . instrumentalOffset ;
vocals . pitch = playbackRate ;
vocals . volume = 1.0 ;
2024-10-19 08:21:50 +00:00
2025-04-23 03:31:58 +00:00
// trace('STARTING SONG AT:');
// trace('${FlxG.sound.music.time}');
// trace('${vocals.time}');
2025-05-03 04:48:21 +00:00
2025-04-23 03:31:58 +00:00
vocals . play ( ) ;
}
2024-10-19 08:21:50 +00:00
FlxG . sound . music . play ( ) ;
2023-01-23 00:55:30 +00:00
2024-08-26 22:01:36 +00:00
#if FEATURE_DISCORD_RPC
2023-06-22 05:41:01 +00:00
// Updating Discord Rich Presence (with Time Left)
2024-09-19 14:03:16 +00:00
DiscordClient . instance . setPresence (
{
2024-09-19 15:13:05 +00:00
state : buildDiscordRPCState ( ) ,
details : buildDiscordRPCDetails ( ) ,
2024-09-25 07:58:43 +00:00
largeImageKey : discordRPCAlbum ,
2024-09-19 14:03:16 +00:00
smallImageKey : discordRPCIcon
} ) ;
// DiscordClient.changePresence(detailsText, '${currentChart.songName} ($discordRPCDifficulty)', discordRPCIcon, true, currentSongLengthMs);
2023-06-22 05:41:01 +00:00
#end
2023-07-27 00:03:31 +00:00
if ( startTimestamp > 0 )
{
2024-09-21 10:33:47 +00:00
// FlxG.sound.music.time = startTimestamp - Conductor.instance.combinedOffset;
2023-07-27 00:03:31 +00:00
handleSkippedNotes ( ) ;
}
2024-02-23 09:00:31 +00:00
dispatchEvent ( new ScriptEvent ( SONG_START ) ) ;
2025-03-27 06:55:45 +00:00
#if FEATURE_NEWGROUNDS
Events . logStartSong ( currentSong . id , currentVariation ) ;
#end
2025-06-26 05:17:19 +00:00
resyncVocals ( ) ;
2023-01-23 00:55:30 +00:00
}
/ * *
2025-06-06 04:11:55 +00:00
* Resynchronize the vocal tracks if t h e y h a v e b e c o m e o f f s e t f r o m t h e i n s t r u m e n t a l .
2024-08-26 22:01:36 +00:00
* /
2023-06-22 05:41:01 +00:00
function resyncVocals ( ) : Void
2023-01-23 00:55:30 +00:00
{
2024-02-13 07:09:25 +00:00
if ( vocals == null ) return ;
2023-01-23 00:55:30 +00:00
2023-12-05 07:44:57 +00:00
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
2024-08-28 09:40:50 +00:00
if ( ! ( FlxG . sound . music ? . playing ? ? false ) ) r e t u r n ;
2023-08-02 22:08:49 +00:00
2025-05-02 05:24:18 +00:00
var timeToPlayAt: Float = Math . min ( FlxG . sound . music . length ,
Math . max ( Math . min ( Conductor . instance . combinedOffset , 0 ) , Conductor . instance . songPosition ) - Conductor . instance . combinedOffset ) ;
2024-09-19 07:02:59 +00:00
trace ( ' R e s y n c i n g v o c a l s t o ${ timeToPlayAt } ' ) ;
2024-10-19 08:21:50 +00:00
2024-07-23 18:02:09 +00:00
FlxG . sound . music . pause ( ) ;
2023-06-22 05:41:01 +00:00
vocals . pause ( ) ;
2023-06-02 19:35:01 +00:00
2024-07-23 18:02:09 +00:00
FlxG . sound . music . time = timeToPlayAt ;
FlxG . sound . music . play ( false , timeToPlayAt ) ;
2023-06-02 19:35:01 +00:00
2024-07-23 18:02:09 +00:00
vocals . time = timeToPlayAt ;
vocals . play ( false , timeToPlayAt ) ;
2023-06-22 05:41:01 +00:00
}
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Updates the position and contents of the score display .
* /
2023-06-22 05:41:01 +00:00
function updateScoreText ( ) : Void
{
// TODO: Add functionality for modules to update the score text.
2024-03-06 03:27:07 +00:00
if ( isBotPlayMode )
{
scoreText . text = ' B o t P l a y E n a b l e d ' ;
}
e lse
{
2024-07-10 18:23:50 +00:00
// TODO: Add an option for this maybe?
var commaSeparated: Bool = true ;
scoreText . text = ' S c o r e : ${ FlxStringUtil . formatMoney ( songScore , false , commaSeparated ) } ' ;
2024-03-06 03:27:07 +00:00
}
2023-06-22 05:41:01 +00:00
}
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Updates the values of the health bar .
* /
2023-06-22 05:41:01 +00:00
function updateHealthBar ( ) : Void
{
2024-03-06 03:27:07 +00:00
if ( isBotPlayMode )
{
healthLerp = Constants . HEALTH_MAX ;
}
e lse
{
healthLerp = FlxMath . lerp ( healthLerp , health , 0.15 ) ;
}
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Callback executed when one of the note keys is pressed .
* /
2023-06-22 05:41:01 +00:00
function onKeyPress ( event : PreciseInputEvent ) : Void
2023-01-23 00:55:30 +00:00
{
2023-08-02 22:08:49 +00:00
if ( isGamePaused ) return ;
2023-06-22 05:41:01 +00:00
// Do the minimal possible work here.
inputPressQueue . push ( event ) ;
}
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Callback executed when one of the note keys is released .
* /
2023-06-22 05:41:01 +00:00
function onKeyRelease ( event : PreciseInputEvent ) : Void
{
2023-08-02 22:08:49 +00:00
if ( isGamePaused ) return ;
2023-06-22 05:41:01 +00:00
// Do the minimal possible work here.
inputReleaseQueue . push ( event ) ;
2023-06-02 19:35:01 +00:00
}
/ * *
2024-08-26 22:01:36 +00:00
* Handles opponent note hits and player note misses .
* /
2023-06-27 21:22:51 +00:00
function processNotes ( elapsed : Float ) : Void
2023-06-02 19:35:01 +00:00
{
2025-04-23 03:31:58 +00:00
if ( playerStrumline . notes ? . members == null || opponentStrumline . notes ? . members == null ) r e t u r n ;
2023-02-22 19:46:46 +00:00
2023-06-22 05:41:01 +00:00
// Process notes on the opponent's side.
for ( note in opponentStrumline . notes . members )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
if ( note == null ) continue ;
2025-06-18 03:23:09 +00:00
var r = GRhythmUtil . processWindow ( note , false ) ;
if ( r . botplayHit )
2023-01-23 00:55:30 +00:00
{
2024-05-08 12:26:17 +00:00
var event: NoteScriptEvent = new HitNoteScriptEvent ( note , 0.0 , 0 , ' p e r f e c t ' , false , 0 ) ;
2023-06-22 05:41:01 +00:00
dispatchEvent ( event ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Calling event.cancelEvent() skips all the other logic! Neat!
if ( event . eventCanceled ) continue ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00: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-06 02:11:58 +00:00
if ( note . holdNoteSprite != null )
2023-01-23 00:55:30 +00:00
{
2023-07-06 02:11:58 +00:00
opponentStrumline . playNoteHoldCover ( note . holdNoteSprite ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-02 19:35:01 +00:00
}
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2023-07-08 05:03:46 +00:00
// Process hold notes on the opponent's side.
for ( holdNote in opponentStrumline . holdNotes . members )
{
2025-04-23 03:31:58 +00:00
if ( holdNote == null || ! holdNote . alive || holdNote . noteData == null ) continue ;
2023-06-02 19:35:01 +00:00
2023-07-08 05:03:46 +00:00
// 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-23 00:55:30 +00:00
{
2023-07-08 05:03:46 +00:00
currentStage . getDad ( ) . holdTimer = 0 ;
2023-01-23 00:55:30 +00:00
}
}
2024-01-15 19:20:44 +00:00
if ( holdNote . missedNote && ! holdNote . handledMiss )
{
2024-01-16 03:10:42 +00:00
// When the opponent drops a hold note.
2024-01-15 19:20:44 +00:00
holdNote . handledMiss = true ;
2024-01-16 03:10:42 +00:00
// We dropped a hold note.
2024-03-23 22:11:06 +00:00
// Play miss animation, but don't penalize.
2025-04-23 03:31:58 +00:00
if ( currentStage != null ) currentStage . getOpponent ( ) . playSingAnimation ( holdNote . noteData . getDirection ( ) , true ) ;
2024-01-15 19:20:44 +00:00
}
2023-06-02 19:35:01 +00:00
}
2023-07-08 05:03:46 +00:00
2023-06-22 05:41:01 +00:00
// Process notes on the player's side.
for ( note in playerStrumline . notes . members )
2023-06-02 19:35:01 +00:00
{
2024-01-16 03:10:42 +00:00
if ( note == null ) continue ;
2025-06-18 03:23:09 +00:00
var r = GRhythmUtil . processWindow ( note , ! isBotPlayMode ) ;
if ( r . botplayHit )
2024-01-16 03:10:42 +00:00
{
2024-03-06 03:27:07 +00:00
// We call onHitNote to play the proper animations,
// but not goodNoteHit! This means zero score and zero notes hit for the results screen!
// Call an event to allow canceling the note hit.
// NOTE: This is what handles the character animations!
2024-05-08 12:26:17 +00:00
var event: NoteScriptEvent = new HitNoteScriptEvent ( note , 0.0 , 0 , ' p e r f e c t ' , false , 0 ) ;
2024-03-06 03:27:07 +00:00
dispatchEvent ( event ) ;
// Calling event.cancelEvent() skips all the other logic! Neat!
if ( event . eventCanceled ) continue ;
// Command the bot to hit the note on time.
// NOTE: This is what handles the strumline and cleaning up the note itself!
playerStrumline . hitNote ( note ) ;
if ( note . holdNoteSprite != null )
{
playerStrumline . playNoteHoldCover ( note . holdNoteSprite ) ;
}
2023-06-02 19:35:01 +00:00
}
2025-06-18 03:23:09 +00:00
if ( ! r . cont ) continue ;
2023-01-23 00:55:30 +00:00
2023-06-27 17:43:42 +00: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 05:41:01 +00:00
{
// Call an event to allow canceling the note miss.
// NOTE: This is what handles the character animations!
2025-05-03 21:33:59 +00:00
var event: NoteScriptEvent = new NoteScriptEvent ( NOTE_MISS , note , Constants . HEALTH_MISS_PENALTY , Highscore . tallies . combo , true ) ;
2023-06-22 05:41:01 +00:00
dispatchEvent ( event ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Calling event.cancelEvent() skips all the other logic! Neat!
if ( event . eventCanceled ) continue ;
2023-01-23 00:55:30 +00:00
2024-08-28 09:47:34 +00:00
// Skip handling the miss in botplay!
if ( ! isBotPlayMode )
{
2024-09-05 05:22:45 +00:00
// Judge the miss.
// NOTE: This is what handles the scoring.
2025-06-27 23:29:53 +00:00
// trace('Missed note! ${note.noteData}');
2024-09-05 05:22:45 +00:00
onNoteMiss ( note , event . playSound , event . healthChange ) ;
2024-08-28 09:47:34 +00:00
}
2023-06-27 17:43:42 +00:00
note . handledMiss = true ;
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
}
2023-06-27 00:40:26 +00: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 21:22:51 +00:00
for ( holdNote in playerStrumline . holdNotes . members )
2023-01-23 00:55:30 +00:00
{
2023-06-27 22:06:33 +00:00
if ( holdNote == null || ! holdNote . alive ) continue ;
2023-06-27 21:22:51 +00:00
// While the hold note is being hit, and there is length on the hold note...
2024-06-07 23:59:02 +00:00
if ( holdNote . hitNote && ! holdNote . missedNote && holdNote . sustainLength > 0 )
2023-06-27 21:22:51 +00:00
{
// Grant the player health.
2024-06-07 23:59:02 +00:00
if ( ! isBotPlayMode )
{
health += Constants . HEALTH_HOLD_BONUS_PER_SECOND * elapsed ;
songScore += Std . int ( Constants . SCORE_HOLD_BONUS_PER_SECOND * elapsed ) ;
}
2024-06-11 04:40:43 +00:00
2024-06-07 23:59:02 +00:00
// Make sure the player keeps singing while the note is held by the bot.
if ( isBotPlayMode && currentStage != null && currentStage . getBoyfriend ( ) != null && currentStage . getBoyfriend ( ) . isSinging ( ) )
{
currentStage . getBoyfriend ( ) . holdTimer = 0 ;
}
2023-06-27 21:22:51 +00:00
}
2023-07-08 05:03:46 +00:00
2024-01-16 03:10:42 +00:00
if ( holdNote . missedNote && ! holdNote . handledMiss )
{
// The player dropped a hold note.
holdNote . handledMiss = true ;
2025-03-20 07:19:29 +00:00
// Mute vocals and play miss animation.
2024-04-03 08:52:28 +00:00
// vocals.playerVolume = 0;
// if (currentStage != null && currentStage.getBoyfriend() != null) currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
2025-03-20 07:19:29 +00:00
if ( ! isBotPlayMode )
{
if ( holdNote . sustainLength > Constants . HOLD_DROP_PENALTY_THRESHOLD_MS )
{
// Penalize the player for letting go of a hold note too early.
trace ( ' P l a y e r d r o p p e d a h o l d n o t e , p e n a l i z i n g . . . ( h a s h i t : ${ holdNote . hitNote } ) ' ) ;
// Different penalty based on whether the note itself was missed,
// or the note was hit and then the hold was dropped.
var remainingLengthSec = holdNote . sustainLength / Constants . MS_PER_SEC ;
var healthChangeUncapped = remainingLengthSec * Constants . HEALTH_HOLD_DROP_PENALTY_PER_SECOND ;
// If the base note of the hold was missed, don't penalize them more on top of that.
var healthChangeMax = Constants . HEALTH_HOLD_DROP_PENALTY_MAX - ( holdNote . hitNote ? - Constants . HEALTH_MISS_PENALTY : 0 ) ;
2025-03-20 07:35:23 +00:00
var healthChange = healthChangeUncapped . clamp ( healthChangeMax , 0 ) ;
2025-03-20 07:19:29 +00:00
var scoreChange = Std . int ( Constants . SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec ) ;
2025-05-03 21:33:59 +00:00
var event: HoldNoteScriptEvent = new HoldNoteScriptEvent ( NOTE_HOLD_DROP , holdNote , healthChange , scoreChange , true , Highscore . tallies . combo ) ;
2025-03-20 07:19:29 +00:00
dispatchEvent ( event ) ;
2025-05-29 07:00:46 +00:00
// Calling event.cancelEvent() skips all the other logic! Neat!
if ( event . eventCanceled ) continue ;
2025-03-20 07:19:29 +00:00
trace ( ' P e n a l i z i n g s c o r e b y ${ event . score } a n d h e a l t h b y ${ event . healthChange } f o r d r o p p i n g h o l d n o t e ( i s c o m b o b r e a k : ${ event . isComboBreak } ) ! ' ) ;
2025-04-26 16:29:55 +00:00
applyScore ( event . score , ' ' , event . healthChange , event . isComboBreak ) ;
2025-03-20 07:19:29 +00:00
// Play the miss sound.
2025-05-29 07:00:46 +00:00
if ( event . playSound )
{
2025-08-05 15:33:47 +00:00
if ( vocals != null ) vocals . playerVolume = 0 ;
FunkinSound . playOnce ( Paths . soundRandom ( ' m i s s n o t e ' , 1 , 3 ) , FlxG . random . float ( 0.5 , 0.6 ) ) ;
2025-05-29 07:00:46 +00:00
}
2025-03-20 07:19:29 +00:00
}
e lse
{
trace ( ' H o l d n o t e t o o s h o r t , n o t p e n a l i z i n g . . . ' ) ;
}
}
2024-01-16 03:10:42 +00:00
}
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2023-07-27 00:03:31 +00:00
function handleSkippedNotes ( ) : Void
{
for ( note in playerStrumline . notes . members )
{
2023-07-27 01:34:38 +00:00
if ( note == null || note . hasBeenHit ) continue ;
2023-07-27 00:03:31 +00:00
var hitWindowEnd = note . strumTime + Constants . HIT_WINDOW_MS ;
2023-12-14 21:56:20 +00:00
if ( Conductor . instance . songPosition > hitWindowEnd )
2023-07-27 00:03:31 +00:00
{
// We have passed this note.
// Flag the note for deletion without actually penalizing the player.
note . handledMiss = true ;
}
}
2023-07-27 01:34:38 +00:00
2025-06-06 04:11:55 +00:00
// Respawns notes that were between the previous time and the current time when skipping backward, or destroy notes between the previous time and the current time when skipping forward.
2023-07-27 01:34:38 +00:00
playerStrumline . handleSkippedNotes ( ) ;
opponentStrumline . handleSkippedNotes ( ) ;
2023-07-27 00:03:31 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* PreciseInputEvents are put into a queue between update ( ) calls ,
* and then processed here .
* /
2023-06-22 05:41:01 +00:00
function processInputQueue ( ) : Void
{
if ( inputPressQueue . length + inputReleaseQueue . length == 0 ) return ;
// Ignore inputs during cutscenes.
if ( isInCutscene || disableKeys )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
inputPressQueue = [ ] ;
inputReleaseQueue = [ ] ;
return ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
// Generate a list of notes within range.
2023-06-27 17:43:42 +00:00
var notesInRange: Array < NoteSprite > = playerStrumline . getNotesMayHit ( ) ;
2023-06-22 05:41:01 +00:00
var notesByDirection: Array < Array < NoteSprite > > = [ [ ] , [ ] , [ ] , [ ] ] ;
for ( note in notesInRange )
notesByDirection [ note . direction ] . push ( note ) ;
while ( inputPressQueue . length > 0 )
2023-03-02 09:20:03 +00:00
{
2025-04-23 03:31:58 +00:00
var input: Null < PreciseInputEvent > = inputPressQueue . shift ( ) ;
if ( input == null ) continue ;
2023-03-02 09:20:03 +00:00
2023-06-27 00:40:26 +00:00
playerStrumline . pressKey ( input . noteDirection ) ;
2023-06-02 19:35:01 +00:00
2024-08-28 09:47:34 +00:00
// Don't credit or penalize inputs in Bot Play.
if ( isBotPlayMode ) continue ;
2023-06-22 05:41:01 +00:00
var notesInDirection: Array < NoteSprite > = notesByDirection [ input . noteDirection ] ;
2023-06-02 19:35:01 +00:00
2024-08-28 09:42:14 +00:00
#if FEATURE_GHOST_TAPPING
if ( ( ! playerStrumline . mayGhostTap ( ) ) && notesInDirection . length == 0 )
#else
if ( notesInDirection . length == 0 )
#end
2023-03-18 19:47:23 +00:00
{
2023-06-27 17:43:42 +00:00
// 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 ) ;
2024-05-19 05:47:36 +00:00
trace ( ' P E N A L T Y S c o r e : ${ songScore } ' ) ;
2023-03-18 19:47:23 +00:00
}
2024-09-05 05:22:45 +00:00
e lse if ( notesInDirection . length == 0 )
{
// Press a key with no penalty.
2023-06-22 05:41:01 +00:00
2024-09-05 05:22:45 +00:00
// Play the strumline animation.
playerStrumline . playPress ( input . noteDirection ) ;
trace ( ' N O P E N A L T Y S c o r e : ${ songScore } ' ) ;
}
e lse
{
// 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 ;
2023-06-22 05:41:01 +00:00
2024-09-05 05:22:45 +00:00
// Judge and hit the note.
2024-09-22 00:33:51 +00:00
// trace('Hit note! ${targetNote.noteData}');
2024-09-05 05:22:45 +00:00
goodNoteHit ( targetNote , input ) ;
2024-09-22 00:33:51 +00:00
// trace('Score: ${songScore}');
2023-06-22 05:41:01 +00:00
2024-09-05 05:22:45 +00:00
notesInDirection . remove ( targetNote ) ;
2023-06-22 05:41:01 +00:00
2024-09-05 05:22:45 +00:00
// Play the strumline animation.
playerStrumline . playConfirm ( input . noteDirection ) ;
}
2023-03-02 09:20:03 +00:00
}
2023-06-22 05:41:01 +00:00
while ( inputReleaseQueue . length > 0 )
{
2025-04-23 03:31:58 +00:00
var input: Null < PreciseInputEvent > = inputReleaseQueue . shift ( ) ;
if ( input == null ) continue ;
2023-06-22 05:41:01 +00:00
// Play the strumline animation.
playerStrumline . playStatic ( input . noteDirection ) ;
2023-06-27 00:40:26 +00:00
playerStrumline . releaseKey ( input . noteDirection ) ;
2023-06-22 05:41:01 +00:00
}
2025-05-10 14:04:57 +00:00
playerStrumline . noteVibrations . tryNoteVibration ( ) ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
function goodNoteHit ( note : NoteSprite , input : PreciseInputEvent ) : Void
2023-06-02 19:35:01 +00:00
{
2024-03-01 13:13:06 +00:00
// 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.
2025-06-26 01:16:33 +00:00
var diff: Float = Conductor . instance . songPosition - note . noteData . time ;
var totalDiff: Float = diff ;
if ( diff < 0 ) totalDiff = diff + inputLatencyMs ;
e lse
totalDiff = diff - inputLatencyMs ;
var noteDiff: Int = Std . int ( totalDiff ) ;
2024-03-01 13:13:06 +00:00
var score = Scoring . scoreNote ( noteDiff , PBOT1 ) ;
var daRating = Scoring . judgeNote ( noteDiff , PBOT1 ) ;
var healthChange = 0.0 ;
2024-05-08 12:26:17 +00:00
var isComboBreak = false ;
2024-03-01 13:13:06 +00:00
switch ( daRating )
{
c ase ' s i c k ' :
healthChange = Constants . HEALTH_SICK_BONUS ;
2024-05-08 12:26:17 +00:00
isComboBreak = Constants . JUDGEMENT_SICK_COMBO_BREAK ;
2024-03-01 13:13:06 +00:00
c ase ' g o o d ' :
healthChange = Constants . HEALTH_GOOD_BONUS ;
2024-05-08 12:26:17 +00:00
isComboBreak = Constants . JUDGEMENT_GOOD_COMBO_BREAK ;
2024-03-01 13:13:06 +00:00
c ase ' b a d ' :
healthChange = Constants . HEALTH_BAD_BONUS ;
2024-05-08 12:26:17 +00:00
isComboBreak = Constants . JUDGEMENT_BAD_COMBO_BREAK ;
2024-03-01 13:13:06 +00:00
c ase ' s h i t ' :
healthChange = Constants . HEALTH_SHIT_BONUS ;
2024-10-04 11:58:21 +00:00
isComboBreak = Constants . JUDGEMENT_SHIT_COMBO_BREAK ;
2024-03-01 13:13:06 +00:00
}
// Send the note hit event.
2025-08-01 06:05:11 +00:00
var event: HitNoteScriptEvent = new HitNoteScriptEvent ( note , healthChange , score , daRating , isComboBreak ,
2025-04-23 03:31:58 +00:00
note . scoreable ? Highscore . tallies . combo + 1 : Highscore . tallies . combo , noteDiff , daRating == ' s i c k ' ) ;
2023-07-08 05:03:46 +00:00
dispatchEvent ( event ) ;
2023-06-02 19:35:01 +00:00
2023-07-08 05:03:46 +00:00
// Calling event.cancelEvent() skips all the other logic! Neat!
if ( event . eventCanceled ) return ;
2024-05-08 12:26:17 +00:00
// Display the hit on the strums
2025-01-17 00:41:04 +00:00
playerStrumline . hitNote ( note , ! event . isComboBreak ) ;
2024-05-15 12:46:08 +00:00
if ( event . doesNotesplash ) playerStrumline . playNoteSplash ( note . noteData . getDirection ( ) ) ;
2024-05-08 12:26:17 +00:00
if ( note . isHoldNote && note . holdNoteSprite != null ) playerStrumline . playNoteHoldCover ( note . holdNoteSprite ) ;
2025-04-23 03:31:58 +00:00
if ( vocals != null ) vocals . playerVolume = 1 ;
2024-05-08 12:26:17 +00:00
2024-03-01 13:13:06 +00:00
// Display the combo meter and add the calculation to the score.
2025-08-01 06:05:11 +00:00
if ( note . scoreable )
{
Highscore . tallies . totalNotesHit ++ ;
applyScore ( event . score , event . judgement , event . healthChange , event . isComboBreak ) ;
popUpScore ( event . judgement ) ;
}
2023-01-23 00:55:30 +00:00
}
2023-06-02 19:35:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Called when a note leaves the screen and is considered missed by the player .
* @ param note
* /
2024-03-01 13:13:06 +00:00
function onNoteMiss ( note : NoteSprite , playSound : Bool = false , healthChange : Float ) : Void
2023-06-02 19:35:01 +00:00
{
2024-02-15 22:25:28 +00:00
// If we are here, we already CALLED the onNoteMiss script hook!
2023-06-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
if ( ! isPracticeMode )
2023-06-02 19:35:01 +00:00
{
2023-06-22 05:41:01 +00: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-02 19:35:01 +00:00
2023-06-22 05:41:01 +00:00
var indices: Array < Int > = [ ] ;
for ( i in 0 ... pressArray . length )
{
if ( pressArray [ i ] ) indices . push ( i ) ;
}
}
2023-06-02 19:35:01 +00:00
2025-03-20 03:24:01 +00:00
applyScore ( Scoring . getMissScore ( ) , ' m i s s ' , healthChange , true ) ;
2023-06-02 19:35:01 +00:00
2024-02-15 22:25:28 +00:00
if ( playSound )
2023-06-22 05:41:01 +00:00
{
2025-04-23 03:31:58 +00:00
if ( vocals != null ) vocals . playerVolume = 0 ;
2024-03-23 21:50:48 +00:00
FunkinSound . playOnce ( Paths . soundRandom ( ' m i s s n o t e ' , 1 , 3 ) , FlxG . random . float ( 0.5 , 0.6 ) ) ;
2023-06-22 05:41:01 +00:00
}
2023-06-02 19:35:01 +00:00
}
2023-01-23 00:55:30 +00:00
/ * *
2024-08-26 22:01:36 +00: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 .
*
* @ param direction
* @ param hasPossibleNotes
* /
2023-06-22 05:41:01 +00:00
function ghostNoteMiss ( direction : NoteDirection , hasPossibleNotes : Bool = true ) : Void
2023-01-23 00:55:30 +00:00
{
var event: GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent ( direction , // Direction missed in.
hasPossibleNotes , // Whether there was a note you could have hit.
2025-03-20 03:57:21 +00:00
Constants . HEALTH_GHOST_MISS_PENALTY , // How much health to add (negative).
2023-01-23 00:55:30 +00:00
- 10 // Amount of score to add (negative).
) ;
dispatchEvent ( event ) ;
// Calling event.cancelEvent() skips animations and penalties. Neat!
2023-01-23 03:25:45 +00:00
if ( event . eventCanceled ) return ;
2023-01-23 00:55:30 +00:00
health += event . healthChange ;
2023-07-27 00:03:31 +00:00
songScore += event . scoreChange ;
2023-01-23 00:55:30 +00:00
2023-03-02 09:20:03 +00:00
if ( ! isPracticeMode )
{
var pressArray: Array < Bool > = [
controls . NOTE_LEFT_P ,
controls . NOTE_DOWN_P ,
controls . NOTE_UP_P ,
controls . NOTE_RIGHT_P
] ;
2023-06-02 19:35:01 +00:00
2023-03-18 19:47:23 +00:00
var indices: Array < Int > = [ ] ;
for ( i in 0 ... pressArray . length )
{
if ( pressArray [ i ] ) indices . push ( i ) ;
}
2023-03-02 09:20:03 +00:00
}
2023-01-23 00:55:30 +00:00
if ( event . playSound )
{
2025-04-23 03:31:58 +00:00
if ( vocals != null ) vocals . playerVolume = 0 ;
2024-03-23 21:50:48 +00:00
FunkinSound . playOnce ( Paths . soundRandom ( ' m i s s n o t e ' , 1 , 3 ) , FlxG . random . float ( 0.1 , 0.2 ) ) ;
2023-01-23 00:55:30 +00:00
}
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Debug keys . Disabled while i n c u t s c e n e s .
* /
2023-06-22 05:41:01 +00:00
function debugKeyShit ( ) : Void
2023-01-23 00:55:30 +00:00
{
2024-09-25 12:39:57 +00:00
#if FEATURE_STAGE_EDITOR
2023-11-16 05:02:42 +00:00
// Open the stage editor overlaying the current state.
if ( controls . DEBUG_STAGE )
2023-06-22 05:41:01 +00:00
{
// hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!!
disableKeys = true ;
persistentUpdate = false ;
openSubState ( new StageOffsetSubState ( ) ) ;
}
2024-09-25 12:39:57 +00:00
#end
2023-06-22 05:41:01 +00:00
2024-09-25 12:39:57 +00:00
#if FEATURE_CHART_EDITOR
2023-11-16 05:02:42 +00:00
// Redirect to the chart editor playing the current song.
if ( controls . DEBUG_CHART )
{
disableKeys = true ;
persistentUpdate = false ;
2024-06-10 14:53:32 +00:00
if ( isChartingMode )
{
2024-09-25 12:39:57 +00:00
// Close the playtest substate.
2024-06-10 14:53:32 +00:00
FlxG . sound . music ? . pause ( ) ;
this . close ( ) ;
}
e lse
{
2025-04-23 03:31:58 +00:00
if ( currentStage != null ) this . remove ( currentStage ) ;
2024-06-10 14:53:32 +00:00
FlxG . switchState ( ( ) - > new ChartEditorState (
{
targetSongId : currentSong . id ,
2025-02-08 10:39:13 +00:00
targetSongDifficulty : currentDifficulty ,
targetSongVariation : currentVariation ,
2024-06-10 14:53:32 +00:00
} ) ) ;
}
2023-11-16 05:02:42 +00:00
}
2024-04-24 20:00:50 +00:00
#end
2023-11-16 05:02:42 +00:00
2025-02-18 16:31:06 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2024-07-19 03:27:41 +00:00
// H: Hide the HUD.
if ( FlxG . keys . justPressed . H ) camHUD . visible = ! camHUD . visible ;
2023-06-22 05:41:01 +00:00
// 1: End the song immediately.
2024-02-27 00:03:04 +00:00
if ( FlxG . keys . justPressed . ONE ) endSong ( true ) ;
2023-06-22 05:41:01 +00:00
// 2: Gain 10% health.
2023-06-27 17:43:42 +00:00
if ( FlxG . keys . justPressed . TWO ) health += 0.1 * Constants . HEALTH_MAX ;
2023-06-22 05:41:01 +00:00
// 3: Lose 5% health.
2023-06-27 17:43:42 +00:00
if ( FlxG . keys . justPressed . THREE ) health -= 0.05 * Constants . HEALTH_MAX ;
2025-02-18 16:31:06 +00:00
#end
2023-06-22 05:41:01 +00:00
// 9: Toggle the old icon.
2025-07-16 19:22:17 +00:00
if ( ( FlxG . keys . justPressed . NINE #if FEATURE_TOUCH_CONTROLS || ( TouchUtil . justPressed && TouchUtil . overlapsComplex ( iconP1 ) ) #end )
& & iconP1 != null ) iconP1 . toggleOldIcon ( ) ;
2023-06-22 05:41:01 +00:00
2025-02-18 16:31:06 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-07-27 01:34:38 +00:00
// 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 ) ;
2025-02-18 16:31:06 +00:00
#end
2023-06-22 05:41:01 +00:00
}
/ * *
2024-08-26 22:01:36 +00:00
* Handles applying health , score , and ratings .
* /
2024-05-08 12:26:17 +00:00
function applyScore ( score : Int , daRating : String , healthChange : Float , isComboBreak : Bool )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
switch ( daRating )
{
c ase ' s i c k ' :
Highscore . tallies . sick += 1 ;
c ase ' g o o d ' :
Highscore . tallies . good += 1 ;
c ase ' b a d ' :
Highscore . tallies . bad += 1 ;
c ase ' s h i t ' :
Highscore . tallies . shit += 1 ;
2024-05-08 12:26:17 +00:00
c ase ' m i s s ' :
Highscore . tallies . missed += 1 ;
2025-04-26 16:29:55 +00:00
d efault :
// Nothing!
2023-06-22 05:41:01 +00:00
}
2024-03-01 13:13:06 +00:00
health += healthChange ;
2024-01-16 03:10:42 +00:00
if ( isComboBreak )
{
// Break the combo, but don't increment tallies.misses.
2024-03-28 04:19:57 +00:00
if ( Highscore . tallies . combo >= 10 ) comboPopUps . displayCombo ( 0 ) ;
Highscore . tallies . combo = 0 ;
2023-06-22 05:41:01 +00:00
}
2024-01-16 03:10:42 +00:00
e lse
{
Highscore . tallies . combo ++ ;
if ( Highscore . tallies . combo > Highscore . tallies . maxCombo ) Highscore . tallies . maxCombo = Highscore . tallies . combo ;
}
2024-05-08 12:26:17 +00:00
songScore += score ;
}
2024-01-16 03:10:42 +00:00
2024-05-08 12:26:17 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Handles rating popups when a note is hit .
* /
2024-05-08 12:26:17 +00:00
function popUpScore ( daRating : String , ? combo : Int ) : Void
{
if ( daRating == ' m i s s ' )
2023-06-22 05:41:01 +00:00
{
2024-05-08 12:26:17 +00:00
// If daRating is 'miss', that means we made a mistake and should not continue.
FlxG . log . warn ( ' p o p U p S c o r e j u d g e d a n o t e a s a m i s s ! ' ) ;
// TODO: Remove this.
// comboPopUps.displayRating('miss');
return ;
2023-06-22 05:41:01 +00:00
}
2024-05-08 12:26:17 +00:00
if ( combo == null ) combo = Highscore . tallies . combo ;
2023-07-27 00:03:31 +00:00
2023-03-02 09:20:03 +00:00
if ( ! isPracticeMode )
{
2023-06-22 05:41:01 +00:00
// TODO: Input splitter uses old input system, make it pull from the precise input queue directly.
2023-03-02 09:20:03 +00:00
var pressArray: Array < Bool > = [
controls . NOTE_LEFT_P ,
controls . NOTE_DOWN_P ,
controls . NOTE_UP_P ,
controls . NOTE_RIGHT_P
] ;
2023-06-02 19:35:01 +00:00
2023-03-18 19:47:23 +00:00
var indices: Array < Int > = [ ] ;
for ( i in 0 ... pressArray . length )
{
if ( pressArray [ i ] ) indices . push ( i ) ;
}
2023-03-02 09:20:03 +00:00
}
2023-06-22 05:41:01 +00:00
comboPopUps . displayRating ( daRating ) ;
2024-10-04 11:58:21 +00:00
if ( combo >= 10 ) comboPopUps . displayCombo ( combo ) ;
2024-04-16 00:36:38 +00:00
2025-04-23 03:31:58 +00:00
if ( vocals != null ) vocals . playerVolume = 1 ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Handle keyboard inputs during cutscenes .
* This includes advancing conversations and skipping videos .
* @ param elapsed Time elapsed since last game update .
* /
2023-06-22 05:41:01 +00:00
function handleCutsceneKeys ( elapsed : Float ) : Void
2023-01-23 00:55:30 +00:00
{
2024-02-28 05:19:08 +00:00
if ( isGamePaused ) return ;
2025-04-14 15:20:15 +00:00
var pauseButtonCheck: Bool = false ;
var androidPause: Bool = false ;
#if android
androidPause = FlxG . android . justPressed . BACK ;
#end
#if mobile
2025-05-09 10:28:31 +00:00
pauseButtonCheck = TouchUtil . pressAction ( pauseButton ) ;
2025-04-14 15:20:15 +00:00
#end
2023-06-22 05:41:01 +00:00
if ( currentConversation != null )
2023-01-23 00:55:30 +00:00
{
2024-02-28 05:19:08 +00:00
// Pause/unpause may conflict with advancing the conversation!
2025-04-14 15:20:15 +00:00
if ( ( controls . CUTSCENE_ADVANCE #if mobile || ( ! pauseButtonCheck && TouchUtil . justPressed ) #end ) && ! justUnpaused )
2023-01-23 00:55:30 +00:00
{
2024-02-28 19:51:39 +00:00
currentConversation . advanceConversation ( ) ;
2023-01-23 00:55:30 +00:00
}
2025-04-14 15:20:15 +00:00
e lse if ( ( controls . PAUSE || androidPause || pauseButtonCheck ) && ! justUnpaused )
2023-06-22 05:41:01 +00:00
{
2025-06-22 20:11:29 +00:00
pause ( Conversation ) ;
2023-06-22 05:41:01 +00:00
}
}
e lse if ( VideoCutscene . isPlaying ( ) )
{
// This is a video cutscene.
2025-04-14 15:20:15 +00:00
if ( ( controls . PAUSE || androidPause || pauseButtonCheck ) && ! justUnpaused )
2023-01-23 00:55:30 +00:00
{
2025-06-22 20:11:29 +00:00
pause ( Cutscene ) ;
2024-02-28 05:19:08 +00:00
}
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
}
2023-01-23 00:55:30 +00:00
2024-02-27 00:03:04 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Handle logic for a c t u a l l y s k i p p i n g a v i d e o c u t s c e n e a f t e r i t h a s b e e n h e l d .
* /
2024-02-27 00:03:04 +00:00
function skipVideoCutscene ( ) : Void
{
VideoCutscene . finishVideo ( ) ;
}
2023-06-22 05:41:01 +00:00
/ * *
2024-08-26 22:01:36 +00: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 c u t s c e n e o r s o m e t h i n g ) .
* Remember to call ` endSong ` again when the song should actually end !
* @ param rightGoddamnNow If true , don ' t p l a y t h e f a n c y a n i m a t i o n w h e r e y o u z o o m o n t o G i r l f r i e n d . U s e d a f t e r a c u t s c e n e .
* /
2024-02-27 00:03:04 +00:00
public function endSong ( rightGoddamnNow : Bool = false ) : Void
2023-06-22 05:41:01 +00:00
{
2024-04-02 01:59:53 +00:00
if ( FlxG . sound . music != null ) FlxG . sound . music . volume = 0 ;
2025-04-23 03:31:58 +00:00
if ( vocals != null ) vocals . volume = 0 ;
2024-02-27 00:03:04 +00:00
mayPauseGame = false ;
2025-07-17 09:51:06 +00:00
isSongEnd = true ;
2024-02-27 00:03:04 +00:00
2025-03-19 07:12:55 +00:00
// Prevent ghost misses while the song is ending.
disableKeys = true ;
2025-04-14 15:20:15 +00:00
#if mobile
// Hide the buttons while the song is ending.
2025-06-29 00:17:45 +00:00
if ( hitbox != null ) hitbox . visible = false ;
2025-06-11 23:53:42 +00:00
pauseButton . visible = false ;
2025-06-08 03:09:40 +00:00
pauseCircle . visible = false ;
2025-04-14 15:20:15 +00:00
#end
2024-02-27 00:03:04 +00:00
// 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-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
deathCounter = 0 ;
2024-02-27 00:03:04 +00:00
2024-06-11 04:40:43 +00:00
// TODO: This line of code makes me sad, but you can't really fix it without a breaking migration.
// `easy`, `erect`, `normal-pico`, etc.
var suffixedDifficulty = ( currentVariation != Constants . DEFAULT_VARIATION
& & currentVariation != ' e r e c t ' ) ? ' $ currentDifficulty - ${ currentVariation } ' : currentDifficulty ;
2024-04-02 01:59:53 +00:00
var isNewHighscore = false ;
2024-06-11 04:40:43 +00:00
var prevScoreData: Null < SaveScoreData > = Save . instance . getSongScore ( currentSong . id , suffixedDifficulty ) ;
2024-04-02 01:59:53 +00:00
2023-06-22 05:41:01 +00:00
if ( currentSong != null && currentSong . validScore )
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
2023-10-17 04:38:28 +00:00
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 ,
} ,
} ;
2024-03-05 02:18:40 +00:00
// adds current song data into the tallies for the level (story levels)
Highscore . talliesLevel = Highscore . combineTallies ( Highscore . tallies , Highscore . talliesLevel ) ;
2025-04-01 19:14:54 +00:00
#if FEATURE_NEWGROUNDS
2025-04-02 21:45:18 +00:00
Leaderboards . submitSongScore ( currentSong . id , suffixedDifficulty , songScore ) ;
2025-04-01 19:14:54 +00:00
#end
2024-06-11 04:40:43 +00:00
if ( ! isPracticeMode && ! isBotPlayMode )
2023-10-17 04:38:28 +00:00
{
2025-03-27 06:55:45 +00:00
#if FEATURE_NEWGROUNDS
Events . logCompleteSong ( currentSong . id , currentVariation ) ;
#end
2024-06-11 04:40:43 +00:00
isNewHighscore = Save . instance . isSongHighScore ( currentSong . id , suffixedDifficulty , data ) ;
// If no high score is present, save both score and rank.
// If score or rank are better, save the highest one.
// If neither are higher, nothing will change.
Save . instance . applySongRank ( currentSong . id , suffixedDifficulty , data ) ;
2024-10-07 03:51:50 +00:00
if ( isNewHighscore ) { }
2023-10-17 04:38:28 +00:00
}
2023-06-22 05:41:01 +00:00
}
2024-10-07 03:51:50 +00:00
#if FEATURE_NEWGROUNDS
// Only award medals if we are LEGIT.
if ( ! isPracticeMode && ! isBotPlayMode && ! isChartingMode && currentSong . validScore )
{
2025-03-11 00:47:06 +00:00
// Award a medal for beating at least one song on any difficulty on a Friday.
2024-10-07 03:51:50 +00:00
if ( Date . now ( ) . getDay ( ) == 5 ) Medals . award ( FridayNight ) ;
// Determine the score rank for this song we just finished.
2025-04-23 03:31:58 +00:00
var scoreRank: Null < ScoringRank > = Scoring . calculateRank (
2024-10-07 03:51:50 +00:00
{
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 ,
}
} ) ;
2025-04-01 18:48:16 +00:00
// Award various medals based on variation, difficulty, song ID, and scoring rank.
2024-10-07 03:51:50 +00:00
if ( scoreRank == ScoringRank . SHIT ) Medals . award ( LossRating ) ;
if ( scoreRank >= ScoringRank . PERFECT && currentDifficulty == ' h a r d ' ) Medals . award ( PerfectRatingHard ) ;
if ( scoreRank == ScoringRank . PERFECT_GOLD && currentDifficulty == ' h a r d ' ) Medals . award ( GoldPerfectRatingHard ) ;
if ( Constants . DEFAULT_DIFFICULTY_LIST_ERECT . contains ( currentDifficulty ) ) Medals . award ( ErectDifficulty ) ;
if ( scoreRank == ScoringRank . PERFECT_GOLD && currentDifficulty == ' n i g h t m a r e ' ) Medals . award ( GoldPerfectRatingNightmare ) ;
if ( currentVariation == ' p i c o ' && ! PlayStatePlaylist . isStoryMode ) Medals . award ( FreeplayPicoMix ) ;
2025-03-30 06:56:03 +00:00
if ( currentVariation == ' p i c o ' && currentSong . id == ' s t r e s s ' ) Medals . award ( FreeplayStressPico ) ;
2025-03-27 06:55:45 +00:00
2025-04-23 03:31:58 +00:00
if ( scoreRank != null ) Events . logEarnRank ( scoreRank . toString ( ) ) ;
2024-10-07 03:51:50 +00:00
}
#end
2025-06-07 14:26:38 +00:00
#if FEATURE_MOBILE_ADVERTISEMENTS
2025-06-10 12:29:08 +00:00
if ( AdMobUtil . PLAYING_COUNTER < AdMobUtil . MAX_BEFORE_AD ) AdMobUtil . PLAYING_COUNTER ++ ;
2025-06-07 14:26:38 +00:00
#end
2023-06-22 05:41:01 +00:00
if ( PlayStatePlaylist . isStoryMode )
2023-01-23 00:55:30 +00:00
{
2024-04-02 01:59:53 +00:00
isNewHighscore = false ;
2023-06-22 05:41:01 +00:00
PlayStatePlaylist . campaignScore += songScore ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// Pop the next song ID from the list.
// Returns null if the list is empty.
2025-04-23 03:31:58 +00:00
var targetSongId: Null < String > = PlayStatePlaylist . playlistSongIds . shift ( ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
if ( targetSongId == null )
{
if ( currentSong . validScore )
{
2023-10-03 23:14:46 +00:00
var data =
2023-10-17 04:38:28 +00:00
{
score : PlayStatePlaylist . campaignScore ,
tallies :
{
2024-05-18 00:26:34 +00:00
// TODO: Sum up the values for the whole week!
2023-10-17 04:38:28 +00:00
sick : 0 ,
good : 0 ,
bad : 0 ,
shit : 0 ,
missed : 0 ,
combo : 0 ,
maxCombo : 0 ,
totalNotesHit : 0 ,
totalNotes : 0 ,
} ,
} ;
2025-04-23 03:31:58 +00:00
if ( PlayStatePlaylist . campaignId != null )
{
#if FEATURE_NEWGROUNDS
// Award a medal for beating a Story level.
Medals . awardStoryLevel ( PlayStatePlaylist . campaignId ) ;
2024-10-07 03:51:50 +00:00
2025-04-23 03:31:58 +00:00
// Submit the score for the Story level to Newgrounds.
Leaderboards . submitLevelScore ( PlayStatePlaylist . campaignId , PlayStatePlaylist . campaignDifficulty , PlayStatePlaylist . campaignScore ) ;
2025-03-27 06:55:45 +00:00
2025-04-23 03:31:58 +00:00
Events . logCompleteLevel ( PlayStatePlaylist . campaignId ) ;
#end
2024-10-07 03:51:50 +00:00
2025-04-23 03:31:58 +00:00
if ( Save . instance . isLevelHighScore ( PlayStatePlaylist . campaignId , PlayStatePlaylist . campaignDifficulty , data ) )
{
Save . instance . setLevelScore ( PlayStatePlaylist . campaignId , PlayStatePlaylist . campaignDifficulty , data ) ;
isNewHighscore = true ;
}
2023-10-17 04:38:28 +00:00
}
}
2023-01-23 00:55:30 +00:00
2023-07-26 20:52:58 +00:00
if ( isSubState )
{
this . close ( ) ;
}
e lse
{
2024-02-27 00:03:04 +00:00
if ( rightGoddamnNow )
{
2024-04-02 01:59:53 +00:00
moveToResultsScreen ( isNewHighscore ) ;
2024-02-27 00:03:04 +00:00
}
e lse
{
2024-04-02 01:59:53 +00:00
zoomIntoResultsScreen ( isNewHighscore ) ;
2024-02-27 00:03:04 +00:00
}
2023-07-26 20:52:58 +00:00
}
2023-06-22 05:41:01 +00:00
}
e lse
{
var difficulty: String = ' ' ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
trace ( ' L o a d i n g n e x t s o n g ( $ targetSongId : $ difficulty ) ' ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
FlxTransitionableState . skipNextTransIn = true ;
FlxTransitionableState . skipNextTransOut = true ;
2023-01-23 00:55:30 +00:00
2025-04-23 03:31:58 +00:00
FlxG . sound . music ? . stop ( ) ;
vocals ? . stop ( ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
// TODO: Softcode this cutscene.
2023-09-08 21:46:44 +00:00
if ( currentSong . id == ' e g g n o g ' )
2023-06-22 05:41:01 +00:00
{
2024-02-22 23:55:24 +00: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 05:41:01 +00:00
camHUD . visible = false ;
isInCutscene = true ;
2023-01-23 00:55:30 +00:00
2024-03-23 21:50:48 +00:00
FunkinSound . playOnce ( Paths . sound ( ' L i g h t s _ S h u t _ o f f ' ) , function ( ) {
2023-06-22 05:41:01 +00:00
// no camFollow so it centers on horror tree
2025-04-23 03:31:58 +00:00
var targetSong: Song = SongRegistry . instance . fetchEntry ( targetSongId ) ? ? throw ' C o u l d n o t f i n d a s o n g w i t h t h e I D $ targetSongId ' ;
2024-07-23 08:34:26 +00:00
var targetVariation: String = currentVariation ;
if ( ! targetSong . hasDifficulty ( PlayStatePlaylist . campaignDifficulty , currentVariation ) )
{
targetVariation = targetSong . getFirstValidVariation ( PlayStatePlaylist . campaignDifficulty ) ? ? Constants . DEFAULT_VARIATION ;
}
2025-04-23 03:31:58 +00:00
if ( currentStage != null ) this . remove ( currentStage ) ;
2024-02-17 04:48:43 +00:00
LoadingState . loadPlayState (
2024-02-06 00:46:11 +00:00
{
targetSong : targetSong ,
targetDifficulty : PlayStatePlaylist . campaignDifficulty ,
2024-07-23 08:34:26 +00:00
targetVariation : targetVariation ,
2024-02-17 04:48:43 +00:00
cameraFollowPoint : cameraFollowPoint . getPosition ( ) ,
2024-02-06 00:46:11 +00:00
} ) ;
} ) ;
2023-06-22 05:41:01 +00:00
}
2024-02-17 04:48:43 +00:00
e lse
{
2025-04-23 03:31:58 +00:00
var targetSong: Song = SongRegistry . instance . fetchEntry ( targetSongId ) ? ? throw ' C o u l d n o t f i n d a s o n g w i t h I D $ targetSongId ' ;
2024-07-23 08:34:26 +00:00
var targetVariation: String = currentVariation ;
if ( ! targetSong . hasDifficulty ( PlayStatePlaylist . campaignDifficulty , currentVariation ) )
{
targetVariation = targetSong . getFirstValidVariation ( PlayStatePlaylist . campaignDifficulty ) ? ? Constants . DEFAULT_VARIATION ;
}
2025-04-23 03:31:58 +00:00
if ( currentStage != null ) this . remove ( currentStage ) ;
2024-02-17 04:48:43 +00:00
LoadingState . loadPlayState (
{
targetSong : targetSong ,
targetDifficulty : PlayStatePlaylist . campaignDifficulty ,
2024-07-23 08:34:26 +00:00
targetVariation : targetVariation ,
2024-02-17 04:48:43 +00:00
cameraFollowPoint : cameraFollowPoint . getPosition ( ) ,
} ) ;
}
2023-06-22 05:41:01 +00:00
}
}
e lse
2023-01-23 00:55:30 +00:00
{
2023-07-26 20:52:58 +00:00
if ( isSubState )
{
this . close ( ) ;
}
e lse
{
2024-02-27 00:03:04 +00:00
if ( rightGoddamnNow )
{
2024-05-18 00:26:34 +00:00
moveToResultsScreen ( isNewHighscore , prevScoreData ) ;
2024-02-27 00:03:04 +00:00
}
e lse
{
2024-05-18 00:26:34 +00:00
zoomIntoResultsScreen ( isNewHighscore , prevScoreData ) ;
2024-02-27 00:03:04 +00:00
}
2023-07-26 20:52:58 +00:00
}
2023-01-23 00:55:30 +00:00
}
}
2023-07-26 20:52:58 +00:00
public override function close ( ) : Void
{
2023-07-27 01:34:38 +00:00
criticalFailure = true ; // Stop game updates.
2023-07-26 20:52:58 +00:00
performCleanup ( ) ;
super . close ( ) ;
}
2023-01-23 00:55:30 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Perform necessary cleanup before leaving the PlayState .
* /
2023-06-22 05:41:01 +00:00
function performCleanup ( ) : Void
2023-01-23 00:55:30 +00:00
{
2024-03-10 23:35:41 +00:00
// If the camera is being tweened, stop it.
cancelAllCameraTweens ( ) ;
2024-03-27 03:24:59 +00:00
// Dispatch the destroy event.
dispatchEvent ( new ScriptEvent ( DESTROY , false ) ) ;
2024-02-28 08:01:20 +00:00
if ( currentConversation != null )
{
remove ( currentConversation ) ;
currentConversation . kill ( ) ;
}
2023-06-22 05:41:01 +00:00
if ( currentChart != null )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
// TODO: Uncache the song.
2023-01-23 00:55:30 +00:00
}
2025-05-08 19:53:50 +00:00
// Prevent vwoosh timer from running outside PlayState (e.g Chart Editor)
vwooshTimer . cancel ( ) ;
2024-03-05 02:19:24 +00:00
if ( overrideMusic )
2023-08-04 20:15:07 +00:00
{
2024-03-05 02:19:24 +00:00
// Stop the music. Do NOT destroy it, something still references it!
2024-03-26 23:38:42 +00:00
if ( FlxG . sound . music != null ) FlxG . sound . music . pause ( ) ;
2024-03-05 02:19:24 +00:00
if ( vocals != null )
{
vocals . pause ( ) ;
remove ( vocals ) ;
}
2023-08-04 20:15:07 +00:00
}
e lse
{
2024-03-05 02:19:24 +00:00
// Stop and destroy the music.
2024-03-26 23:38:42 +00:00
if ( FlxG . sound . music != null ) FlxG . sound . music . pause ( ) ;
2024-02-05 18:35:30 +00:00
if ( vocals != null )
{
2024-02-28 05:19:08 +00:00
vocals . destroy ( ) ;
2024-02-05 18:35:30 +00:00
remove ( vocals ) ;
}
2023-08-04 20:15:07 +00:00
}
2023-07-27 01:34:38 +00:00
2025-04-14 00:55:46 +00:00
forEachPausedSound ( ( s ) - > s . destroy ( ) ) ;
2025-06-30 17:22:39 +00:00
FlxTween . globalManager . clear ( ) ;
FlxTimer . globalManager . clear ( ) ;
2023-06-22 05:41:01 +00:00
// Remove reference to stage and remove sprites from it to save memory.
if ( currentStage != null )
2023-01-23 00:55:30 +00:00
{
2023-06-22 05:41:01 +00:00
remove ( currentStage ) ;
currentStage . kill ( ) ;
currentStage = null ;
2023-01-23 00:55:30 +00:00
}
2023-06-22 05:41:01 +00:00
GameOverSubState . reset ( ) ;
2024-02-28 19:51:39 +00:00
PauseSubState . reset ( ) ;
2024-07-13 19:45:58 +00:00
Countdown . reset ( ) ;
2023-06-22 05:41:01 +00:00
// Clear the static reference to this state.
instance = null ;
2023-01-23 00:55:30 +00:00
}
/ * *
2024-08-26 22:01:36 +00:00
* Play the camera zoom animation and then move to the results screen once it ' s d o n e .
* /
2024-05-18 00:26:34 +00:00
function zoomIntoResultsScreen ( isNewHighscore : Bool , ? prevScoreData : SaveScoreData ) : Void
2023-06-16 21:37:56 +00:00
{
2023-06-22 05:41:01 +00:00
trace ( ' W E N T T O R E S U L T S S C R E E N ! ' ) ;
2023-06-16 21:37:56 +00:00
2023-06-22 05:41:01 +00:00
// Stop camera zooming on beat.
cameraZoomRate = 0 ;
2023-06-16 21:37:56 +00:00
2024-05-09 16:51:03 +00:00
// Cancel camera and scroll tweening if it's active.
2024-03-10 23:35:41 +00:00
cancelAllCameraTweens ( ) ;
2024-05-10 20:23:35 +00:00
cancelScrollSpeedTweens ( ) ;
2024-03-10 23:35:41 +00:00
2023-06-22 05:41:01 +00: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.
2025-04-23 03:31:58 +00:00
var boyfriend: Null < BaseCharacter > = currentStage ? . getBoyfriend ( ) ;
var girlfriend: Null < BaseCharacter > = currentStage ? . getGirlfriend ( ) ;
var dad: Null < BaseCharacter > = currentStage ? . getDad ( ) ;
var targetDad: Bool = dad ? . characterId == ' g f ' ;
var targetBF: Bool = girlfriend == null && ! targetDad ;
2023-06-16 21:37:56 +00:00
2025-04-23 03:31:58 +00:00
if ( targetBF && boyfriend != null )
2023-06-16 21:37:56 +00:00
{
2025-04-23 03:31:58 +00:00
FlxG . camera . follow ( boyfriend , null , 0.05 ) ;
2023-06-16 21:37:56 +00:00
}
2025-04-23 03:31:58 +00:00
e lse if ( targetDad && dad != null )
2023-06-16 21:37:56 +00:00
{
2025-04-23 03:31:58 +00:00
FlxG . camera . follow ( dad , null , 0.05 ) ;
2023-06-22 05:41:01 +00:00
}
2025-04-23 03:31:58 +00:00
e lse if ( girlfriend != null )
2023-06-22 05:41:01 +00:00
{
2025-04-23 03:31:58 +00:00
FlxG . camera . follow ( girlfriend , null , 0.05 ) ;
2023-06-16 21:37:56 +00:00
}
2024-04-25 03:50:19 +00:00
// TODO: Make target offset configurable.
// In the meantime, we have to replace the zoom animation with a fade out.
FlxG . camera . targetOffset . y -= 350 ;
FlxG . camera . targetOffset . x += 20 ;
// Replace zoom animation with a fade out for now.
2024-06-08 00:02:54 +00:00
FlxG . camera . fade ( FlxColor . BLACK , 0.6 ) ;
2024-04-25 03:50:19 +00:00
FlxTween . tween ( camHUD , { alpha : 0 } , 0.6 ,
{
onComplete : function ( _ ) {
2024-05-18 00:26:34 +00:00
moveToResultsScreen ( isNewHighscore , prevScoreData ) ;
2024-04-25 03:50:19 +00:00
}
} ) ;
2023-06-16 21:37:56 +00:00
2023-06-22 05:41:01 +00:00
// Zoom in on Girlfriend (or BF if no GF)
new FlxTimer ( ) . start ( 0.8 , function ( _ ) {
if ( targetBF )
{
2025-04-23 03:31:58 +00:00
boyfriend ? . animation . play ( ' h e y ' ) ;
2023-06-22 05:41:01 +00:00
}
e lse if ( targetDad )
{
2025-04-23 03:31:58 +00:00
dad ? . animation . play ( ' c h e e r ' ) ;
2023-06-22 05:41:01 +00:00
}
e lse
{
2025-04-23 03:31:58 +00:00
girlfriend ? . animation . play ( ' c h e e r ' ) ;
2023-06-22 05:41:01 +00:00
}
// Zoom over to the Results screen.
2024-04-25 03:50:19 +00:00
// TODO: Re-enable this.
/ *
2024-08-26 22:01:36 +00:00
FlxTween . tween ( FlxG . camera , { zoom : 1200 } , 1.1 ,
{
ease : FlxEase . expoIn ,
} ) ;
* /
2023-06-22 05:41:01 +00:00
} ) ;
2023-01-23 00:55:30 +00:00
}
2024-02-27 00:03:04 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Move to the results screen right goddamn now .
* /
2024-05-18 00:26:34 +00:00
function moveToResultsScreen ( isNewHighscore : Bool , ? prevScoreData : SaveScoreData ) : Void
2024-02-27 00:03:04 +00:00
{
2025-04-23 03:31:58 +00:00
var currentChart: SongDifficulty = currentChart ? ?
{
trace ( ' E R R O R : C a n n o t m o v e t o r e s u l t s s c r e e n w i t h a n u l l c h a r t . ' ) ;
return ;
}
2024-02-27 00:03:04 +00:00
persistentUpdate = false ;
2025-04-23 03:31:58 +00:00
vocals ? . stop ( ) ;
2024-02-27 00:03:04 +00:00
camHUD . alpha = 1 ;
2024-03-05 02:18:40 +00:00
var talliesToUse: Tallies = PlayStatePlaylist . isStoryMode ? Highscore . talliesLevel : Highscore . tallies ;
2024-02-27 00:03:04 +00:00
var res: ResultState = new ResultState (
{
storyMode : PlayStatePlaylist . isStoryMode ,
2024-05-30 09:25:51 +00:00
songId : currentChart . song . id ,
difficultyId : currentDifficulty ,
2025-04-02 21:45:18 +00:00
variationId : currentVariation ,
2024-07-04 02:50:39 +00:00
characterId : currentChart . characters . player ,
2024-02-27 00:03:04 +00:00
title : PlayStatePlaylist . isStoryMode ? ( ' ${ PlayStatePlaylist . campaignTitle } ' ) : ( ' ${ currentChart . songName } b y ${ currentChart . songArtist } ' ) ,
2024-05-18 00:26:34 +00:00
prevScoreData : prevScoreData ,
2024-04-02 01:59:53 +00:00
scoreData :
{
2024-04-06 06:06:06 +00:00
score : PlayStatePlaylist . isStoryMode ? PlayStatePlaylist . campaignScore : songScore ,
2024-04-02 01:59:53 +00:00
tallies :
{
2024-04-06 06:06:06 +00:00
sick : talliesToUse . sick ,
good : talliesToUse . good ,
bad : talliesToUse . bad ,
shit : talliesToUse . shit ,
missed : talliesToUse . missed ,
combo : talliesToUse . combo ,
maxCombo : talliesToUse . maxCombo ,
totalNotesHit : talliesToUse . totalNotesHit ,
totalNotes : talliesToUse . totalNotes ,
2024-04-02 01:59:53 +00:00
} ,
} ,
2025-03-20 00:32:21 +00:00
isNewHighscore : isNewHighscore ,
isPracticeMode : isPracticeMode ,
isBotPlayMode : isBotPlayMode ,
2024-02-27 00:03:04 +00:00
} ) ;
2024-06-02 03:36:57 +00:00
this . persistentDraw = false ;
2024-02-27 00:03:04 +00:00
openSubState ( res ) ;
}
2023-01-23 00:55:30 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Pauses music and vocals easily .
* /
2023-06-22 05:41:01 +00:00
public function pauseMusic ( ) : Void
2023-01-23 00:55:30 +00:00
{
2024-05-01 03:24:43 +00:00
if ( FlxG . sound . music != null ) FlxG . sound . music . pause ( ) ;
if ( vocals != null ) vocals . pause ( ) ;
2023-01-23 00:55:30 +00:00
}
/ * *
2024-08-26 22:01:36 +00:00
* Resets the camera ' s z o o m l e v e l a n d f o c u s p o i n t .
* /
2025-04-23 03:31:58 +00:00
public function resetCamera ( resetZoom : Bool = true , cancelTweens : Bool = true , snap : Bool = true ) : Void
2023-01-23 00:55:30 +00:00
{
2024-03-10 23:35:41 +00:00
// Cancel camera tweens if any are active.
if ( cancelTweens )
2024-02-28 06:29:40 +00:00
{
2024-03-10 23:35:41 +00:00
cancelAllCameraTweens ( ) ;
2024-02-28 06:29:40 +00:00
}
2024-06-08 00:26:17 +00:00
FlxG . camera . follow ( cameraFollowPoint , LOCKON , Constants . DEFAULT_CAMERA_FOLLOW_RATE ) ;
2023-01-23 00:55:30 +00:00
FlxG . camera . targetOffset . set ( ) ;
2024-02-28 06:29:40 +00:00
if ( resetZoom )
{
2024-03-16 15:38:10 +00:00
resetCameraZoom ( ) ;
2024-02-28 06:29:40 +00:00
}
2023-07-26 20:52:58 +00:00
// Snap the camera to the follow point immediately.
2024-05-15 12:42:11 +00:00
if ( snap ) FlxG . camera . focusOn ( cameraFollowPoint . getPosition ( ) ) ;
2023-01-23 00:55:30 +00:00
}
2024-03-28 19:34:13 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Sets the camera follow point ' s p o s i t i o n a n d t w e e n s t h e c a m e r a t h e r e .
* /
2025-04-23 03:31:58 +00:00
public function tweenCameraToPosition ( x : Float = 0 , y : Float = 0 , duration : Float = 0 , ? ease : Null < Float -> Float > ) : Void
2024-03-28 19:34:13 +00:00
{
cameraFollowPoint . setPosition ( x , y ) ;
tweenCameraToFollowPoint ( duration , ease ) ;
}
2024-02-28 06:29:40 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Disables camera following and tweens the camera to the follow point manually .
* /
2025-04-23 03:31:58 +00:00
public function tweenCameraToFollowPoint ( duration : Float = 0 , ? ease : Null < Float -> Float > ) : Void
2024-02-28 06:29:40 +00:00
{
// Cancel the current tween if it's active.
2024-03-10 23:35:41 +00:00
cancelCameraFollowTween ( ) ;
if ( duration == 0 )
{
// Instant movement. Just reset the camera to force it to the follow point.
resetCamera ( false , false ) ;
}
e lse
{
// Disable camera following for the duration of the tween.
2025-04-23 03:31:58 +00:00
@ : nullSafety ( Off )
2024-03-10 23:35:41 +00:00
FlxG . camera . target = null ;
// Follow tween! Caching it so we can cancel/pause it later if needed.
var followPos: FlxPoint = cameraFollowPoint . getPosition ( ) - FlxPoint . weak ( FlxG . camera . width * 0.5 , FlxG . camera . height * 0.5 ) ;
cameraFollowTween = FlxTween . tween ( FlxG . camera . scroll , { x : followPos . x , y : followPos . y } , duration ,
{
ease : ease ,
onComplete : function ( _ ) {
resetCamera ( false , false ) ; // Re-enable camera following when the tween is complete.
}
} ) ;
}
}
public function cancelCameraFollowTween ( )
{
2024-02-28 06:29:40 +00:00
if ( cameraFollowTween != null )
{
cameraFollowTween . cancel ( ) ;
}
2024-03-10 23:35:41 +00:00
}
2024-02-28 06:29:40 +00:00
2024-03-10 23:35:41 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Tweens the camera zoom to the desired amount .
* /
2025-04-23 03:31:58 +00:00
public function tweenCameraZoom ( zoom : Float = 1 , duration : Float = 0 , direct : Bool = false , ? ease : Null < Float -> Float > ) : Void
2024-03-10 23:35:41 +00:00
{
// Cancel the current tween if it's active.
cancelCameraZoomTween ( ) ;
2024-02-28 06:29:40 +00:00
2024-03-28 18:05:38 +00:00
// Direct mode: Set zoom directly.
// Stage mode: Set zoom as a multiplier of the current stage's default zoom.
var targetZoom = zoom * ( direct ? FlxCamera . defaultZoom : stageZoom ) ;
2024-03-10 23:35:41 +00:00
2024-03-28 18:05:38 +00:00
if ( duration == 0 )
2024-03-10 23:35:41 +00:00
{
2024-03-28 18:05:38 +00:00
// Instant zoom. No tween needed.
currentCameraZoom = targetZoom ;
2024-03-10 23:35:41 +00:00
}
2024-03-28 18:05:38 +00:00
e lse
2024-03-10 23:35:41 +00:00
{
2024-03-28 18:05:38 +00:00
// Zoom tween! Caching it so we can cancel/pause it later if needed.
cameraZoomTween = FlxTween . tween ( this , { currentCameraZoom : targetZoom } , duration , { ease : ease } ) ;
2024-03-10 23:35:41 +00:00
}
}
2025-04-23 03:31:58 +00:00
public function cancelCameraZoomTween ( ) : Void
2024-03-10 23:35:41 +00:00
{
if ( cameraZoomTween != null )
{
cameraZoomTween . cancel ( ) ;
}
}
/ * *
2024-08-26 22:01:36 +00:00
* Cancel all active camera tweens simultaneously .
* /
2024-03-10 23:35:41 +00:00
public function cancelAllCameraTweens ( )
{
cancelCameraFollowTween ( ) ;
cancelCameraZoomTween ( ) ;
2024-02-28 06:29:40 +00:00
}
2025-06-06 04:11:55 +00:00
var prevScrollTargets: Array < Dynamic > = [ ] ; // used to snap scroll speed when things go unruly
2024-05-15 16:17:09 +00:00
2023-01-23 00:55:30 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* The magical function that s h a l l t w e e n t h e s c r o l l s p e e d .
* /
2024-05-10 20:23:35 +00:00
public function tweenScrollSpeed ( ? speed : Float , ? duration : Float , ? ease : Null < Float -> Float > , strumlines : Array < String > ) : Void
2024-05-09 16:51:03 +00:00
{
// Cancel the current tween if it's active.
2024-05-10 20:23:35 +00:00
cancelScrollSpeedTweens ( ) ;
2024-05-15 16:17:09 +00:00
// Snap to previous event value to prevent the tween breaking when another event cancels the previous tween.
for ( i in prevScrollTargets )
{
2024-05-15 16:24:04 +00:00
var value: Float = i [ 0 ] ;
var strum: Strumline = Reflect . getProperty ( this , i [ 1 ] ) ;
2024-05-15 16:17:09 +00:00
strum . scrollSpeed = value ;
}
// for next event, clean array.
prevScrollTargets = [ ] ;
2024-05-10 20:23:35 +00:00
for ( i in strumlines )
2024-05-09 16:51:03 +00:00
{
2025-04-23 03:31:58 +00:00
var value: Float = speed ? ? 0 ;
2024-05-10 20:23:35 +00:00
var strum: Strumline = Reflect . getProperty ( this , i ) ;
if ( duration == 0 )
{
strum . scrollSpeed = value ;
}
e lse
{
scrollSpeedTweens . push ( FlxTween . tween ( strum ,
{
' s c r o l l S p e e d ' : value
} , duration , { ease : ease } ) ) ;
}
2024-05-15 16:17:09 +00:00
// make sure charts dont break if the charter is dumb and stupid
prevScrollTargets . push ( [ value , i ] ) ;
2024-05-09 16:51:03 +00:00
}
}
2024-05-10 20:23:35 +00:00
public function cancelScrollSpeedTweens ( )
2024-05-09 16:51:03 +00:00
{
2024-05-10 20:23:35 +00:00
for ( tween in scrollSpeedTweens )
2024-05-09 16:51:03 +00:00
{
2024-05-10 20:23:35 +00:00
if ( tween != null )
{
tween . cancel ( ) ;
}
2024-05-09 16:51:03 +00:00
}
2024-05-10 20:37:46 +00:00
scrollSpeedTweens = [ ] ;
2024-05-09 16:51:03 +00:00
}
2025-04-14 00:55:46 +00:00
function forEachPausedSound ( f : FlxSound -> Void ) : Void
{
for ( sound in soundsPausedBySubState )
{
f ( sound ) ;
}
soundsPausedBySubState . clear ( ) ;
}
2025-02-18 16:31:06 +00:00
#if FEATURE_DEBUG_FUNCTIONS
2023-01-23 00:55:30 +00:00
/ * *
2024-08-26 22:01:36 +00:00
* Jumps forward or backward a number of sections in the song .
* Accounts for B P M c h a n g e s , d o e s n o t p r e v e n t d e a t h f r o m s k i p p e d n o t e s .
* @ param sections The number of sections to jump , negative to go backwards .
* /
2023-06-22 05:41:01 +00:00
function changeSection ( sections : Int ) : Void
2023-01-23 00:55:30 +00:00
{
2024-02-27 00:03:04 +00:00
// FlxG.sound.music.pause();
2023-01-23 00:55:30 +00:00
2024-02-27 00:03:04 +00:00
var targetTimeSteps: Float = Conductor . instance . currentStepTime + ( Conductor . instance . stepsPerMeasure * sections ) ;
2023-12-14 21:56:20 +00:00
var targetTimeMs: Float = Conductor . instance . getStepTimeInMs ( targetTimeSteps ) ;
2023-01-23 00:55:30 +00:00
2024-02-27 00:03:04 +00:00
// Don't go back in time to before the song started.
targetTimeMs = Math . max ( 0 , targetTimeMs ) ;
2024-03-29 02:33:50 +00:00
if ( FlxG . sound . music != null )
{
FlxG . sound . music . time = targetTimeMs ;
}
2023-01-23 00:55:30 +00:00
2023-07-27 00:03:31 +00:00
handleSkippedNotes ( ) ;
2024-02-27 00:03:04 +00:00
SongEventRegistry . handleSkippedEvents ( songEvents , Conductor . instance . songPosition ) ;
2023-07-27 00:03:31 +00:00
// regenNoteData(FlxG.sound.music.time);
2023-06-02 19:35:01 +00:00
2024-04-25 03:50:19 +00:00
Conductor . instance . update ( FlxG . sound ? . music ? . time ? ? 0.0 ) ;
2023-01-23 00:55:30 +00:00
2023-06-22 05:41:01 +00:00
resyncVocals ( ) ;
2023-01-23 00:55:30 +00:00
}
2025-02-18 16:31:06 +00:00
#end
2021-08-27 22:08:01 +00:00
}