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 const int EVAC_SHIP_HEALTH = 25000 const int EVAC_SHIP_SHIELDS = 2000 // 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 // this only supports 1 evac at once atm, is this ideal? entity currentEvacNode entity evacDropship entity evacIcon } file struct EvacShipSetting { asset shipModel string flyinSound string hoverSound string flyoutSound } void function Evac_Init() { EvacShared_Init() RegisterSignal( "EvacShipLeaves" ) RegisterSignal( "EvacOver" ) PrecacheParticleSystem( FX_EVAC_MARKER ) } 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 ) // no players can evac? end match thread CheckIfAnyPlayerLeft( GetOtherTeam( winner ) ) 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 EvacSpectatorFunc( entity player ) { svGlobal.levelEnt.EndSignal( "GameStateChanged" ) file.evacDropship.EndSignal( "OnDestroy" ) entity cam = GetEnt( expect string( file.currentEvacNode.kv.target ) ) if ( !IsValid( cam ) ) return player.SetObserverModeStaticPosition( cam.GetOrigin() ) player.SetObserverModeStaticAngles( cam.GetAngles() ) player.StartObserverMode( OBS_MODE_STATIC ) file.evacDropship.WaitSignal( "EvacOver" ) } void function SetRespawnAndWait( bool mode ) { wait GAME_EPILOGUE_PLAYER_RESPAWN_LEEWAY SetRespawnsEnabled( mode ) // clear any respawn availablity, or players are able to save there respawn for whenever they want foreach( entity player in GetPlayerArray() ) ClearRespawnAvailable( player ) } 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 if( GetGameState() != eGameState.Postmatch ) 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 ) { // get evac ship sound and model for specific team EvacShipSetting evacShip = GetEvacShipSettingByTeam( evacTeam ) 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 = customEvacNode if ( !IsValid( customEvacNode ) ) evacNode = file.evacNodes.getrandom() file.currentEvacNode = evacNode // 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() // start evac beam int index = GetParticleSystemIndex( FX_EVAC_MARKER ) entity effectFriendly = StartParticleEffectInWorld_ReturnEntity( index, evacNode.GetOrigin(), < 0,0,0 > ) SetTeam( effectFriendly, evacTeam ) effectFriendly.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY 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() ) thread DropShipTempHide( dropship ) // prevent showing model and health bar on spawn dropship.SetModel( evacShip.shipModel ) dropship.SetValueForModelKey( evacShip.shipModel ) dropship.SetMaxHealth( EVAC_SHIP_HEALTH ) dropship.SetHealth( EVAC_SHIP_HEALTH ) dropship.SetShieldHealth( EVAC_SHIP_SHIELDS ) SetTargetName( dropship, "#NPC_EVAC_DROPSHIP" ) DispatchSpawn( dropship ) // reduce nuclear core's damage AddEntityCallback_OnDamaged( dropship, EvacDropshipDamaged ) 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 if( !IsValid( dropship ) ) { 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 Spectator_SetCustomSpectatorFunc( EvacSpectatorFunc ) thread PlayAnim( dropship, "cd_dropship_rescue_side_start", evacNode ) // fly in sound and effect EmitSoundOnEntity( dropship, evacShip.flyinSound ) thread WarpInEffectEvacShip( dropship ) // 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 ) // hover sound EmitSoundOnEntity( dropship, evacShip.hoverSound ) // eta until leave SetTeamActiveObjective( evacTeam, "EG_DropshipExtract2", Time() + waitTime, file.evacIcon ) SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtract2", Time() + waitTime, file.evacIcon ) // dialogue PlayFactionDialogueToTeam( "mp_evacGo", evacTeam ) PlayFactionDialogueToTeam( "mp_evacStop", GetOtherTeam( evacTeam ) ) // stop evac beam if( IsValid( effectFriendly ) ) EffectStop( effectFriendly ) // 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 ) || player.IsTitan() || player.ContextAction_IsBusy() || !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() } // fly out sound StopSoundOnEntity( dropship, evacShip.hoverSound ) EmitSoundOnEntity( dropship, evacShip.flyoutSound ) // 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 dropship.kv.VisibilityFlags = 0 // prevent jetpack trails being like "dive" into ground WaitFrame() // better wait because we are server if( !IsValid( dropship ) ) return thread __WarpOutEffectShared( dropship ) // 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() dropship.Signal( "EvacOver" ) 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" ) // let evacing team able to see the ship again dropship.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY // skybox player.SetSkyCamera( GetEnt( SKYBOXSPACE ) ) 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 // need to cancel if the dropship dies dropship.EndSignal( "OnDeath", "OnDestroy" ) 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 ) 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 } ) } } } // if there's no player left in evacing team, we end this match void function CheckIfAnyPlayerLeft( int evacTeam ) { wait GAME_EPILOGUE_PLAYER_RESPAWN_LEEWAY float startTime = Time() OnThreadEnd( function() : ( evacTeam ) { SetTeamActiveObjective( evacTeam, "EG_DropshipExtractEvacPlayersKilled" ) SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtractEvacPlayersKilled" ) thread EvacEpilogueCompleted( null ) } ) while( true ) { if( GetPlayerArrayOfTeam_Alive( evacTeam ).len() == 0 ) break if( GetGameState() == eGameState.Postmatch ) return WaitFrame() } } void function DropShipTempHide( entity dropship ) { dropship.kv.VisibilityFlags = 0 // or it will still shows the jetpack fxs HideName( dropship ) wait 0.46 if( IsValid( dropship ) ) { dropship.kv.VisibilityFlags = ENTITY_VISIBLE_TO_EVERYONE ShowName( dropship ) } } EvacShipSetting function GetEvacShipSettingByTeam( int team ) { EvacShipSetting tempSetting tempSetting.shipModel = $"models/vehicle/goblin_dropship/goblin_dropship_hero.mdl" tempSetting.flyinSound = "Goblin_IMC_Evac_Flyin" tempSetting.hoverSound = "Goblin_IMC_Evac_Hover" tempSetting.flyoutSound = "Goblin_IMC_Evac_FlyOut" if( team == TEAM_MILITIA ) { tempSetting.shipModel = $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" tempSetting.flyinSound = "Crow_MCOR_Evac_Flyin" tempSetting.hoverSound = "Crow_MCOR_Evac_Hover" tempSetting.flyoutSound = "Crow_MCOR_Evac_Flyout" } return tempSetting } void function EvacDropshipDamaged( entity evacShip, var damageInfo ) { int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo ) if( damageSourceID == damagedef_nuclear_core ) DamageInfo_SetDamage( damageInfo, DamageInfo_GetDamage( damageInfo ) * EVAC_SHIP_DAMAGE_MULTIPLIER_AGAINST_NUCLEAR_CORE ) } void function WarpInEffectEvacShip( entity dropship ) { dropship.EndSignal( "OnDestroy" ) float sfxWait = 0.1 float totalTime = WARPINFXTIME float preWaitTime = 0.16 // give it some time so it's actually playing anim, and we can get it's "origin" attatch for playing warp in effect string sfx = "dropship_warpin" wait preWaitTime int attach = dropship.LookupAttachment( "origin" ) vector origin = dropship.GetAttachmentOrigin( attach ) vector angles = dropship.GetAttachmentAngles( attach ) entity fx = PlayFX( FX_GUNSHIP_CRASH_EXPLOSION_ENTRANCE, origin, angles ) fx.FXEnableRenderAlways() fx.DisableHibernation() wait sfxWait EmitSoundAtPosition( TEAM_UNASSIGNED, origin, sfx ) wait totalTime - sfxWait }