untyped global function Evac_Init global function AddEvacNode global function SetEvacSpaceNode global function IsEvacDropship global function EvacEpilogueSetup global function Evac const float EVAC_INITIAL_WAIT = 5.0 const float EVAC_ARRIVAL_TIME = 40.0 const float EVAC_WAIT_TIME = 18.0 // we don't use these because they're busted, just keeping them const array EVAC_EMBARK_ANIMS_1P = [ "ptpov_e3_rescue_side_embark_A", "ptpov_e3_rescue_side_embark_B", "ptpov_e3_rescue_side_embark_C", "ptpov_e3_rescue_side_embark_D", "ptpov_e3_rescue_side_embark_E", "ptpov_e3_rescue_side_embark_F", "ptpov_e3_rescue_side_embark_G", "ptpov_e3_rescue_side_embark_H" ] const array EVAC_EMBARK_ANIMS_3P = [ "pt_e3_rescue_side_embark_A", "pt_e3_rescue_side_embark_B", "pt_e3_rescue_side_embark_C", "pt_e3_rescue_side_embark_D", "pt_e3_rescue_side_embark_E", "pt_e3_rescue_side_embark_F", "pt_e3_rescue_side_embark_G", "pt_e3_rescue_side_embark_H" ] const array EVAC_IDLE_ANIMS_1P = [ "ptpov_e3_rescue_side_embark_A_idle", "ptpov_e3_rescue_side_embark_B_idle", "ptpov_e3_rescue_side_embark_C_idle", "ptpov_e3_rescue_side_embark_D_idle", "ptpov_e3_rescue_side_embark_E_idle", "ptpov_e3_rescue_side_embark_F_idle", "ptpov_e3_rescue_side_embark_G_idle", "ptpov_e3_rescue_side_embark_H_idle" ] const array EVAC_IDLE_ANIMS_3P = [ "pt_e3_rescue_side_idle_A", "pt_e3_rescue_side_idle_B", "pt_e3_rescue_side_idle_C", "pt_e3_rescue_side_idle_D", "pt_e3_rescue_side_idle_E", "pt_e3_rescue_side_idle_F", "pt_e3_rescue_side_idle_G", "pt_e3_rescue_side_idle_H" ] struct { array evacNodes entity spaceNode entity evacDropship entity evacIcon } file void function Evac_Init() { EvacShared_Init() RegisterSignal( "EvacShipLeaves" ) } void function AddEvacNode( entity evacNode ) { file.evacNodes.append( evacNode ) } void function SetEvacSpaceNode( entity spaceNode ) { file.spaceNode = spaceNode } bool function IsEvacDropship( entity ent ) { return file.evacDropship == ent && IsValid( file.evacDropship ) } // evac epilogue void function EvacEpilogueSetup() { // don't do much here because other scripts should be able to call evac stuff themselves AddCallback_GameStateEnter( eGameState.Epilogue, EvacEpilogue ) } void function EvacEpilogue() { int winner = GetWinningTeam() // make sure we don't run evac if it won't be supported for current map/gamestate bool canRunEvac = GetCurrentPlaylistVarInt( "max_teams", 2 ) == 2 && ( winner == TEAM_MILITIA || winner == TEAM_IMC ) && ( file.evacNodes.len() > 0 || IsValid( GetEnt( "escape_node1" ) ) ) // code assumes escape_node1 should be first node if automatically registering if ( canRunEvac ) { thread SetRespawnAndWait( false ) thread Evac( GetOtherTeam( winner ), EVAC_INITIAL_WAIT, EVAC_ARRIVAL_TIME, EVAC_WAIT_TIME, EvacEpiloguePlayerCanBoard, EvacEpilogueShouldLeaveEarly, EvacEpilogueCompleted ) } else { thread SetRespawnAndWait( false ) //prevent respawns during the fade to black, should only be an issue if the match is a draw thread EvacEpilogueCompleted( null ) //this is hacky but like, this also shouldn't really be hit in normal gameplay } } void function SetRespawnAndWait(bool mode) { wait GAME_EPILOGUE_PLAYER_RESPAWN_LEEWAY SetRespawnsEnabled( mode ) } bool function EvacEpiloguePlayerCanBoard( entity dropship, entity player ) { // can't board a dropship on a different team if ( dropship.GetTeam() != player.GetTeam() ) return false // check if there are any free slots on the dropship, if there are then they can board foreach ( entity player in dropship.s.evacSlots ) if ( !IsValid( player ) ) return true // no empty slots return false } bool function EvacEpilogueShouldLeaveEarly( entity dropship ) { int numEvacing foreach ( entity player in dropship.s.evacSlots ) if ( IsValid( player ) ) numEvacing++ return GetPlayerArrayOfTeam_Alive( dropship.GetTeam() ).len() == numEvacing || numEvacing == dropship.s.evacSlots.len() } void function EvacEpilogueCompleted( entity dropship ) { wait 5.0 foreach ( entity player in GetPlayerArray() ) ScreenFadeToBlackForever( player, 2.0 ) wait 2.0 SetGameState( eGameState.Postmatch ) } // global evac func, anything can call this, it's not necessarily an epilogue thing void function Evac( int evacTeam, float initialWait, float arrivalTime, float waitTime, bool functionref( entity, entity ) canBoardCallback, bool functionref( entity ) shouldLeaveEarlyCallback, void functionref( entity ) completionCallback, entity customEvacNode = null ) { wait initialWait // setup evac nodes if not manually registered if ( file.evacNodes.len() == 0 && !IsValid( customEvacNode ) ) { for ( int i = 1; ; i++ ) { entity newNode = GetEnt( "escape_node" + i ) if ( !IsValid( newNode ) ) break file.evacNodes.append( newNode ) } } // setup space node if not manually registered if ( !IsValid( file.spaceNode ) ) file.spaceNode = GetEnt( "spaceNode" ) entity evacNode if ( !IsValid( customEvacNode ) ) evacNode = file.evacNodes.getrandom() // setup client evac position file.evacIcon = CreateEntity( "info_target" ) file.evacIcon.SetOrigin( evacNode.GetOrigin() ) file.evacIcon.kv.spawnFlags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT DispatchSpawn( file.evacIcon ) file.evacIcon.DisableHibernation() wait 0.5 // need to wait here, or the target won't appear on clients for some reason // eta until arrive SetTeamActiveObjective( evacTeam, "EG_DropshipExtract", Time() + arrivalTime, file.evacIcon ) SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtract", Time() + arrivalTime, file.evacIcon ) // would've liked to use cd_dropship_rescue_side_start length here, but can't since this is done before dropship spawn, can't wait arrivalTime - 4.33333 entity dropship = CreateDropship( evacTeam, evacNode.GetOrigin(), evacNode.GetAngles() ) dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) dropship.SetValueForModelKey( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) DispatchSpawn( dropship ) dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) AddEntityCallback_OnKilled( dropship, EvacDropshipKilled ) dropship.s.evacSlots <- [ null, null, null, null, null, null, null, null ] file.evacDropship = dropship dropship.EndSignal( "OnDestroy" ) OnThreadEnd( function() : ( evacTeam, completionCallback, dropship ) { if ( "evacTrigger" in dropship.s ) dropship.s.evacTrigger.Destroy() // this should be for both teams SetTeamActiveObjective( evacTeam, "EG_DropshipExtractDropshipDestroyed" ) SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_DropshipExtractDropshipDestroyed" ) foreach ( entity player in dropship.s.evacSlots ) { if ( !IsValid( player ) ) continue player.ClearInvulnerable() } // this is called whether dropship is destroyed or evac finishes, callback can handle this itself thread completionCallback( dropship ) }) // flyin thread PlayAnim( dropship, "cd_dropship_rescue_side_start", evacNode ) // calculate time until idle start float sequenceDuration = dropship.GetSequenceDuration( "cd_dropship_rescue_side_start" ) float cycleFrac = dropship.GetScriptedAnimEventCycleFrac( "cd_dropship_rescue_side_start", "ReadyToLoad" ) wait sequenceDuration * cycleFrac thread PlayAnim( dropship, "cd_dropship_rescue_side_idle", evacNode ) // eta until leave SetTeamActiveObjective( evacTeam, "EG_DropshipExtract2", Time() + EVAC_WAIT_TIME, file.evacIcon ) SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtract2", Time() + EVAC_WAIT_TIME, file.evacIcon ) // setup evac trigger entity trigger = CreateEntity( "trigger_cylinder" ) trigger.SetRadius( 150 ) trigger.SetAboveHeight( 100 ) trigger.SetBelowHeight( 100 ) trigger.SetOrigin( dropship.GetOrigin() ) trigger.SetParent( dropship, "ORIGIN" ) DispatchSpawn( trigger ) // have to do this inline since we capture the completionCallback trigger.SetEnterCallback( void function( entity trigger, entity player ) : ( canBoardCallback, dropship ) { if ( !player.IsPlayer() || !IsAlive( player ) || !canBoardCallback( dropship, player ) || PlayerInDropship( player, dropship ) ) return thread AddPlayerToEvacDropship( dropship, player ) }) dropship.s.evacTrigger <- trigger float waitStartTime = Time() while ( Time() - waitStartTime < waitTime ) { if ( shouldLeaveEarlyCallback( dropship ) ) break WaitFrame() } // holster all weapons foreach ( entity player in dropship.s.evacSlots ) if ( IsValid( player ) ) player.HolsterWeapon() // fly away dropship.Signal( "EvacShipLeaves" ) thread PlayAnim( dropship, "cd_dropship_rescue_side_end", evacNode ) SetTeamActiveObjective( evacTeam, "EG_DropshipExtractDropshipFlyingAway" ) SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtractDropshipFlyingAway" ) // todo: play the warpout effect somewhere here wait dropship.GetSequenceDuration( "cd_dropship_rescue_side_end" ) - WARPINFXTIME foreach ( entity player in dropship.s.evacSlots ) if ( IsValid( player ) ) Remote_CallFunction_NonReplay( player, "ServerCallback_PlayScreenFXWarpJump" ) wait WARPINFXTIME // go to space // hardcoded angles here are a hack, spacenode position doesn't face the planet in the skybox, for some reason // nvm removing for now //file.spaceNode.SetAngles( < 30, -75, 20 > ) dropship.SetOrigin( file.spaceNode.GetOrigin() ) dropship.SetAngles( file.spaceNode.GetAngles() ) dropship.SetInvulnerable() thread PlayAnim( dropship, "ds_space_flyby_dropshipA", file.spaceNode ) foreach( entity player in GetPlayerArray() ) { // evac-ed players only beyond this point if ( !PlayerInDropship( player, dropship ) ) { if ( player.GetTeam() == dropship.GetTeam() ) SetPlayerActiveObjective( player, "EG_DropshipExtractFailedEscape" ) continue } SetPlayerActiveObjective( player, "EG_DropshipExtractSuccessfulEscape" ) // skybox player.SetSkyCamera( GetEnt( "skybox_cam_intro" ) ) Remote_CallFunction_NonReplay( player, "ServerCallback_DisableHudForEvac" ) Remote_CallFunction_NonReplay( player, "ServerCallback_SetClassicSkyScale", dropship.GetEncodedEHandle(), 0.7 ) Remote_CallFunction_NonReplay( player, "ServerCallback_SetMapSettings", 4.0, false, 0.4, 0.125 ) // display player [Evacuated] in killfeed foreach ( entity otherPlayer in GetPlayerArray() ) Remote_CallFunction_NonReplay( otherPlayer, "ServerCallback_EvacObit", player.GetEncodedEHandle() ) } } void function AddPlayerToEvacDropship( entity dropship, entity player ) { int slot = RandomInt( dropship.s.evacSlots.len() ) for ( int i = 0; i < dropship.s.evacSlots.len(); i++ ) { if ( !IsValid( dropship.s.evacSlots[ slot ] ) ) { dropship.s.evacSlots[ slot ] = player break } slot = ( slot + 1 ) % expect int( dropship.s.evacSlots.len() ) } // no slots available if ( !PlayerInDropship( player, dropship ) ) return player.SetInvulnerable() player.UnforceCrouch() player.ForceStand() FirstPersonSequenceStruct fp //fp.firstPersonAnim = EVAC_EMBARK_ANIMS_1P[ slot ] fp.thirdPersonAnim = EVAC_EMBARK_ANIMS_3P[ slot ] fp.attachment = "RESCUE" fp.teleport = true fp.thirdPersonCameraAttachments = [ "VDU" ] // this seems wrong, firstperson anim has better angles, but no head EmitSoundOnEntityOnlyToPlayer( player, player, SHIFTER_START_SOUND_3P ) // should play SHIFTER_START_SOUND_1P when they actually arrive in the ship i think, unsure how this is supposed to be done PlayPhaseShiftDisappearFX( player ) waitthread FirstPersonSequence( fp, player, dropship ) FirstPersonSequenceStruct idleFp idleFp.firstPersonAnimIdle = EVAC_IDLE_ANIMS_1P[ slot ] idleFp.thirdPersonAnimIdle = EVAC_IDLE_ANIMS_3P[ slot ] idleFp.attachment = "RESCUE" idleFp.teleport = true idleFp.hideProxy = true idleFp.viewConeFunction = ViewConeWide thread FirstPersonSequence( idleFp, player, dropship ) ViewConeWide( player ) // gotta do this after for some reason, adding it to idleFp does not work for some reason } bool function PlayerInDropship( entity player, entity dropship ) { // couldn't get "player in dropship.s.evacSlots" to work for some reason, likely due to static typing? foreach ( entity dropshipPlayer in dropship.s.evacSlots ) if ( dropshipPlayer == player ) return true return false } void function EvacDropshipKilled( entity dropship, var damageInfo ) { foreach ( entity player in dropship.s.evacSlots ) { if ( IsValid( player ) ) { player.ClearParent() player.Die( DamageInfo_GetAttacker( damageInfo ), DamageInfo_GetWeapon( damageInfo ), { damageSourceId = eDamageSourceId.evac_dropship_explosion, scriptType = DF_GIB } ) } } }