diff options
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut')
-rw-r--r-- | Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut index a8089679..38c9cacd 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut @@ -1,6 +1,504 @@ +untyped global function GamemodeAITdm_Init +const SQUADS_PER_TEAM = 3 + +const REAPERS_PER_TEAM = 2 + +const LEVEL_SPECTRES = 125 +const LEVEL_STALKERS = 380 +const LEVEL_REAPERS = 500 + +struct +{ + // Due to team based escalation everything is an array + array< int > levels = [ LEVEL_SPECTRES, LEVEL_SPECTRES ] + array< array< string > > podEntities = [ [ "npc_soldier" ], [ "npc_soldier" ] ] + array< bool > reapers = [ false, false ] +} file + + void function GamemodeAITdm_Init() { + SetSpawnpointGamemodeOverride( ATTRITION ) // use bounty hunt spawns as vanilla game has no spawns explicitly defined for aitdm + + AddCallback_GameStateEnter( eGameState.Prematch, OnPrematchStart ) + AddCallback_GameStateEnter( eGameState.Playing, OnPlaying ) + + AddCallback_OnNPCKilled( HandleScoreEvent ) + AddCallback_OnPlayerKilled( HandleScoreEvent ) + + AddCallback_OnClientConnected( OnPlayerConnected ) + + AddCallback_NPCLeeched( OnSpectreLeeched ) + + if ( GetCurrentPlaylistVarInt( "aitdm_archer_grunts", 0 ) == 0 ) + { + AiGameModes_SetGruntWeapons( [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) + AiGameModes_SetSpectreWeapons( [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + } + else + { + AiGameModes_SetGruntWeapons( [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetSpectreWeapons( [ "mp_weapon_rocket_launcher" ] ) + } + + ScoreEvent_SetupEarnMeterValuesForMixedModes() +} + +// Starts skyshow, this also requiers AINs but doesn't crash if they're missing +void function OnPrematchStart() +{ + thread StratonHornetDogfightsIntense() +} + +void function OnPlaying() +{ + // don't run spawning code if ains and nms aren't up to date + if ( GetAINScriptVersion() == AIN_REV && GetNodeCount() != 0 ) + { + thread SpawnIntroBatch_Threaded( TEAM_MILITIA ) + thread SpawnIntroBatch_Threaded( TEAM_IMC ) + } +} + +// Sets up mode specific hud on client +void function OnPlayerConnected( entity player ) +{ + Remote_CallFunction_NonReplay( player, "ServerCallback_AITDM_OnPlayerConnected" ) +} + +// Used to handle both player and ai events +void function HandleScoreEvent( entity victim, entity attacker, var damageInfo ) +{ + // Basic checks + if ( victim == attacker || !( attacker.IsPlayer() || attacker.IsTitan() ) || GetGameState() != eGameState.Playing ) + return + + // Hacked spectre filter + if ( victim.GetOwner() == attacker ) + return + + // Split score so we can check if we are over the score max + // without showing the wrong value on client + int teamScore + int playerScore + string eventName + + // Handle AI, marvins aren't setup so we check for them to prevent crash + if ( victim.IsNPC() && victim.GetClassName() != "npc_marvin" ) + { + switch ( victim.GetClassName() ) + { + case "npc_soldier": + case "npc_spectre": + case "npc_stalker": + playerScore = 1 + break + case "npc_super_spectre": + playerScore = 3 + break + default: + playerScore = 0 + break + } + + // Titan kills get handled bellow this + if ( eventName != "KillNPCTitan" && eventName != "" ) + playerScore = ScoreEvent_GetPointValue( GetScoreEvent( eventName ) ) + } + + if ( victim.IsPlayer() ) + playerScore = 5 + + // Player ejecting triggers this without the extra check + if ( victim.IsTitan() && victim.GetBossPlayer() != attacker ) + playerScore += 10 + + + teamScore = playerScore + + // Check score so we dont go over max + if ( GameRules_GetTeamScore(attacker.GetTeam()) + teamScore > GetScoreLimit_FromPlaylist() ) + teamScore = GetScoreLimit_FromPlaylist() - GameRules_GetTeamScore(attacker.GetTeam()) + + // Add score + update network int to trigger the "Score +n" popup + AddTeamScore( attacker.GetTeam(), teamScore ) + attacker.AddToPlayerGameStat( PGS_ASSAULT_SCORE, playerScore ) + attacker.SetPlayerNetInt("AT_bonusPoints", attacker.GetPlayerGameStat( PGS_ASSAULT_SCORE ) ) +} + +// When attrition starts both teams spawn ai on preset nodes, after that +// Spawner_Threaded is used to keep the match populated +void function SpawnIntroBatch_Threaded( int team ) +{ + array<entity> dropPodNodes = GetEntArrayByClass_Expensive( "info_spawnpoint_droppod_start" ) + array<entity> dropShipNodes = GetValidIntroDropShipSpawn( dropPodNodes ) + + array<entity> podNodes + + array<entity> shipNodes + + + // mp_rise has weird droppod_start nodes, this gets around it + // To be more specific the teams aren't setup and some nodes are scattered in narnia + if( GetMapName() == "mp_rise" ) + { + entity spawnPoint + + // Get a spawnpoint for team + foreach ( point in GetEntArrayByClass_Expensive( "info_spawnpoint_dropship_start" ) ) + { + if ( point.HasKey( "gamemode_tdm" ) ) + if ( point.kv[ "gamemode_tdm" ] == "0" ) + continue + + if ( point.GetTeam() == team ) + { + spawnPoint = point + break + } + } + + // Get nodes close enough to team spawnpoint + foreach ( node in dropPodNodes ) + { + if ( node.HasKey("teamnum") && Distance2D( node.GetOrigin(), spawnPoint.GetOrigin()) < 2000 ) + podNodes.append( node ) + } + } + else + { + // Sort per team + foreach ( node in dropPodNodes ) + { + if ( node.GetTeam() == team ) + podNodes.append( node ) + } + } + + shipNodes = GetValidIntroDropShipSpawn( podNodes ) + + + // Spawn logic + int startIndex = 0 + bool first = true + entity node + + int pods = RandomInt( podNodes.len() + 1 ) + + int ships = shipNodes.len() + + for ( int i = 0; i < SQUADS_PER_TEAM; i++ ) + { + if ( pods != 0 || ships == 0 ) + { + int index = i + + if ( index > podNodes.len() - 1 ) + index = RandomInt( podNodes.len() ) + + node = podNodes[ index ] + thread AiGameModes_SpawnDropPod( node.GetOrigin(), node.GetAngles(), team, "npc_soldier", SquadHandler ) + + pods-- + } + else + { + if ( startIndex == 0 ) + startIndex = i // save where we started + + node = shipNodes[ i - startIndex ] + thread AiGameModes_SpawnDropShip( node.GetOrigin(), node.GetAngles(), team, 4, SquadHandler ) + + ships-- + } + + // Vanilla has a delay after first spawn + if ( first ) + wait 2 + + first = false + } + + wait 15 + + thread Spawner_Threaded( team ) +} + +// Populates the match +void function Spawner_Threaded( int team ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // used to index into escalation arrays + int index = team == TEAM_MILITIA ? 0 : 1 + + + while( true ) + { + Escalate( team ) + + // TODO: this should possibly not count scripted npc spawns, probably only the ones spawned by this script + array<entity> npcs = GetNPCArrayOfTeam( team ) + int count = npcs.len() + int reaperCount = GetNPCArrayEx( "npc_super_spectre", team, -1, <0,0,0>, -1 ).len() + + // REAPERS + if ( file.reapers[ index ] ) + { + array< entity > points = SpawnPoints_GetDropPod() + if ( reaperCount < REAPERS_PER_TEAM ) + { + entity node = points[ GetSpawnPointIndex( points, team ) ] + waitthread AiGameModes_SpawnReaper( node.GetOrigin(), node.GetAngles(), team, "npc_super_spectre_aitdm", ReaperHandler ) + } + } + + // NORMAL SPAWNS + if ( count < SQUADS_PER_TEAM * 4 - 2 ) + { + string ent = file.podEntities[ index ][ RandomInt( file.podEntities[ index ].len() ) ] + + array< entity > points = GetZiplineDropshipSpawns() + // Prefer dropship when spawning grunts + if ( ent == "npc_soldier" && points.len() != 0 ) + { + if ( RandomInt( points.len() ) ) + { + entity node = points[ GetSpawnPointIndex( points, team ) ] + waitthread AiGameModes_SpawnDropShip( node.GetOrigin(), node.GetAngles(), team, 4, SquadHandler ) + continue + } + } + + points = SpawnPoints_GetDropPod() + entity node = points[ GetSpawnPointIndex( points, team ) ] + waitthread AiGameModes_SpawnDropPod( node.GetOrigin(), node.GetAngles(), team, ent, SquadHandler ) + } + + WaitFrame() + } +} + +// Based on points tries to balance match +void function Escalate( int team ) +{ + int score = GameRules_GetTeamScore( team ) + int index = team == TEAM_MILITIA ? 1 : 0 + // This does the "Enemy x incoming" text + string defcon = team == TEAM_MILITIA ? "IMCdefcon" : "MILdefcon" + + // Return if the team is under score threshold to escalate + if ( score < file.levels[ index ] || file.reapers[ index ] ) + return + + // Based on score escalate a team + switch ( file.levels[ index ] ) + { + case LEVEL_SPECTRES: + file.levels[ index ] = LEVEL_STALKERS + file.podEntities[ index ].append( "npc_spectre" ) + SetGlobalNetInt( defcon, 2 ) + return + + case LEVEL_STALKERS: + file.levels[ index ] = LEVEL_REAPERS + file.podEntities[ index ].append( "npc_stalker" ) + SetGlobalNetInt( defcon, 3 ) + return + + case LEVEL_REAPERS: + file.reapers[ index ] = true + SetGlobalNetInt( defcon, 4 ) + return + } + + unreachable // hopefully +} + + +// Decides where to spawn ai +// Each team has their "zone" where they and their ai spawns +// These zones should swap based on which team is dominating where +int function GetSpawnPointIndex( array< entity > points, int team ) +{ + entity zone = DecideSpawnZone_Generic( points, team ) + + if ( IsValid( zone ) ) + { + // 20 Tries to get a random point close to the zone + for ( int i = 0; i < 20; i++ ) + { + int index = RandomInt( points.len() ) + + if ( Distance2D( points[ index ].GetOrigin(), zone.GetOrigin() ) < 6000 ) + return index + } + } + + return RandomInt( points.len() ) +} + +// tells infantry where to go +// In vanilla there seem to be preset paths ai follow to get to the other teams vone and capture it +// AI can also flee deeper into their zone suggesting someone spent way too much time on this +void function SquadHandler( array<entity> guys ) +{ + // Not all maps have assaultpoints / have weird assault points ( looking at you ac ) + // So we use enemies with a large radius + array< entity > points = GetNPCArrayOfEnemies( guys[0].GetTeam() ) + + if ( points.len() == 0 ) + return + + vector point + point = points[ RandomInt( points.len() ) ].GetOrigin() + + array<entity> players = GetPlayerArrayOfEnemies( guys[0].GetTeam() ) + + // Setup AI + foreach ( guy in guys ) + { + guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) + guy.AssaultPoint( point ) + guy.AssaultSetGoalRadius( 1600 ) // 1600 is minimum for npc_stalker, works fine for others + + // show on enemy radar + foreach ( player in players ) + guy.Minimap_AlwaysShow( 0, player ) + + + //thread AITdm_CleanupBoredNPCThread( guy ) + } + + // Every 5 - 15 secs change AssaultPoint + while ( true ) + { + foreach ( guy in guys ) + { + // Check if alive + if ( !IsAlive( guy ) ) + { + guys.removebyvalue( guy ) + continue + } + // Stop func if our squad has been killed off + if ( guys.len() == 0 ) + return + + // Get point and send guy to it + points = GetNPCArrayOfEnemies( guy.GetTeam() ) + if ( points.len() == 0 ) + continue + + point = points[ RandomInt( points.len() ) ].GetOrigin() + + guy.AssaultPoint( point ) + } + wait RandomFloatRange(5.0,15.0) + } +} + +// Award for hacking +void function OnSpectreLeeched( entity spectre, entity player ) +{ + // Set Owner so we can filter in HandleScore + spectre.SetOwner( player ) + // Add score + update network int to trigger the "Score +n" popup + AddTeamScore( player.GetTeam(), 1 ) + player.AddToPlayerGameStat( PGS_ASSAULT_SCORE, 1 ) + player.SetPlayerNetInt("AT_bonusPoints", player.GetPlayerGameStat( PGS_ASSAULT_SCORE ) ) +} + +// Same as SquadHandler, just for reapers +void function ReaperHandler( entity reaper ) +{ + array<entity> players = GetPlayerArrayOfEnemies( reaper.GetTeam() ) + foreach ( player in players ) + reaper.Minimap_AlwaysShow( 0, player ) + + reaper.AssaultSetGoalRadius( 500 ) + + // Every 10 - 20 secs get a player and go to him + // Definetly not annoying or anything :) + while( IsAlive( reaper ) ) + { + players = GetPlayerArrayOfEnemies( reaper.GetTeam() ) + if ( players.len() != 0 ) + { + entity player = GetClosest2D( players, reaper.GetOrigin() ) + reaper.AssaultPoint( player.GetOrigin() ) + } + wait RandomFloatRange(10.0,20.0) + } + // thread AITdm_CleanupBoredNPCThread( reaper ) +} + +// Currently unused as this is handled by SquadHandler +// May need to use this if my implementation falls apart +void function AITdm_CleanupBoredNPCThread( entity guy ) +{ + // track all ai that we spawn, ensure that they're never "bored" (i.e. stuck by themselves doing fuckall with nobody to see them) for too long + // if they are, kill them so we can free up slots for more ai to spawn + // we shouldn't ever kill ai if players would notice them die + + // NOTE: this partially covers up for the fact that we script ai alot less than vanilla probably does + // vanilla probably messes more with making ai assaultpoint to fights when inactive and stuff like that, we don't do this so much + guy.EndSignal( "OnDestroy" ) + wait 15.0 // cover spawning time from dropship/pod + before we start cleaning up + + int cleanupFailures = 0 // when this hits 2, cleanup the npc + while ( cleanupFailures < 2 ) + { + wait 10.0 + + if ( guy.GetParent() != null ) + continue // never cleanup while spawning + + array<entity> otherGuys = GetPlayerArray() + otherGuys.extend( GetNPCArrayOfTeam( GetOtherTeam( guy.GetTeam() ) ) ) + + bool failedChecks = false + + foreach ( entity otherGuy in otherGuys ) + { + // skip dead people + if ( !IsAlive( otherGuy ) ) + continue + + failedChecks = false + + // don't kill if too close to anything + if ( Distance( otherGuy.GetOrigin(), guy.GetOrigin() ) < 2000.0 ) + break + + // don't kill if ai or players can see them + if ( otherGuy.IsPlayer() ) + { + if ( PlayerCanSee( otherGuy, guy, true, 135 ) ) + break + } + else + { + if ( otherGuy.CanSee( guy ) ) + break + } + + // don't kill if they can see any ai + if ( guy.CanSee( otherGuy ) ) + break + + failedChecks = true + } + + if ( failedChecks ) + cleanupFailures++ + else + cleanupFailures-- + } + + print( "cleaning up bored npc: " + guy + " from team " + guy.GetTeam() ) + guy.Destroy() }
\ No newline at end of file |