From d3945f4b65417d154492e28831b5fbd230416118 Mon Sep 17 00:00:00 2001 From: F1F7Y <64418963+F1F7Y@users.noreply.github.com> Date: Fri, 29 Apr 2022 23:05:09 +0200 Subject: Add Attrition (#321) * Skyshow * Attrition score stuff * DropShip and DropPod spawn funcs * Attrition intro * Reaper spawn func * Attrition assault point logic * Cleanup * Match logic + basic spawn algo * Fix crash * Improve ai ? * Add AI weapon setter funcs * Bounty hunt score basics * code cleanup * add cleanup for bored ai * fix issue with failedChecks not being reset properly in cleanup * more cleanup * stop ai spawns on epilogue * don't run aitdm spawning code if we don't have ains/nms * Fix missing nodes crash * initial bounty hunt stuff * oops forgot to push this * me when i'm a competent developer * formatting and such * cap score + proper mp_rise spawns fix * Better squad handler * comment reaper out; needs to be checked out * Fix crash site crash * reapers * minor visual change is what Id call this * bh basic damage scoring * going to push this so there's a base to work with, still doesn't work * fix funny suicide bug :) * I hate crash site + score funnies * comlpex skill issue * final score fix, lets hope so * Hacked spectre fix * 1p npc executions fix * stalkers * Archer Grunts :) * minor fixing * Skyshow * Attrition score stuff * DropShip and DropPod spawn funcs * Attrition intro * Reaper spawn func * Attrition assault point logic * Cleanup * Match logic + basic spawn algo * Fix crash * Improve ai ? * Add AI weapon setter funcs * Bounty hunt score basics * code cleanup * add cleanup for bored ai * fix issue with failedChecks not being reset properly in cleanup * more cleanup * stop ai spawns on epilogue * don't run aitdm spawning code if we don't have ains/nms * Fix missing nodes crash * initial bounty hunt stuff * oops forgot to push this * me when i'm a competent developer * formatting and such * cap score + proper mp_rise spawns fix * Better squad handler * comment reaper out; needs to be checked out * Fix crash site crash * reapers * minor visual change is what Id call this * bh basic damage scoring * going to push this so there's a base to work with, still doesn't work * fix funny suicide bug :) * I hate crash site + score funnies * comlpex skill issue * final score fix, lets hope so * Hacked spectre fix * 1p npc executions fix * stalkers * Archer Grunts :) * minor fixing * Unlock attrition in mode select menu * move archer grunts settings Co-authored-by: BobTheBob <32057864+BobTheBob9@users.noreply.github.com> --- .../scripts/vscripts/gamemodes/_ai_gamemodes.gnut | 164 +++++++ .../scripts/vscripts/gamemodes/_gamemode_aitdm.nut | 498 +++++++++++++++++++++ .../scripts/vscripts/gamemodes/_gamemode_at.nut | 366 ++++++++++++++- 3 files changed, 1027 insertions(+), 1 deletion(-) (limited to 'Northstar.CustomServers/mod/scripts/vscripts/gamemodes') diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut index cf7f7e150..d6d578bb7 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 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 guys ) squadHandler = null ) +{ + string squadName = MakeSquadName( team, UniqueString( "" ) ) + array 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 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 function GetValidIntroDropShipSpawn( array introNodes ) +{ + array 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 a80896793..38c9cacda 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 dropPodNodes = GetEntArrayByClass_Expensive( "info_spawnpoint_droppod_start" ) + array dropShipNodes = GetValidIntroDropShipSpawn( dropPodNodes ) + + array podNodes + + array 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 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 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 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 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 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 573ea72fc..915e03e08 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 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 campsToRegisterOnEntitiesDidLoad + + array banks + array 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 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 > 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 guys ) : ( camp, aiType ) + { + AT_HandleSquadSpawn( guys, camp, aiType ) + }) +} + +void function AT_HandleSquadSpawn( array 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 ]-- +} -- cgit v1.2.3