aboutsummaryrefslogtreecommitdiff
path: root/Northstar.Coop/scripts/vscripts/sp/_gauntlet.gnut
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.Coop/scripts/vscripts/sp/_gauntlet.gnut')
-rw-r--r--Northstar.Coop/scripts/vscripts/sp/_gauntlet.gnut1054
1 files changed, 1054 insertions, 0 deletions
diff --git a/Northstar.Coop/scripts/vscripts/sp/_gauntlet.gnut b/Northstar.Coop/scripts/vscripts/sp/_gauntlet.gnut
new file mode 100644
index 000000000..4cde30941
--- /dev/null
+++ b/Northstar.Coop/scripts/vscripts/sp/_gauntlet.gnut
@@ -0,0 +1,1054 @@
+global function Gauntlet_ServerInit
+global function EnableAllGauntlets
+global function DisableAllGauntlets
+global function EnableGauntlet
+global function DisableGauntlet
+global function Gauntlet_HideLeaderboard
+global function Gauntlet_ShowLeaderboard
+global function Gauntlet_NPC_PostSpawn
+global function ClientCommand_Gauntlet_PlayerRestartedFromMenu
+global function Gauntlet_StartGhostPlayback
+global function Gauntlet_StopGhostPlayback
+global function Gauntlet_ChallengeLeaderboardGhosts
+#if DEV
+global function Gauntlet_Player_GhostRecordOrPlayback
+#endif
+
+const float GAUNTLET_ENEMY_MISSED_TIME_PENALTY = 2.0
+const float GAUNTLET_TARGET_DISSOLVE_TIME = 1.0
+const float GAUNTLET_TARGET_DISSOLVE_TIME_MS = GAUNTLET_TARGET_DISSOLVE_TIME * 100
+
+void function Gauntlet_ServerInit()
+{
+ AddClientCommandCallback( "Gauntlet_PlayerRestartedFromMenu", ClientCommand_Gauntlet_PlayerRestartedFromMenu )
+ AddCallback_EntitiesDidLoad( Gauntlet_PostEntityLoadSetup )
+ AddCallback_OnClientConnected( Gauntlet_PlayerConnected )
+ AddCallback_OnLoadSaveGame( Gauntlet_OnLoadSaveGame )
+
+ RegisterSignal( "trigStart_OnStartTouch" )
+ RegisterSignal( "trigStart_OnEndTouch" )
+ RegisterSignal( "trigStart2_OnStartTouch" )
+ RegisterSignal( "trigStart2_OnEndTouch" )
+
+ RegisterSignal( "Gauntlet_PlayerHitStartTrig" )
+ RegisterSignal( "Gauntlet_PlayerWentBackwardsThroughStartTrig" )
+ RegisterSignal( "Gauntlet_PlayerHitFinishTrig" )
+ RegisterSignal( "Gauntlet_CheckpointHit" )
+ RegisterSignal( "Gauntlet_ForceRestart" )
+ RegisterSignal( "GhostAnimationPlayback_Start" )
+ RegisterSignal( "GhostAnimationPlayback_Stop" )
+ RegisterSignal( "Gauntlet_PlayerBeatChallengeGhost" )
+ RegisterSignal( "Gauntlet_PlayerBeatAllChallengeGhosts" )
+ #if DEV
+ RegisterSignal( "RecordAnimation_Start")
+ RegisterSignal( "Player_StartRecordingGhost_HintStart" )
+ #endif
+}
+
+void function Gauntlet_PostEntityLoadSetup()
+{
+ foreach ( gauntlet in GetGauntlets() )
+ {
+ InitGauntlet( gauntlet )
+
+ if ( gauntlet.startEnabled )
+ EnableGauntlet( gauntlet )
+ }
+}
+
+void function Gauntlet_PlayerConnected( entity player )
+{
+ foreach ( gauntlet in GetGauntlets() )
+ {
+ // send ghost duration data to client- only server can read the anim durations
+ foreach ( ghost in gauntlet.ghosts )
+ Remote_CallFunction_Replay( player, "ScriptCallback_Gauntlet_SetGhostDuration", gauntlet.id, ghost.id, ghost.duration )
+
+ if ( gauntlet.showLeaderboard )
+ {
+ Gauntlet_ShowLeaderboard( gauntlet )
+
+ if ( gauntlet.activeGhostID != -1 )
+ Gauntlet_RefreshActiveGhostID( gauntlet )
+ }
+ }
+}
+
+void function Gauntlet_OnLoadSaveGame( entity player )
+{
+ thread Gauntlet_OnLoadSaveGame_Thread( player )
+}
+
+void function Gauntlet_OnLoadSaveGame_Thread( entity player )
+{
+ wait 1.0
+ Gauntlet_PlayerConnected( player )
+}
+
+// turns on a particular gauntlet
+void function EnableGauntlet( GauntletInfo gauntlet )
+{
+ Assert( gauntlet.isInited, "Must run InitGauntlet before enabling" )
+
+ if ( gauntlet.isEnabled )
+ return
+
+ Gauntlet_CreateSignalEnt( gauntlet )
+
+ foreach ( player in GetPlayerArray() )
+ Remote_CallFunction_Replay( player, "ScriptCallback_EnableGauntlet", gauntlet.id )
+
+ thread Gauntlet_Think( gauntlet )
+
+ gauntlet.isEnabled = true
+}
+
+// turns off a particular gauntlet
+void function DisableGauntlet( GauntletInfo gauntlet )
+{
+ if ( !gauntlet.isEnabled )
+ return
+
+ gauntlet.signalEnt.Signal( "DisableGauntlet" )
+
+ Gauntlet_CleanupSignalEnt( gauntlet )
+ Gauntlet_ClearSpawnedNPCs( gauntlet )
+ thread ClearDroppedWeapons( GAUNTLET_TARGET_DISSOLVE_TIME + 0.1 ) // needs to be longer than gauntlet ghost dissolve time so weapons drop
+
+ foreach ( player in GetPlayerArray() )
+ Remote_CallFunction_Replay( player, "ScriptCallback_DisableGauntlet", gauntlet.id )
+
+ gauntlet.isEnabled = false
+}
+
+void function Gauntlet_HideLeaderboard( GauntletInfo gauntlet )
+{
+ Gauntlet_SetLeaderboardEnabled( gauntlet, false )
+
+ foreach ( player in GetPlayerArray() )
+ Remote_CallFunction_Replay( player, "ScriptCallback_HideLeaderboard", gauntlet.id )
+}
+
+void function Gauntlet_ShowLeaderboard( GauntletInfo gauntlet )
+{
+ Gauntlet_SetLeaderboardEnabled( gauntlet, true )
+
+ foreach ( player in GetPlayerArray() )
+ Remote_CallFunction_Replay( player, "ScriptCallback_ShowLeaderboard", gauntlet.id )
+}
+
+void function Gauntlet_Checkpoints( GauntletInfo gauntlet )
+{
+ if ( !gauntlet.checkpoints.len() )
+ return
+
+ foreach ( trig in gauntlet.checkpoints )
+ thread Gauntlet_CheckpointTrig_WaitForPlayer( gauntlet, trig )
+}
+
+void function Gauntlet_CheckpointTrig_WaitForPlayer( GauntletInfo gauntlet, entity trig )
+{
+ gauntlet.player.EndSignal( "OnDestroy" )
+ gauntlet.player.EndSignal( "Gauntlet_RunStarted" )
+ gauntlet.player.EndSignal( "Gauntlet_RunStopped" )
+ trig.EndSignal( "OnDestroy" )
+
+ table result
+ entity activator
+
+ while ( 1 )
+ {
+ result = trig.WaitSignal( "OnStartTouch" )
+ activator = expect entity( result.activator )
+
+ if ( !activator.IsPlayer() )
+ continue
+
+ if ( !IsAlive( activator ) )
+ continue
+
+ if ( activator.IsTitan() )
+ continue
+
+ break
+ }
+
+ gauntlet.checkpointsHit++
+ activator.Signal( "Gauntlet_CheckpointHit" )
+}
+
+void function Gauntlet_ClearSpawnedNPCs( GauntletInfo gauntlet )
+{
+ foreach ( guy in gauntlet.spawned )
+ {
+ if ( IsAlive( guy ) )
+ {
+ Gauntlet_UnfreezeNPC( guy )
+ guy.Die()
+ }
+ }
+
+ gauntlet.spawned = []
+}
+
+void function Gauntlet_SpawnNPCs( GauntletInfo gauntlet )
+{
+ Gauntlet_ClearSpawnedNPCs( gauntlet )
+
+ array<entity> spawned = SpawnFromSpawnerArray( gauntlet.spawners )
+ foreach ( guy in spawned )
+ thread Gauntlet_NPC_PostSpawn( guy, gauntlet )
+
+ gauntlet.spawned = spawned
+}
+
+void function Gauntlet_NPC_PostSpawn( entity npc, GauntletInfo gauntlet )
+{
+ if ( IsGrunt( npc ) )
+ {
+ // TODO- pulse as player runs through course, so the effect highlights the different ranges where the enemies are
+ Highlight_SetEnemyHighlightWithParam1( npc, "gauntlet_target_highlight", npc.EyePosition() )
+
+ npc.SetHealth( 1 )
+
+ npc.SetCanBeMeleeExecuted( false )
+ }
+
+ npc.EndSignal( "OnDeath" )
+ thread Gauntlet_NPC_DeathWait( npc, gauntlet )
+
+ AddEntityCallback_OnDamaged( npc, Gauntlet_NPC_Damaged )
+
+ npc.SetNoTarget( true )
+ npc.SetEfficientMode( true )
+ npc.SetHologram()
+ npc.SetDeathActivity( "ACT_DIESIMPLE" )
+
+ wait RandomFloatRange( 0.5, 1.0 ) // This is no good, too variable. TODO put in a pose instead
+
+ npc.Freeze()
+}
+
+void function Gauntlet_NPC_Damaged( entity npc, var damageInfo )
+{
+ printt( "NPC Damaged!", npc.GetHealth() )
+
+ float dmg = DamageInfo_GetDamage( damageInfo )
+ float finalHealth = npc.GetHealth() - dmg
+
+ if ( finalHealth <= 0 )
+ Gauntlet_UnfreezeNPC( npc )
+}
+
+void function Gauntlet_NPC_DeathWait( entity npc, GauntletInfo gauntlet )
+{
+ gauntlet.signalEnt.EndSignal( "DisableGauntlet" )
+
+ npc.WaitSignal( "OnDeath" )
+
+ EmitSoundAtPosition( TEAM_UNASSIGNED, npc.GetOrigin(), "holopilot_impacts_training" )
+ npc.Dissolve( ENTITY_DISSOLVE_PHASESHIFT, Vector( 0, 0, 0 ), GAUNTLET_TARGET_DISSOLVE_TIME_MS )
+
+ if ( !gauntlet.isActive )
+ return
+
+ if ( gauntlet.runFinished )
+ return
+
+ gauntlet.enemiesKilled++
+
+ Remote_CallFunction_Replay( gauntlet.player, "ScriptCallback_Gauntlet_SetEnemyInfo", gauntlet.id, gauntlet.spawners.len(), gauntlet.enemiesKilled )
+}
+
+void function Gauntlet_UnfreezeNPC( entity npc )
+{
+ if ( !npc.IsFrozen() )
+ return
+
+ npc.Unfreeze()
+}
+
+string function EnableAllGauntlets()
+{
+ foreach ( idx, gauntlet in GetGauntlets() )
+ EnableGauntlet( gauntlet )
+
+ return( "All gauntlets enabled" )
+}
+
+string function DisableAllGauntlets()
+{
+ foreach ( idx, gauntlet in GetGauntlets() )
+ DisableGauntlet( gauntlet )
+
+ return( "All gauntlets disabled" )
+}
+
+void function Gauntlet_Think( GauntletInfo gauntlet )
+{
+ gauntlet.signalEnt.EndSignal( "DisableGauntlet" )
+
+ OnThreadEnd(
+ function() : ( gauntlet )
+ {
+ Gauntlet_ResetTrackerStats( gauntlet )
+
+ if ( gauntlet.player && !gauntlet.runFinished )
+ Gauntlet_AbortRun( gauntlet )
+ }
+ )
+
+ while ( 1 )
+ {
+ thread Gauntlet_StartTrigThink( gauntlet )
+
+ waitthread Gauntlet_WaitForPlayerToStart( gauntlet )
+
+ Gauntlet_ResetTrackerStats( gauntlet )
+
+ Gauntlet_StartRun( gauntlet )
+
+ thread Gauntlet_HandlePlayerForceRestart( gauntlet )
+
+ waitthread Gauntlet_WaitForStop( gauntlet )
+
+ waitthread Gauntlet_StopRun( gauntlet )
+ }
+}
+
+void function Gauntlet_StartRun( GauntletInfo gauntlet )
+{
+ printt( "Gauntlet Run Started for player " + gauntlet.player )
+
+ RestockPlayerAmmo( gauntlet.player )
+ EmitSoundOnEntityOnlyToPlayer( gauntlet.player, gauntlet.player, "training_scr_gaunlet_start" )
+
+ gauntlet.isActive = true
+ gauntlet.startTime = Time()
+ gauntlet.player.Signal( "Gauntlet_RunStarted" )
+ gauntlet.signalEnt.Signal( "Gauntlet_RunStarted" )
+ level.ui.playerRunningGauntlet = true
+
+ Remote_CallFunction_Replay( gauntlet.player, "ScriptCallback_Gauntlet_StartRun", gauntlet.id )
+ Remote_CallFunction_Replay( gauntlet.player, "ScriptCallback_Gauntlet_SetEnemyInfo", gauntlet.id, gauntlet.spawners.len(), 0 )
+
+ thread Gauntlet_SpawnNPCs( gauntlet )
+
+ thread Gauntlet_Checkpoints( gauntlet )
+}
+
+void function Gauntlet_StopRun( GauntletInfo gauntlet )
+{
+ gauntlet.isActive = false
+ level.ui.playerRunningGauntlet = false
+
+ string feedbackSound = ""
+
+ ResetPlayerHealthAndStatus( gauntlet.player )
+
+ if ( !gauntlet.runFinished )
+ {
+ Gauntlet_AbortRun( gauntlet )
+ feedbackSound = "training_scr_gaunlet_abort"
+ }
+ else
+ {
+ Gauntlet_FinishRun( gauntlet )
+
+ if ( gauntlet.lastRunDefeatedGhost )
+ feedbackSound = "training_scr_gaunlet_high_score"
+ else if ( gauntlet.lastRunBestTime )
+ feedbackSound = "training_scr_gaunlet_high_score"
+ else
+ feedbackSound = "training_scr_gaunlet_end"
+
+ if ( feedbackSound != "" && IsAlive( gauntlet.player ) )
+ EmitSoundOnEntityOnlyToPlayer( gauntlet.player, gauntlet.player, feedbackSound )
+ }
+
+ wait 0.1 // let the gauntlet finish and count NPCs remaining before killing the remainder
+ Gauntlet_ClearSpawnedNPCs( gauntlet )
+ thread ClearDroppedWeapons( GAUNTLET_TARGET_DISSOLVE_TIME + 0.1 ) // needs to be longer than gauntlet ghost dissolve time so weapons drop
+
+ if ( IsValid( gauntlet.player ) )
+ ClearActiveProjectilesForTeam( gauntlet.player.GetTeam() )
+
+ // need to wait before firing final signal, so this signal doesn't kill Gauntlet_HandlePlayerForceRestart
+ if ( IsValid( gauntlet.player ) )
+ gauntlet.player.Signal( "Gauntlet_RunStopped" )
+
+ if ( IsValid( gauntlet.signalEnt ) )
+ gauntlet.signalEnt.Signal( "Gauntlet_RunStopped" )
+
+ wait 0.1 // let other threads catch the signals and check the gauntlet struct before ResetTrackerStats
+}
+
+void function ResetPlayerHealthAndStatus( entity player )
+{
+ if ( !IsAlive( player ) )
+ return
+
+ player.SetHealth( player.GetMaxHealth() )
+
+ array<int> statusEffectsToStop = []
+ statusEffectsToStop.append( eStatusEffect.emp )
+ statusEffectsToStop.append( eStatusEffect.move_slow )
+ statusEffectsToStop.append( eStatusEffect.turn_slow )
+
+ foreach ( statusEffect in statusEffectsToStop )
+ {
+ if ( StatusEffect_Get( player, statusEffect ) > 0.0 )
+ StatusEffect_StopAll( player, statusEffect ) // arc grenade stun
+ }
+}
+
+void function Gauntlet_FinishRun( GauntletInfo gauntlet )
+{
+ RestockPlayerAmmo( gauntlet.player )
+
+ float elapsedTime = Time() - gauntlet.startTime
+ printt( "Gauntlet Run Finished, elapsed time", elapsedTime )
+
+ // time penalties for missed enemies
+ float enemiesMissedTimePenalty = 0.0
+ if ( gauntlet.spawners.len() > gauntlet.enemiesKilled )
+ {
+ int numEnemiesRemaining = gauntlet.spawners.len() - gauntlet.enemiesKilled
+ enemiesMissedTimePenalty = ( numEnemiesRemaining.tofloat() * GAUNTLET_ENEMY_MISSED_TIME_PENALTY )
+
+ elapsedTime += enemiesMissedTimePenalty
+ }
+
+ // check if new best time was set
+ gauntlet.lastRunTime = elapsedTime
+ if ( gauntlet.bestTime == -1.0 || elapsedTime < gauntlet.bestTime )
+ {
+ printt( "New best time!" )
+ gauntlet.bestTime = elapsedTime
+ gauntlet.lastRunBestTime = true
+
+ // if there's a player ghost (for leaderboard), update its duration
+ if ( gauntlet.hasPlayerGhost )
+ {
+ // update player ghost
+ GauntletGhost playerGhost = Gauntlet_GetPlayerGhost( gauntlet )
+ Gauntlet_SetGhostDuration( gauntlet, playerGhost, gauntlet.bestTime )
+ }
+ }
+
+ // did player beat a ghost racer?
+ if ( Gauntlet_HasActiveGhost( gauntlet ) )
+ {
+ GauntletGhost activeGhost = Gauntlet_GetActiveGhost( gauntlet )
+
+ if ( gauntlet.lastRunTime < activeGhost.duration )
+ {
+ printt( "player beat active ghost!" )
+ gauntlet.lastRunDefeatedGhost = true
+ }
+ }
+
+ Remote_CallFunction_Replay( gauntlet.player, "ScriptCallback_Gauntlet_FinishRun", gauntlet.id, elapsedTime, gauntlet.bestTime, enemiesMissedTimePenalty )
+}
+
+void function Gauntlet_AbortRun( GauntletInfo gauntlet )
+{
+ entity player = gauntlet.player
+ if ( !IsValid( player ) )
+ return
+
+ RestockPlayerAmmo_Silent( gauntlet.player )
+
+ EmitSoundOnEntityOnlyToPlayer( player, player, "training_scr_gaunlet_abort" )
+
+ Remote_CallFunction_Replay( player, "ScriptCallback_Gauntlet_AbortRun", gauntlet.id )
+}
+
+void function Gauntlet_WaitForPlayerToStart( GauntletInfo gauntlet )
+{
+ WaitSignal( gauntlet.signalEnt, "Gauntlet_PlayerHitStartTrig" )
+ Assert( IsValid( gauntlet.player ) )
+}
+
+entity function Gauntlet_StartTrigThink( GauntletInfo gauntlet )
+{
+ entity trigStart = gauntlet.trigStart
+ entity trigStart2 = gauntlet.trigStart2
+
+ EndSignal( gauntlet.signalEnt, "OnDestroy" )
+ EndSignal( trigStart, "OnDestroy" )
+ EndSignal( trigStart2, "OnDestroy" )
+ EndSignal( gauntlet.signalEnt, "Gauntlet_RunStopped" )
+
+ table result
+ string signal
+ entity player
+
+ //printt( "WaitForPlayerToHitStartTrig started" )
+
+ // "trigStart_OnStartTouch", "trigStart_OnEndTouch", "trigStart2_OnStartTouch", "trigStart2_OnEndTouch"
+ thread Gauntlet_PlayerStartSignals( gauntlet, trigStart, "trigStart_" )
+ thread Gauntlet_PlayerStartSignals( gauntlet, trigStart2, "trigStart2_" )
+
+ while ( 1 )
+ {
+ entity alreadyTouchingEnt = null
+ foreach ( p in GetPlayerArray() )
+ {
+ if ( trigStart.IsTouching( p ) && Gauntlet_EntCanActivateGauntletTrigger( p ) )
+ {
+ alreadyTouchingEnt = p
+ break
+ }
+ }
+
+ if ( IsValid( alreadyTouchingEnt ) )
+ {
+ player = alreadyTouchingEnt
+ }
+ else
+ {
+ //printt( "Waiting for trigStart OnStartTouch" )
+
+ result = WaitSignal( trigStart, "OnStartTouch" )
+ player = expect entity( result.activator )
+ }
+
+ if ( !Gauntlet_EntCanActivateGauntletTrigger( player ) )
+ continue
+
+ if ( !IsAlive( player ) )
+ continue
+
+ //printt( "WAITING for trigStart_OnEndTouch" )
+
+ while ( IsAlive( player ) )
+ {
+ WaitSignal( player, "trigStart_OnEndTouch" )
+
+ //printt( "RECEIVED trigStart_OnEndTouch" )
+
+ // player exited start trig without running gauntlet
+ if ( !trigStart2.IsTouching( player ) )
+ {
+ player.Signal( "Gauntlet_PlayerWentBackwardsThroughStartTrig" )
+ continue
+ }
+
+ //printt( "WAITING for trigStart_OnStartTouch or trigStart2_OnEndTouch" )
+
+ // player is now in trig2
+ result = WaitSignal( player, "trigStart_OnStartTouch", "trigStart2_OnEndTouch" )
+ signal = expect string( result.signal )
+ if ( signal == "trigStart2_OnEndTouch" )
+ {
+ //printt( "RECEIVED trigStart2_OnEndTouch" )
+
+ // player exited trig2 without touching trig1, so we know they started the gauntlet
+ if ( !trigStart.IsTouching( player ) )
+ {
+ //printt( "SENDING Gauntlet_PlayerHitStartTrig" )
+ gauntlet.signalEnt.Signal( "Gauntlet_PlayerHitStartTrig" )
+ gauntlet.player = player
+ }
+ }
+ }
+ }
+}
+
+void function Gauntlet_PlayerStartSignals( GauntletInfo gauntlet, entity trig, string signalPrefix )
+{
+ EndSignal( trig, "OnDestroy" )
+ EndSignal( gauntlet.signalEnt, "Gauntlet_RunStopped" )
+
+ /*
+ OnThreadEnd(
+ function() : ( )
+ {
+ printt( "Gauntlet_PlayerStartSignals ENDED" )
+ }
+ )
+
+ printt( "PlayerStartSignals started" )
+ */
+
+ while ( 1 )
+ {
+ table result = WaitSignal( trig, "OnStartTouch", "OnEndTouch" )
+ string signal = expect string( result.signal )
+ entity activator = expect entity( result.activator )
+
+ if ( !Gauntlet_EntCanActivateGauntletTrigger( activator ) )
+ continue
+
+ string outboundSignal = signalPrefix
+ if ( signal == "OnStartTouch" )
+ outboundSignal += "OnStartTouch"
+ else if ( signal == "OnEndTouch" )
+ outboundSignal += "OnEndTouch"
+
+ Assert( outboundSignal != signalPrefix )
+
+ Signal( activator, outboundSignal )
+ }
+}
+
+void function Gauntlet_WaitForStop( GauntletInfo gauntlet )
+{
+ gauntlet.player.EndSignal( "OnDeath" )
+ gauntlet.player.EndSignal( "Gauntlet_PlayerWentBackwardsThroughStartTrig" )
+ gauntlet.player.EndSignal( "Gauntlet_ForceRestart" )
+ gauntlet.signalEnt.EndSignal( "DisableGauntlet" )
+
+ table result
+ entity activator
+
+ while ( 1 )
+ {
+ result = gauntlet.trigFinish.WaitSignal( "OnStartTouch" )
+ activator = expect entity( result.activator )
+
+ if ( !activator.IsPlayer() )
+ continue
+
+ if ( activator != gauntlet.player )
+ continue
+
+ gauntlet.player.Signal( "Gauntlet_PlayerHitFinishTrig" )
+
+ gauntlet.runFinished = true
+ if ( gauntlet.checkpoints.len() && gauntlet.checkpointsHit < gauntlet.checkpoints.len() )
+ gauntlet.runFinished = false
+
+ break
+ }
+}
+
+bool function Gauntlet_EntCanActivateGauntletTrigger( entity ent )
+{
+ if ( !ent.IsPlayer() )
+ return false
+
+ if ( !IsAlive( ent ) )
+ return false
+
+ if ( ent.IsTitan() )
+ return false
+
+ return true
+}
+
+void function Gauntlet_HandlePlayerForceRestart( GauntletInfo gauntlet )
+{
+ gauntlet.player.EndSignal( "OnDestroy" )
+ gauntlet.player.EndSignal( "Gauntlet_RunStopped" )
+
+ gauntlet.player.WaitSignal( "Gauntlet_ForceRestart" )
+
+ thread Gauntlet_TeleportPlayerToStart( gauntlet )
+}
+
+void function Gauntlet_TeleportPlayerToStart( GauntletInfo gauntlet )
+{
+ entity player = gauntlet.player
+ entity startpoint = gauntlet.startpoint
+
+ if ( !IsAlive( player ) )
+ return
+
+ if ( !IsValid( startpoint ) )
+ return
+
+ EndSignal( player, "OnDestroy" )
+
+ // wait for quick death to finish before continuing
+ printt( "player doing quick death (1)?", player.p.doingQuickDeath )
+ while ( player.p.doingQuickDeath )
+ wait 0.1
+
+ //printt( "starting reset fade" )
+
+ float fadeTime = 0.1
+ float holdTime = 0.3
+ ScreenFadeToBlack( player, fadeTime, holdTime )
+ player.FreezeControlsOnServer()
+ player.SetVelocity( <0,0,0> )
+
+ OnThreadEnd(
+ function() : ( player, gauntlet )
+ {
+ if ( IsValid( player ) )
+ {
+ player.UnfreezeControlsOnServer()
+ player.UnforceStand()
+ thread Gauntlet_TeleportFailsafe( player, gauntlet )
+ }
+ }
+ )
+
+ wait fadeTime
+
+ // again, wait for quick death to finish before continuing since it could have started during fadeTime
+ printt( "player doing quick death (2)?", player.p.doingQuickDeath )
+ while ( player.p.doingQuickDeath )
+ wait 0.1
+
+ printt( "moving player back to start" )
+
+ player.FreezeControlsOnServer() // just in case they were unfrozen by quick death ending since we started waiting
+ player.SetOrigin( OriginToGround( startpoint.GetOrigin() + <0,0,1> ) )
+ player.SetAngles( startpoint.GetAngles() )
+ player.SetVelocity( <0,0,0> )
+ player.ForceStand()
+
+ wait holdTime
+}
+
+// HACK this is in case the quick death teleport happens on the exact same server frame as the gauntlet restart teleport
+void function Gauntlet_TeleportFailsafe( entity player, GauntletInfo gauntlet )
+{
+ // HACK this breaks in other levels that don't have the flag trigger
+ // in the future set this up as a gauntlet setting
+ if ( GetMapName() != "sp_training" )
+ return
+
+ EndSignal( player, "OnDestroy" )
+
+ wait 0.5
+
+ if ( !gauntlet.isActive && !Flag( "PlayerInGauntletEntryway" ) )
+ {
+ printt( "Gauntlet reset FAILSAFE!" )
+ thread Gauntlet_TeleportPlayerToStart( gauntlet )
+ }
+}
+
+bool function ClientCommand_Gauntlet_PlayerRestartedFromMenu( entity player, array<string> args )
+{
+ player.Signal( "Gauntlet_ForceRestart" )
+ return true
+}
+
+void function Gauntlet_CreateSignalEnt( GauntletInfo gauntlet )
+{
+ Assert( !IsValid( gauntlet.signalEnt ) )
+
+ entity signalEnt = CreateEntity( "info_target" )
+ DispatchSpawn( signalEnt )
+
+ gauntlet.signalEnt = signalEnt
+}
+
+void function Gauntlet_CleanupSignalEnt( GauntletInfo gauntlet )
+{
+ gauntlet.signalEnt.Destroy()
+ gauntlet.signalEnt = null
+}
+
+
+void function Gauntlet_ResetTrackerStats( GauntletInfo gauntlet )
+{
+ gauntlet.startTime = -1
+ gauntlet.runFinished = false
+ gauntlet.lastRunBestTime = false
+ gauntlet.lastRunDefeatedGhost = false
+ gauntlet.enemiesKilled = 0
+}
+
+
+// ===== GHOST RECORDINGS =====
+void function Gauntlet_StartGhostPlayback( GauntletInfo gauntlet, string ghostFileName, string ghostDisplayName = "" )//, bool waitForPlayerToStartFirstRun = true )
+{
+ gauntlet.signalEnt.Signal( "GhostAnimationPlayback_Start" )
+ gauntlet.signalEnt.EndSignal( "GhostAnimationPlayback_Start" )
+ gauntlet.signalEnt.EndSignal( "GhostAnimationPlayback_Stop" )
+ gauntlet.signalEnt.EndSignal( "DisableGauntlet" )
+
+ GauntletGhost ghostInfo = Gauntlet_GetGhostByFileName( gauntlet, ghostFileName )
+ var rec = LoadRecordedAnimation( ghostInfo.fileAsset )
+ float duration = GetRecordedAnimationDuration( rec )
+ printt( "duration is", duration )
+
+ Gauntlet_SetActiveGhostID( gauntlet, ghostInfo.id )
+
+ entity animRef = gauntlet.startpoint
+
+ bool createdIdleRef = false
+ entity idleRef
+ if ( gauntlet.ghostAttractSpot != null )
+ {
+ idleRef = gauntlet.ghostAttractSpot
+ }
+ else
+ {
+ createdIdleRef = true
+ idleRef = CreateScriptMover( animRef.GetOrigin(), animRef.GetAngles() )
+ DropToGround( idleRef )
+ }
+
+ table<int,entity> g = {}
+
+ OnThreadEnd(
+ function() : ( g, idleRef, createdIdleRef, gauntlet )
+ {
+ if ( IsValid( g[0] ) )
+ {
+ g[0].Anim_Stop()
+ StopSoundOnEntity( g[0], "PathHologram_Sustain_Loop_3P" )
+ DissolveGhost( g[0] )
+ }
+
+ if ( createdIdleRef && IsValid( idleRef ) )
+ idleRef.Destroy()
+
+ Gauntlet_ClearActiveGhost( gauntlet )
+ }
+ )
+
+ entity ghost
+ entity ghostWeapon
+
+ bool isFirstRun = true
+
+ while ( 1 )
+ {
+ if ( IsValid( ghost ) )
+ {
+ StopSoundOnEntity( ghost, "PathHologram_Sustain_Loop_3P" )
+ DissolveGhost( ghost )
+ }
+
+ ghost = CreateGhost( idleRef.GetOrigin(), ghostDisplayName )
+ g[0] <- ghost
+ //ghost.SetTitle( "Ghost Runner" )
+ //ShowName( ghost ) // not working
+
+ ghostWeapon = Ghost_GetWeaponEnt( ghost )
+ ghostWeapon.kv.VisibilityFlags = ENTITY_VISIBLE_TO_NOBODY
+
+ thread PlayAnimTeleport( ghost, "pt_OG_training_stand", idleRef )
+
+ if ( !gauntlet.isActive )
+ gauntlet.signalEnt.WaitSignal( "Gauntlet_RunStarted" )
+
+ float startTime = Time()
+
+ ghostWeapon.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+
+ EmitSoundOnEntity( ghost, "PathHologram_Sustain_Loop_3P" )
+
+ ghost.Anim_Stop()
+ ghost.PlayRecordedAnimation( rec, <0,0,0>, <0,0,0>, DEFAULT_SCRIPTED_ANIMATION_BLEND_TIME, animRef )
+
+ thread GhostPlayback_HideGhostIfPlayerIsNear( ghost, ghostWeapon )
+
+ float ghostFadeTime = 1.2
+ float waitBeforeFade = duration - ghostFadeTime
+ WaitSignalTimeout( gauntlet.signalEnt, waitBeforeFade, "Gauntlet_RunStopped" )
+
+ // ended prematurely
+ if ( Time() - startTime < waitBeforeFade )
+ ghost.Anim_Stop()
+
+ isFirstRun = false
+ }
+}
+
+void function GhostPlayback_HideGhostIfPlayerIsNear( entity ghost, entity ghostWeapon )
+{
+ EndSignal( ghost, "OnDestroy" )
+ EndSignal( ghostWeapon, "OnDestroy" )
+
+ const float TICK_WAIT = 0.1
+
+ while ( 1 )
+ {
+ wait TICK_WAIT
+
+ entity nearbyPlayer
+
+ array<entity> players = GetPlayerArray()
+ foreach ( player in players )
+ {
+ if ( !IsAlive( player ) )
+ continue
+
+ if ( PlayerTooCloseToGhost( player, ghost ) )
+ {
+ nearbyPlayer = player
+ break
+ }
+ }
+
+ if ( IsValid( nearbyPlayer ) )
+ {
+ ghost.kv.VisibilityFlags = ENTITY_VISIBLE_TO_NOBODY
+ ghostWeapon.kv.VisibilityFlags = ENTITY_VISIBLE_TO_NOBODY
+
+ while ( PlayerTooCloseToGhost( nearbyPlayer, ghost ) )
+ wait TICK_WAIT
+
+ ghost.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ ghostWeapon.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE
+ }
+
+ }
+}
+
+bool function PlayerTooCloseToGhost( entity player, entity ghost )
+{
+ if ( !IsAlive( player ) )
+ return false
+
+ const float CLOSE_DIST = 64.0
+
+ if ( Distance( player.EyePosition(), ghost.GetOrigin() ) <= CLOSE_DIST )
+ return true
+
+ if ( Distance( player.EyePosition(), ghost.EyePosition() ) <= CLOSE_DIST )
+ return true
+
+ return false
+}
+
+void function Gauntlet_StopGhostPlayback( GauntletInfo gauntlet )
+{
+ gauntlet.signalEnt.Signal( "GhostAnimationPlayback_Stop" )
+}
+
+
+// - Player climbs the leaderboard as her best run time improves
+// - Skips challenging ghosts whose times are worse than the player's
+void function Gauntlet_ChallengeLeaderboardGhosts( entity player, GauntletInfo gauntlet, string endFlag )
+{
+ if ( Flag( endFlag ) )
+ return
+
+ FlagEnd( endFlag )
+
+ player.EndSignal( "OnDestroy" )
+ gauntlet.signalEnt.EndSignal( "OnDestroy" )
+
+ GauntletGhost playerGhost = Gauntlet_GetPlayerGhost( gauntlet )
+
+ int currPlayerIdx = GAUNTLET_LEADERBOARD_MAX_ENTRIES - 1
+ int nextGhostIdx = currPlayerIdx - 1
+
+ while ( currPlayerIdx > 0 )
+ {
+ array<GauntletGhost> leaderboard = Gauntlet_GetLeaderboard( gauntlet )
+
+ // get current player leaderboard position
+ int maxLeaderboardIdx = leaderboard.len() - 1
+ if ( currPlayerIdx >= maxLeaderboardIdx )
+ currPlayerIdx = maxLeaderboardIdx
+
+ foreach ( idx, leaderboardGhost in leaderboard )
+ {
+ if ( leaderboardGhost.fileName == playerGhost.fileName )
+ currPlayerIdx = idx
+ }
+
+ // player is top of the leaderboard, stop racing ghosts
+ if ( currPlayerIdx <= 0 )
+ break
+
+ // if player is not top of leaderboard, cue the ghost above player leaderboard position
+ int nextGhostIdx = currPlayerIdx - 1
+ GauntletGhost ghost = leaderboard[ nextGhostIdx ]
+
+ Assert( ghost.fileName != GHOST_NAME_PLAYER, "Can't race against own player ghost- no anim recording asset" )
+
+ thread Gauntlet_StartGhostPlayback( gauntlet, ghost.fileName, ghost.displayName )
+
+ if ( !gauntlet.isActive )
+ WaitSignal( player, "Gauntlet_RunStarted" )
+
+ // wait for run to stop
+ WaitSignal( player, "Gauntlet_RunStopped" )
+ }
+
+ Gauntlet_ClearActiveGhost( gauntlet )
+
+ gauntlet.allGhostsDefeated = true
+}
+
+#if DEV
+void function Gauntlet_Player_GhostRecordOrPlayback( entity player, GauntletInfo gauntlet, string ghostFileName )
+{
+ if ( GetBugReproNum() == 55 )
+ {
+ thread Gauntlet_Player_StartRecordingGhost( player, gauntlet, ghostFileName )
+ }
+ else
+ {
+ thread Gauntlet_StartGhostPlayback( gauntlet, ghostFileName )
+
+ GauntletGhost ghost = Gauntlet_GetGhostByFileName( gauntlet, ghostFileName )
+ Dev_PrintMessage( player, "Ghost Playback:", "TO RECORD, set bug_reproNum 55", 4.0 )
+ wait 4.0
+ Dev_PrintMessage( player, ghost.displayName, "TO RECORD, set bug_reproNum 55", 4.0 )
+ }
+}
+
+void function Gauntlet_Player_StartRecordingGhost( entity player, GauntletInfo gauntlet, string ghostFileName )
+{
+ player.Signal( "RecordAnimation_Start" )
+ player.EndSignal( "RecordAnimation_Start" )
+ player.EndSignal( "OnDestroy" )
+
+ entity animRef = gauntlet.startpoint
+ GauntletGhost ghost = Gauntlet_GetGhostByFileName( gauntlet, ghostFileName )
+
+ thread Gauntlet_StartGhostPlayback( gauntlet, ghostFileName )
+
+ while ( 1 )
+ {
+ #if PC_PROG
+ thread Gauntlet_Player_StartRecordingGhost_Hints( player, gauntlet, ghost )
+ #endif
+
+ printt( "READY TO RECORD:", ghost.fileName )
+
+ player.WaitSignal( "Gauntlet_RunStarted" )
+
+ player.StartRecordingAnimation( animRef.GetOrigin(), animRef.GetAngles() )
+ printt( "RECORDING STARTED:", ghost.fileName )
+
+ player.WaitSignal( "Gauntlet_RunStopped" )
+
+ var recording = player.StopRecordingAnimation()
+
+ if ( !gauntlet.runFinished )
+ continue
+
+ if ( gauntlet.enemiesKilled < gauntlet.spawners.len() )
+ {
+ Dev_PrintMessage( player, "RECORDING NOT SAVED!", "Must kill all the enemies on your run to save.", 7.0 )
+ printt( "!!!! RECORDED ANIM NOT SAVED!!!!" )
+ continue
+ }
+
+ #if PC_PROG
+ SaveRecordedAnimation( recording, ghost.fileName )
+ Dev_PrintMessage( player, "Anim Data Saved", "BAKE and CLEAR BUG REPRO NUM and RELOAD LEVEL to play it back.", 5.5 )
+ printt( "RECORDED ANIM SAVED:", ghost.fileName )
+
+ wait 5.5
+
+ thread Gauntlet_StartGhostPlayback( gauntlet, ghostFileName )
+ #endif
+ }
+}
+
+void function Gauntlet_Player_StartRecordingGhost_Hints( entity player, GauntletInfo gauntlet, GauntletGhost ghost )
+{
+ player.Signal( "Player_StartRecordingGhost_HintStart" )
+ player.EndSignal( "Player_StartRecordingGhost_HintStart" )
+ player.EndSignal( "OnDestroy" )
+
+ Dev_PrintMessage( player, "Ready To Record Ghost:", "FINISH Gauntlet and kill ALL TARGETS to SAVE GHOST.", 3.0 )
+ wait 3.0
+ Dev_PrintMessage( player, ghost.displayName, "FINISH Gauntlet and kill ALL TARGETS to SAVE GHOST.", 5.0 )
+}
+#endif //DEV \ No newline at end of file