untyped // this needs a refactor lol global function CaptureTheFlag_Init global function RateSpawnpoints_CTF const array SWAP_FLAG_MAPS = [ "mp_forwardbase_kodai", "mp_lf_meadow" ] struct { entity imcFlagSpawn entity imcFlag entity imcFlagReturnTrigger entity militiaFlagSpawn entity militiaFlag entity militiaFlagReturnTrigger array imcCaptureAssistList array militiaCaptureAssistList } file void function CaptureTheFlag_Init() { PrecacheModel( CTF_FLAG_MODEL ) PrecacheModel( CTF_FLAG_BASE_MODEL ) CaptureTheFlagShared_Init() SetSwitchSidesBased( true ) SetSuddenDeathBased( true ) SetShouldUseRoundWinningKillReplay( true ) SetRoundWinningKillReplayKillClasses( false, false ) // make these fully manual AddCallback_OnClientConnected( CTFInitPlayer ) AddCallback_GameStateEnter( eGameState.Prematch, CreateFlags ) AddCallback_OnTouchHealthKit( "item_flag", OnFlagCollected ) AddCallback_OnPlayerKilled( OnPlayerKilled ) AddCallback_OnPilotBecomesTitan( DropFlagForBecomingTitan ) RegisterSignal( "FlagReturnEnded" ) RegisterSignal( "ResetDropTimeout" ) // setup stuff for the functions in sh_gamemode_ctf // don't really like using level for stuff but just how it be level.teamFlags <- {} // setup score event earnmeter values ScoreEvent_SetEarnMeterValues( "KillPilot", 0.05, 0.20 ) ScoreEvent_SetEarnMeterValues( "Headshot", 0.0, 0.02 ) ScoreEvent_SetEarnMeterValues( "FirstStrike", 0.0, 0.05 ) ScoreEvent_SetEarnMeterValues( "KillTitan", 0.0, 0.25 ) ScoreEvent_SetEarnMeterValues( "PilotBatteryStolen", 0.0, 0.35 ) ScoreEvent_SetEarnMeterValues( "FlagCarrierKill", 0.0, 0.20 ) ScoreEvent_SetEarnMeterValues( "FlagTaken", 0.0, 0.10 ) ScoreEvent_SetEarnMeterValues( "FlagCapture", 0.0, 0.30 ) ScoreEvent_SetEarnMeterValues( "FlagCaptureAssist", 0.0, 0.20 ) ScoreEvent_SetEarnMeterValues( "FlagReturn", 0.0, 0.20 ) } void function RateSpawnpoints_CTF( int checkClass, array spawnpoints, int team, entity player ) { // ok this is the 3rd time rewriting this due to not understanding ctf spawns properly // legit just // if there are no enemies in base, spawn them in base // if there are, spawn them outside of it ( but ideally still close ) // max distance away should be like, angel city markets array startSpawns = SpawnPoints_GetPilotStart( team ) array enemyPlayers = GetPlayerArrayOfTeam_Alive( GetOtherTeam( team ) ) vector startSpawnAverage bool enemyInBase = false foreach ( entity startSpawn in startSpawns ) { startSpawnAverage += startSpawn.GetOrigin() foreach ( entity enemy in enemyPlayers ) { if ( Distance( startSpawn.GetOrigin(), enemy.GetOrigin() ) <= 1000.0 ) { enemyInBase = true break } } } startSpawnAverage /= startSpawns.len() print( "spawn for " + player + " is there an enemy in base?" + enemyInBase ) foreach ( entity spawn in spawnpoints ) { float rating = 0.0 bool isStart = false foreach ( entity startSpawn in startSpawns ) { if ( Distance2D( spawn.GetOrigin(), startSpawn.GetOrigin() ) < 1500.0 ) // this was for some reason the only distance i could get to work { isStart = true break } } if ( isStart ) { if ( !enemyInBase ) rating = 1000 + RandomFloat( 100.0 ) else rating = -1000.0 } else if ( !isStart && enemyInBase ) { entity friendlyFlag entity enemyFlag if ( team == TEAM_IMC ) { friendlyFlag = file.imcFlagSpawn enemyFlag = file.militiaFlagSpawn } else { friendlyFlag = file.militiaFlagSpawn enemyFlag = file.imcFlagSpawn } float dist = Distance2D( spawn.GetOrigin(), enemyFlag.GetOrigin() ) float flagDist = Distance2D( startSpawnAverage, enemyFlag.GetOrigin() ) if ( dist < ( flagDist / 2 ) ) // spawns shouldn't be closer to enemies than they are to us rating = -1000.0 if ( dist > flagDist * 1.1 ) // spawn is behind startspawns rating = -1000.0 else { rating = dist // closer spawns are better foreach( entity enemy in enemyPlayers ) // reduce rating if enemies are near by if ( Distance( enemy.GetOrigin(), spawn.GetOrigin() ) < 500.0 ) rating /= 2 } } spawn.CalculateRating( checkClass, team, rating, rating ) } } void function CTFInitPlayer( entity player ) { if ( !IsValid( file.imcFlagSpawn ) ) return vector imcSpawn = file.imcFlagSpawn.GetOrigin() Remote_CallFunction_NonReplay( player, "ServerCallback_SetFlagHomeOrigin", TEAM_IMC, imcSpawn.x, imcSpawn.y, imcSpawn.z ) vector militiaSpawn = file.militiaFlagSpawn.GetOrigin() Remote_CallFunction_NonReplay( player, "ServerCallback_SetFlagHomeOrigin", TEAM_MILITIA, militiaSpawn.x, militiaSpawn.y, militiaSpawn.z ) } void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) { if ( GetFlagForTeam( GetOtherTeam( victim.GetTeam() ) ).GetParent() == victim ) { if ( victim != attacker && attacker.IsPlayer() ) AddPlayerScore( attacker, "FlagCarrierKill", victim ) DropFlag( victim ) } } void function CreateFlags() { if ( IsValid( file.imcFlagSpawn ) ) { file.imcFlagSpawn.Destroy() file.imcFlag.Destroy() file.imcFlagReturnTrigger.Destroy() file.militiaFlagSpawn.Destroy() file.militiaFlag.Destroy() file.militiaFlagReturnTrigger.Destroy() } foreach ( entity spawn in GetEntArrayByClass_Expensive( "info_spawnpoint_flag" ) ) { // on some maps flags are on the opposite side from what they should be // likely this is because respawn uses distance checks from spawns to check this in official // but i don't like doing that so just using a list of maps to swap them on lol bool switchedSides = HasSwitchedSides() == 1 bool shouldSwap = SWAP_FLAG_MAPS.contains( GetMapName() ) ? !switchedSides : switchedSides int flagTeam = spawn.GetTeam() if ( shouldSwap ) { flagTeam = GetOtherTeam( flagTeam ) SetTeam( spawn, flagTeam ) } // create flag base entity base = CreatePropDynamic( CTF_FLAG_BASE_MODEL, spawn.GetOrigin(), spawn.GetAngles(), 0 ) SetTeam( base, spawn.GetTeam() ) svGlobal.flagSpawnPoints[ flagTeam ] = base // create flag entity flag = CreateEntity( "item_flag" ) flag.SetValueForModelKey( CTF_FLAG_MODEL ) SetTeam( flag, flagTeam ) flag.MarkAsNonMovingAttachment() DispatchSpawn( flag ) flag.SetModel( CTF_FLAG_MODEL ) flag.SetOrigin( spawn.GetOrigin() + < 0, 0, base.GetBoundingMaxs().z * 2 > ) // ensure flag doesn't spawn clipped into geometry flag.SetVelocity( < 0, 0, 1 > ) flag.s.canTake <- true flag.s.playersReturning <- [] level.teamFlags[ flag.GetTeam() ] <- flag entity returnTrigger = CreateEntity( "trigger_cylinder" ) SetTeam( returnTrigger, flagTeam ) returnTrigger.SetRadius( CTF_GetFlagReturnRadius() ) returnTrigger.SetAboveHeight( CTF_GetFlagReturnRadius() ) returnTrigger.SetBelowHeight( CTF_GetFlagReturnRadius() ) returnTrigger.SetEnterCallback( OnPlayerEntersFlagReturnTrigger ) returnTrigger.SetLeaveCallback( OnPlayerExitsFlagReturnTrigger ) DispatchSpawn( returnTrigger ) thread TrackFlagReturnTrigger( flag, returnTrigger ) if ( flagTeam == TEAM_IMC ) { file.imcFlagSpawn = base file.imcFlag = flag file.imcFlagReturnTrigger = returnTrigger SetGlobalNetEnt( "imcFlag", file.imcFlag ) SetGlobalNetEnt( "imcFlagHome", file.imcFlagSpawn ) } else { file.militiaFlagSpawn = base file.militiaFlag = flag file.militiaFlagReturnTrigger = returnTrigger SetGlobalNetEnt( "milFlag", file.militiaFlag ) SetGlobalNetEnt( "milFlagHome", file.militiaFlagSpawn ) } } foreach ( entity player in GetPlayerArray() ) CTFInitPlayer( player ) } void function TrackFlagReturnTrigger( entity flag, entity returnTrigger ) { // this is a bit of a hack, it seems parenting the return trigger to the flag actually sets the pickup radius of the flag to be the same as the trigger // this isn't wanted since only pickups should use that additional radius flag.EndSignal( "OnDestroy" ) while ( true ) { returnTrigger.SetOrigin( flag.GetOrigin() ) WaitFrame() } } void function SetFlagStateForTeam( int team, int state ) { if ( state == eFlagState.Away ) // we tell the client the flag is the player carrying it if they're carrying it SetGlobalNetEnt( team == TEAM_IMC ? "imcFlag" : "milFlag", ( team == TEAM_IMC ? file.imcFlag : file.militiaFlag ).GetParent() ) else SetGlobalNetEnt( team == TEAM_IMC ? "imcFlag" : "milFlag", team == TEAM_IMC ? file.imcFlag : file.militiaFlag ) SetGlobalNetInt( team == TEAM_IMC ? "imcFlagState" : "milFlagState", state ) } bool function OnFlagCollected( entity player, entity flag ) { if ( !IsAlive( player ) || flag.GetParent() != null || player.IsTitan() || player.IsPhaseShifted() ) return false if ( player.GetTeam() != flag.GetTeam() && flag.s.canTake ) GiveFlag( player, flag ) // pickup enemy flag else if ( player.GetTeam() == flag.GetTeam() && IsFlagHome( flag ) && PlayerHasEnemyFlag( player ) ) CaptureFlag( player, GetFlagForTeam( GetOtherTeam( flag.GetTeam() ) ) ) // cap the flag return false // don't wanna delete the flag entity } void function GiveFlag( entity player, entity flag ) { print( player + " picked up the flag!" ) flag.Signal( "ResetDropTimeout" ) flag.SetParent( player, "FLAG" ) thread DropFlagIfPhased( player, flag ) // do notifications MessageToPlayer( player, eEventNotifications.YouHaveTheEnemyFlag ) EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_GrabFlag" ) AddPlayerScore( player, "FlagTaken", player ) PlayFactionDialogueToPlayer( "ctf_flagPickupYou", player ) MessageToTeam( player.GetTeam(), eEventNotifications.PlayerHasEnemyFlag, player, player ) EmitSoundOnEntityToTeamExceptPlayer( flag, "UI_CTF_3P_TeamGrabFlag", player.GetTeam(), player ) PlayFactionDialogueToTeamExceptPlayer( "ctf_flagPickupFriendly", player.GetTeam(), player ) MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerHasFriendlyFlag, player, player ) EmitSoundOnEntityToTeam( flag, "UI_CTF_EnemyGrabFlag", flag.GetTeam() ) SetFlagStateForTeam( flag.GetTeam(), eFlagState.Away ) // used for held } void function DropFlagIfPhased( entity player, entity flag ) { player.EndSignal( "StartPhaseShift" ) OnThreadEnd( function() : ( player ) { DropFlag( player, true ) }) while( flag.GetParent() == player ) WaitFrame() } void function DropFlagForBecomingTitan( entity pilot, entity titan ) { DropFlag( pilot, true ) } void function DropFlag( entity player, bool realDrop = true ) { entity flag = GetFlagForTeam( GetOtherTeam( player.GetTeam() ) ) if ( flag.GetParent() != player ) return print( player + " dropped the flag!" ) flag.ClearParent() flag.SetAngles( < 0, 0, 0 > ) flag.SetVelocity( < 0, 0, 0 > ) if ( realDrop ) { // start drop timeout countdown thread TrackFlagDropTimeout( flag ) // add to capture assists if ( player.GetTeam() == TEAM_IMC ) file.imcCaptureAssistList.append( player ) else file.militiaCaptureAssistList.append( player ) // do notifications MessageToPlayer( player, eEventNotifications.YouDroppedTheEnemyFlag ) EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagDrop" ) MessageToTeam( player.GetTeam(), eEventNotifications.PlayerDroppedEnemyFlag, player, player ) // todo need a sound here maybe MessageToTeam( GetOtherTeam( player.GetTeam() ), eEventNotifications.PlayerDroppedFriendlyFlag, player, player ) // todo need a sound here maybe } SetFlagStateForTeam( flag.GetTeam(), eFlagState.Home ) // used for return prompt } void function TrackFlagDropTimeout( entity flag ) { flag.EndSignal( "ResetDropTimeout" ) wait CTF_GetDropTimeout() ResetFlag( flag ) } void function ResetFlag( entity flag ) { // ensure we can't pickup the flag after it's been dropped but before it's been reset flag.s.canTake = false if ( flag.GetParent() != null ) DropFlag( flag.GetParent(), false ) entity spawn if ( flag.GetTeam() == TEAM_IMC ) spawn = file.imcFlagSpawn else spawn = file.militiaFlagSpawn flag.SetOrigin( spawn.GetOrigin() + < 0, 0, spawn.GetBoundingMaxs().z + 1 > ) // we can take it again now flag.s.canTake = true SetFlagStateForTeam( flag.GetTeam(), eFlagState.None ) // used for home flag.Signal( "ResetDropTimeout" ) } void function CaptureFlag( entity player, entity flag ) { // reset flag ResetFlag( flag ) print( player + " captured the flag!" ) // score int team = player.GetTeam() AddTeamScore( team, 1 ) AddPlayerScore( player, "FlagCapture", player ) player.AddToPlayerGameStat( PGS_ASSAULT_SCORE, 1 ) // add 1 to captures on scoreboard SetRoundWinningKillReplayAttacker( player ) // set attacker for last cap replay array assistList if ( player.GetTeam() == TEAM_IMC ) assistList = file.imcCaptureAssistList else assistList = file.militiaCaptureAssistList foreach( entity assistPlayer in assistList ) if ( player != assistPlayer ) AddPlayerScore( assistPlayer, "FlagCaptureAssist", player ) assistList.clear() // notifs MessageToPlayer( player, eEventNotifications.YouCapturedTheEnemyFlag ) EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_PlayerScore" ) MessageToTeam( team, eEventNotifications.PlayerCapturedEnemyFlag, player, player ) EmitSoundOnEntityToTeamExceptPlayer( flag, "UI_CTF_3P_TeamScore", player.GetTeam(), player ) MessageToTeam( GetOtherTeam( team ), eEventNotifications.PlayerCapturedFriendlyFlag, player, player ) EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyScore", flag.GetTeam() ) if ( GameRules_GetTeamScore( team ) == GameMode_GetRoundScoreLimit( GAMETYPE ) - 1 ) { PlayFactionDialogueToTeam( "ctf_notifyWin1more", team ) PlayFactionDialogueToTeam( "ctf_notifyLose1more", GetOtherTeam( team ) ) } } void function OnPlayerEntersFlagReturnTrigger( entity trigger, entity player ) { entity flag if ( trigger.GetTeam() == TEAM_IMC ) flag = file.imcFlag else flag = file.militiaFlag if ( !player.IsPlayer() || !player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null ) return thread TryReturnFlag( player, flag ) } void function OnPlayerExitsFlagReturnTrigger( entity trigger, entity player ) { entity flag if ( trigger.GetTeam() == TEAM_IMC ) flag = file.imcFlag else flag = file.militiaFlag if ( !player.IsPlayer() || !player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null ) return player.Signal( "FlagReturnEnded" ) } void function TryReturnFlag( entity player, entity flag ) { // start return progress bar Remote_CallFunction_NonReplay( player, "ServerCallback_CTF_StartReturnFlagProgressBar", Time() + CTF_GetFlagReturnTime() ) EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagReturnMeter" ) OnThreadEnd( function() : ( player ) { // cleanup Remote_CallFunction_NonReplay( player, "ServerCallback_CTF_StopReturnFlagProgressBar" ) StopSoundOnEntity( player, "UI_CTF_1P_FlagReturnMeter" ) }) player.EndSignal( "FlagReturnEnded" ) player.EndSignal( "OnDeath" ) wait CTF_GetFlagReturnTime() // flag return succeeded // return flag ResetFlag( flag ) // do notifications for return MessageToPlayer( player, eEventNotifications.YouReturnedFriendlyFlag ) AddPlayerScore( player, "FlagReturn", player ) player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, 1 ) MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerReturnedFriendlyFlag, null, player ) EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_TeamReturnsFlag", flag.GetTeam() ) PlayFactionDialogueToTeam( "ctf_flagReturnedFriendly", flag.GetTeam() ) MessageToTeam( GetOtherTeam( flag.GetTeam() ), eEventNotifications.PlayerReturnedEnemyFlag, null, player ) EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyReturnsFlag", GetOtherTeam( flag.GetTeam() ) ) PlayFactionDialogueToTeam( "ctf_flagReturnedEnemy", GetOtherTeam( flag.GetTeam() ) ) }