aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.CustomServers/mod/scripts')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut164
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut498
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut366
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut3
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut2
5 files changed, 1030 insertions, 3 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut
index cf7f7e15..d6d578bb 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut
@@ -1,6 +1,170 @@
global function AiGameModes_Init
+global function AiGameModes_SetGruntWeapons
+global function AiGameModes_SetSpectreWeapons
+
+global function AiGameModes_SpawnDropShip
+global function AiGameModes_SpawnDropPod
+global function AiGameModes_SpawnReaper
+global function AiGameModes_SpawnTitan
+
+global function GetValidIntroDropShipSpawn
+
+
+const INTRO_DROPSHIP_CUTOFF = 2000
+
+struct
+{
+ array< string > gruntWeapons = [ "mp_weapon_rspn101" ]
+ array< string > spectreWeapons = [ "mp_weapon_hemlok_smg" ]
+} file
+
void function AiGameModes_Init()
{
+}
+
+//------------------------------------------------------
+
+void function AiGameModes_SetGruntWeapons( array< string > weapons )
+{
+ file.gruntWeapons = weapons
+}
+
+void function AiGameModes_SetSpectreWeapons( array< string > weapons )
+{
+ file.spectreWeapons = weapons
+}
+
+//------------------------------------------------------
+
+void function AiGameModes_SpawnDropShip( vector pos, vector rot, int team, int count, void functionref( array<entity> guys ) squadHandler = null )
+{
+ string squadName = MakeSquadName( team, UniqueString( "" ) )
+
+ CallinData drop
+ drop.origin = pos
+ drop.yaw = rot.y
+ drop.dist = 768
+ drop.team = team
+ drop.squadname = squadName
+ SetDropTableSpawnFuncs( drop, CreateSoldier, count )
+ SetCallinStyle( drop, eDropStyle.ZIPLINE_NPC )
+
+ thread RunDropshipDropoff( drop )
+
+ WaitSignal( drop, "OnDropoff" )
+
+ array< entity > guys = GetNPCArrayBySquad( squadName )
+
+ foreach ( guy in guys )
+ {
+ ReplaceWeapon( guy, file.gruntWeapons[ RandomInt( file.gruntWeapons.len() ) ], [] )
+ guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE )
+ }
+
+ if ( squadHandler != null )
+ thread squadHandler( guys )
+}
+
+
+void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string content /*( ͡° ͜ʖ ͡°)*/, void functionref( array<entity> guys ) squadHandler = null )
+{
+ string squadName = MakeSquadName( team, UniqueString( "" ) )
+ array<entity> guys
+
+ entity pod = CreateDropPod( pos, <0,0,0> )
+
+ InitFireteamDropPod( pod )
+
+ for ( int i = 0; i < 4 ;i++ )
+ {
+ entity npc = CreateNPC( content, team, pos,<0,0,0> )
+ DispatchSpawn( npc )
+ SetSquad( npc, squadName )
+
+ switch ( content )
+ {
+ case "npc_soldier":
+ ReplaceWeapon( npc, file.gruntWeapons[ RandomInt( file.gruntWeapons.len() ) ], [] )
+ break
+
+ case "npc_spectre":
+ ReplaceWeapon( npc, file.spectreWeapons[ RandomInt( file.spectreWeapons.len() ) ], [] )
+ break
+ }
+
+ npc.SetParent( pod, "ATTACH", true )
+
+ npc.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE )
+ guys.append( npc )
+ }
+
+ // The order here is different so we can show on minimap while were still falling
+ if ( squadHandler != null )
+ thread squadHandler( guys )
+
+ waitthread LaunchAnimDropPod( pod, "pod_testpath", pos, rot )
+
+ ActivateFireteamDropPod( pod, guys )
+}
+
+void function AiGameModes_SpawnReaper( vector pos, vector rot, int team, string aiSettings = "", void functionref( entity reaper ) reaperHandler = null )
+{
+ entity reaper = CreateSuperSpectre( team, pos, rot )
+ SetSpawnOption_Titanfall( reaper )
+ SetSpawnOption_Warpfall( reaper )
+
+ if ( aiSettings != "" )
+ SetSpawnOption_AISettings( reaper, aiSettings )
+
+ DispatchSpawn( reaper )
+
+
+ if ( reaperHandler != null )
+ thread reaperHandler( reaper )
+}
+
+// including aisettings stuff specifically for at bounty titans
+void function AiGameModes_SpawnTitan( vector pos, vector rot, int team, string setFile, string aiSettings = "", void functionref( entity titan ) titanHandler = null )
+{
+ entity titan = CreateNPCTitan( setFile, TEAM_BOTH, pos, rot )
+ SetSpawnOption_Titanfall( titan )
+ SetSpawnOption_Warpfall( titan )
+
+ if ( aiSettings != "" )
+ SetSpawnOption_AISettings( titan, aiSettings )
+
+ DispatchSpawn( titan )
+
+ if ( titanHandler != null )
+ thread titanHandler( titan )
+}
+
+// entity.ReplaceActiveWeapon gave grunts archers sometimes, this is my replacement for it
+void function ReplaceWeapon( entity guy, string weapon, array<string> mods )
+{
+ guy.TakeActiveWeapon()
+ guy.GiveWeapon( weapon, mods )
+ guy.SetActiveWeaponByName( weapon )
+}
+
+// Checks if we can spawn a dropship at a node, this should guarantee dropship ziplines
+array<entity> function GetValidIntroDropShipSpawn( array<entity> introNodes )
+{
+ array<entity> introShipSpawns
+
+ if ( GetZiplineDropshipSpawns().len() == 0 )
+ return []
+
+ foreach ( node in introNodes )
+ {
+ entity closestNode = GetClosest( GetZiplineDropshipSpawns(), node.GetOrigin() )
+ SetTeam( closestNode, node.GetTeam() )
+
+ if ( Distance( closestNode.GetOrigin(), node.GetOrigin() ) < INTRO_DROPSHIP_CUTOFF )
+ introShipSpawns.append( closestNode )
+ }
+
+ return introShipSpawns
} \ No newline at end of file
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
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut
index 573ea72f..915e03e0 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut
@@ -1,12 +1,376 @@
global function GamemodeAt_Init
global function RateSpawnpoints_AT
+const int BH_AI_TEAM = TEAM_BOTH
+const int BOUNTY_TITAN_DAMAGE_POOL = 400 // Rewarded for damage
+const int BOUNTY_TITAN_KILL_REWARD = 100 // Rewarded for kill
+const float WAVE_STATE_TRANSITION_TIME = 5.0
+
+const array<string> VALID_BOUNTY_TITAN_SETTINGS = [
+ "npc_titan_atlas_stickybomb_bounty",
+ "npc_titan_atlas_tracker_bounty",
+ "npc_titan_ogre_minigun_bounty",
+ "npc_titan_ogre_meteor_bounty",
+ "npc_titan_stryder_leadwall_bounty",
+ "npc_titan_stryder_sniper_bounty",
+ "npc_titan_atlas_vanguard_bounty"
+]
+
+
+// IMPLEMENTATION NOTES:
+// bounty hunt is a mode that was clearly pretty heavily developed, and had alot of scrapped concepts (i.e. most wanted player bounties, turret bounties, collectable blackbox objectives)
+// in the interest of time, this script isn't gonna support any of that atm
+// alot of the remote functions also take parameters that aren't used, i'm not gonna populate these and just use default values for now instead
+// however, if you do want to mess with this stuff, almost all the remote functions for this stuff are still present in cl_gamemode_at, and should work fine with minimal fuckery in my experience
+
+struct {
+ array<entity> campsToRegisterOnEntitiesDidLoad
+
+ array<entity> banks
+ array<AT_WaveOrigin> camps
+
+ table< int, table< string, int > > trackedCampNPCSpawns
+} file
+
void function GamemodeAt_Init()
{
+ AddCallback_GameStateEnter( eGameState.Playing, RunATGame )
+
+ AddCallback_OnClientConnected( InitialiseATPlayer )
+ AddSpawnCallbackEditorClass( "info_target", "info_attrition_bank", CreateATBank )
+ AddSpawnCallbackEditorClass( "info_target", "info_attrition_camp", CreateATCamp )
+ AddCallback_EntitiesDidLoad( CreateATCamps_Delayed )
}
void function RateSpawnpoints_AT( int checkclass, array<entity> spawnpoints, int team, entity player )
{
RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) // temp
-} \ No newline at end of file
+}
+
+// world and player inits
+
+void function InitialiseATPlayer( entity player )
+{
+ Remote_CallFunction_NonReplay( player, "ServerCallback_AT_OnPlayerConnected" )
+}
+
+void function CreateATBank( entity spawnpoint )
+{
+ entity bank = CreatePropDynamic( spawnpoint.GetModelName(), spawnpoint.GetOrigin(), spawnpoint.GetAngles(), SOLID_VPHYSICS )
+ bank.SetScriptName( "AT_Bank" )
+
+ // create tracker ent
+ // we don't need to store these at all, client just needs to get them
+ DispatchSpawn( GetAvailableBankTracker( bank ) )
+
+ thread PlayAnim( bank, "mh_inactive_idle" )
+
+ file.banks.append( bank )
+}
+
+void function CreateATCamp( entity spawnpoint )
+{
+ // delay this so we don't do stuff before all spawns are initialised and that
+ file.campsToRegisterOnEntitiesDidLoad.append( spawnpoint )
+}
+
+void function CreateATCamps_Delayed()
+{
+ // we delay registering camps until EntitiesDidLoad since they rely on spawnpoints and stuff, which might not all be ready in the creation callback
+ // unsure if this would be an issue in practice, but protecting against it in case it would be
+ foreach ( entity camp in file.campsToRegisterOnEntitiesDidLoad )
+ {
+ AT_WaveOrigin campStruct
+ campStruct.ent = camp
+ campStruct.origin = camp.GetOrigin()
+ campStruct.radius = expect string( camp.kv.radius ).tofloat()
+ campStruct.height = expect string( camp.kv.height ).tofloat()
+
+ // assumes every info_attrition_camp will have all 9 phases, possibly not a good idea?
+ for ( int i = 0; i < 9; i++ )
+ campStruct.phaseAllowed.append( expect string( camp.kv[ "phase_" + ( i + 1 ) ] ) == "1" )
+
+ // get droppod spawns
+ foreach ( entity spawnpoint in SpawnPoints_GetDropPod() )
+ if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 )
+ campStruct.dropPodSpawnPoints.append( spawnpoint )
+
+ foreach ( entity spawnpoint in SpawnPoints_GetTitan() )
+ if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 )
+ campStruct.titanSpawnPoints.append( spawnpoint )
+
+ // todo: turret spawns someday maybe
+
+ file.camps.append( campStruct )
+ }
+
+ file.campsToRegisterOnEntitiesDidLoad.clear()
+}
+
+// scoring funcs
+
+// don't use this where possible as it doesn't set score and stuff
+void function AT_SetPlayerCash( entity player, int amount )
+{
+ // split into stacks of 256 where necessary
+ int stacks = amount / 256 // automatically rounds down because int division
+
+ player.SetPlayerNetInt( "AT_bonusPoints256", stacks )
+ player.SetPlayerNetInt( "AT_bonusPoints", amount - stacks * 256 )
+}
+
+void function AT_AddPlayerCash( entity player, int amount )
+{
+ // update score difference
+ AddTeamScore( player.GetTeam(), amount / 2 )
+ AT_SetPlayerCash( player, player.GetPlayerNetInt( "AT_bonusPoints" ) + ( player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 ) + amount )
+}
+
+// run gamestate
+
+void function RunATGame()
+{
+ thread RunATGame_Threaded()
+}
+
+void function RunATGame_Threaded()
+{
+ svGlobal.levelEnt.EndSignal( "GameStateChanged" )
+
+ OnThreadEnd( function()
+ {
+ SetGlobalNetBool( "banksOpen", false )
+ })
+
+ wait WAVE_STATE_TRANSITION_TIME // initial wait before first wave
+
+ for ( int waveCount = 1; ; waveCount++ )
+ {
+ wait WAVE_STATE_TRANSITION_TIME
+
+ // cap to number of real waves
+ int waveId = ( waveCount / 2 )
+ // last wave is clearly unfinished so don't use, just cap to last actually used one
+ if ( waveId >= GetWaveDataSize() - 1 )
+ {
+ waveId = GetWaveDataSize() - 2
+ waveCount = waveId * 2
+ }
+
+ SetGlobalNetInt( "AT_currentWave", waveId )
+ bool isBossWave = waveCount / float( 2 ) > waveId // odd number waveCount means boss wave
+
+ // announce the wave
+ foreach ( entity player in GetPlayerArray() )
+ {
+ if ( isBossWave )
+ Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnounceBoss" )
+ else
+ Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnouncePreParty", 0.0, waveId )
+ }
+
+ wait WAVE_STATE_TRANSITION_TIME
+
+ // run the wave
+
+ AT_WaveData wave = GetWaveData( waveId )
+ array< array<AT_SpawnData> > campSpawnData
+
+ if ( isBossWave )
+ campSpawnData = wave.bossSpawnData
+ else
+ campSpawnData = wave.spawnDataArrays
+
+ // initialise pending spawns
+ foreach ( array< AT_SpawnData > campData in campSpawnData )
+ {
+ foreach ( AT_SpawnData spawnData in campData )
+ spawnData.pendingSpawns = spawnData.totalToSpawn
+ }
+
+ // clear tracked spawns
+ file.trackedCampNPCSpawns = {}
+ while ( true )
+ {
+ // if this is ever 0 by the end of this loop, wave is complete
+ int numActiveCampSpawners = 0
+
+ // iterate over camp data for wave
+ for ( int campIdx = 0; campIdx < campSpawnData.len() && campIdx < file.camps.len(); campIdx++ )
+ {
+ if ( !( campIdx in file.trackedCampNPCSpawns ) )
+ file.trackedCampNPCSpawns[ campIdx ] <- {}
+
+ // iterate over ai spawn data for camp
+ foreach ( AT_SpawnData spawnData in campSpawnData[ campIdx ] )
+ {
+ if ( !( spawnData.aitype in file.trackedCampNPCSpawns[ campIdx ] ) )
+ file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] <- 0
+
+ if ( spawnData.pendingSpawns > 0 || file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] > 0 )
+ numActiveCampSpawners++
+
+ // try to spawn as many ai as we can, as long as the camp doesn't already have too many spawned
+ int spawnCount
+ for ( spawnCount = 0; spawnCount < spawnData.pendingSpawns && spawnCount < spawnData.totalAllowedOnField - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ]; )
+ {
+ // not doing this in a generic way atm, but could be good for the future if we want to support more ai
+ switch ( spawnData.aitype )
+ {
+ case "npc_soldier":
+ case "npc_spectre":
+ case "npc_stalker":
+ thread AT_SpawnDroppodSquad( campIdx, spawnData.aitype )
+ spawnCount += 4
+ break
+
+ case "npc_super_spectre":
+ thread AT_SpawnReaper( campIdx )
+ spawnCount += 1
+ break
+
+ case "npc_titan":
+ thread AT_SpawnBountyTitan( campIdx )
+ spawnCount += 1
+ break
+
+ default:
+ print( "BOUNTY HUNT: Tried to spawn unsupported ai of type \"" + "\" at camp " + campIdx )
+ }
+ }
+
+ // track spawns
+ file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] += spawnCount
+ spawnData.pendingSpawns -= spawnCount
+ }
+ }
+
+ if ( numActiveCampSpawners == 0 )
+ break
+
+ wait 0.5
+ }
+
+ wait WAVE_STATE_TRANSITION_TIME
+
+ // banking phase
+ }
+}
+
+// entity funcs
+
+void function AT_SpawnDroppodSquad( int camp, string aiType )
+{
+ entity spawnpoint
+ if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 )
+ spawnpoint = file.camps[ camp ].ent
+ else
+ spawnpoint = file.camps[ camp ].dropPodSpawnPoints.getrandom()
+
+ // add variation to spawns
+ wait RandomFloat( 1.0 )
+
+ AiGameModes_SpawnDropPod( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, aiType, void function( array<entity> guys ) : ( camp, aiType )
+ {
+ AT_HandleSquadSpawn( guys, camp, aiType )
+ })
+}
+
+void function AT_HandleSquadSpawn( array<entity> guys, int camp, string aiType )
+{
+ foreach ( entity guy in guys )
+ {
+ guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE )
+
+ // untrack them on death
+ thread AT_WaitToUntrackNPC( guy, camp, aiType )
+ }
+}
+
+void function AT_SpawnReaper( int camp )
+{
+ entity spawnpoint
+ if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 )
+ spawnpoint = file.camps[ camp ].ent
+ else
+ spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom()
+
+ // add variation to spawns
+ wait RandomFloat( 1.0 )
+
+ AiGameModes_SpawnReaper( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, "npc_super_spectre",void function( entity reaper ) : ( camp )
+ {
+ thread AT_WaitToUntrackNPC( reaper, camp, "npc_super_spectre" )
+ })
+}
+
+void function AT_SpawnBountyTitan( int camp )
+{
+ entity spawnpoint
+ if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 )
+ spawnpoint = file.camps[ camp ].ent
+ else
+ spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom()
+
+ // add variation to spawns
+ wait RandomFloat( 1.0 )
+
+ // look up titan to use
+ int bountyID = 0
+ try
+ {
+ bountyID = ReserveBossID( VALID_BOUNTY_TITAN_SETTINGS.getrandom() )
+ }
+ catch ( ex ) {} // if we go above the expected wave count that vanilla supports, there's basically no way to ensure that this func won't error, so default 0 after that point
+
+ string aisettings = GetTypeFromBossID( bountyID )
+ string titanClass = expect string( Dev_GetAISettingByKeyField_Global( aisettings, "npc_titan_player_settings" ) )
+
+
+ AiGameModes_SpawnTitan( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, titanClass, aisettings, void function( entity titan ) : ( camp, bountyID )
+ {
+ // set up titan-specific death/damage callbacks
+ AddEntityCallback_OnDamaged( titan, OnBountyDamaged)
+ AddEntityCallback_OnKilled( titan, OnBountyKilled )
+
+ titan.GetTitanSoul().soul.skipDoomState = true
+ // i feel like this should be localised, but there's nothing for it in r1_english?
+ titan.SetTitle( GetNameFromBossID( bountyID ) )
+ thread AT_WaitToUntrackNPC( titan, camp, "npc_titan" )
+ } )
+}
+
+// Tracked entities will require their own "wallet"
+// for titans it should be used for rounding error compenstation
+// for infantry it sould be used to store money if the npc kills a player
+void function OnBountyDamaged( entity titan, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !attacker.IsPlayer() )
+ attacker = GetLatestAssistingPlayerInfo( titan ).player
+
+ if ( IsValid( attacker ) && attacker.IsPlayer() )
+ {
+ int reward = int ( BOUNTY_TITAN_DAMAGE_POOL * DamageInfo_GetDamage( damageInfo ) / titan.GetMaxHealth() )
+ printt ( titan.GetMaxHealth(), DamageInfo_GetDamage( damageInfo ) )
+
+ AT_AddPlayerCash( attacker, reward )
+ }
+}
+
+void function OnBountyKilled( entity titan, var damageInfo )
+{
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !attacker.IsPlayer() )
+ attacker = GetLatestAssistingPlayerInfo( titan ).player
+
+ if ( IsValid( attacker ) && attacker.IsPlayer() )
+ AT_AddPlayerCash( attacker, BOUNTY_TITAN_KILL_REWARD )
+}
+
+void function AT_WaitToUntrackNPC( entity guy, int camp, string aiType )
+{
+ guy.WaitSignal( "OnDeath", "OnDestroy" )
+ file.trackedCampNPCSpawns[ camp ][ aiType ]--
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut b/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut
index 719ea336..ccccefaf 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/lobby/sh_private_lobby_modes_init.gnut
@@ -38,7 +38,8 @@ void function PrivateMatchModesInit()
AddPrivateMatchModeSettingEnum( "#GAMEMODE_cp", "cp_amped_capture_points", [ "#SETTING_DISABLED", "#SETTING_ENABLED" ], "1" ) // would've been nice to use amped_capture_points, but this var is already used ingame and its value is default 0
AddPrivateMatchModeSettingEnum( "#GAMEMODE_coliseum", "coliseum_loadouts_#SETTING_ENABLED", [ "#SETTING_DISABLED", "#SETTING_ENABLED" ], "1" )
-
+
+ AddPrivateMatchModeSettingEnum( "#PL_aitdm", "aitdm_archer_grunts", [ "Disabled", "Enabled" ], "0" )
// modes
AddPrivateMatchMode( "ffa" )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut
index 42aa4a62..bfcd23e0 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut
@@ -682,7 +682,7 @@ void function CleanUpEntitiesForRoundEnd()
foreach ( entity npc in GetNPCArray() )
{
- if ( !IsValid( npc ) )
+ if ( !IsValid( npc ) || !IsAlive( npc ) )
continue
// kill rather than destroy, as destroying will cause issues with children which is an issue especially for dropships and titans
npc.Die( svGlobal.worldspawn, svGlobal.worldspawn, { damageSourceId = eDamageSourceId.round_end } )