From 17034fd4a4885e2ba01f7a5fa9c4948eabafed0f 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> --- .../northstar_client_localisation_english.txt | 4 +- .../mod/scripts/vscripts/ui/menu_mode_select.nut | 2 +- .../vscripts/sh_3psequence_to_1p_hacks.gnut | 22 +- .../scripts/vscripts/gamemodes/_ai_gamemodes.gnut | 164 +++++++ .../scripts/vscripts/gamemodes/_gamemode_aitdm.nut | 498 +++++++++++++++++++++ .../scripts/vscripts/gamemodes/_gamemode_at.nut | 366 ++++++++++++++- .../lobby/sh_private_lobby_modes_init.gnut | 3 +- .../mod/scripts/vscripts/mp/_gamestate_mp.nut | 2 +- 8 files changed, 1047 insertions(+), 14 deletions(-) diff --git a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt index fdda8bd8..50f2b51e 100644 --- a/Northstar.Client/mod/resource/northstar_client_localisation_english.txt +++ b/Northstar.Client/mod/resource/northstar_client_localisation_english.txt @@ -81,7 +81,9 @@ Press Yes if you agree to this. This choice can be changed in the mods menu at a "cp_amped_capture_points" "Amped Hardpoints" "coliseum_loadouts_enabled" "Coliseum Loadouts" - + + "aitdm_archer_grunts" "Archer Grunts" + // northstar.custom localisation is just deciding not to work, so putting it here for now "PL_sbox" "Sandbox" "PL_sbox_lobby" "Sandbox Lobby" diff --git a/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut b/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut index a017fb42..32a3c8f5 100644 --- a/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut +++ b/Northstar.Client/mod/scripts/vscripts/ui/menu_mode_select.nut @@ -52,7 +52,7 @@ void function UpdateVisibleModes() Hud_SetEnabled( buttons[ i ], true ) Hud_SetVisible( buttons[ i ], true ) - if ( !ModeSettings_RequiresAI( modesArray[ modeIndex ] ) ) + if ( !ModeSettings_RequiresAI( modesArray[ modeIndex ] ) || modesArray[ modeIndex ] == "aitdm" ) Hud_SetLocked( buttons[ i ], false ) else Hud_SetLocked( buttons[ i ], true ) diff --git a/Northstar.Custom/mod/scripts/vscripts/sh_3psequence_to_1p_hacks.gnut b/Northstar.Custom/mod/scripts/vscripts/sh_3psequence_to_1p_hacks.gnut index abfd269a..4d4d2744 100644 --- a/Northstar.Custom/mod/scripts/vscripts/sh_3psequence_to_1p_hacks.gnut +++ b/Northstar.Custom/mod/scripts/vscripts/sh_3psequence_to_1p_hacks.gnut @@ -123,19 +123,23 @@ Forced1PSequenceData function FirstPersonSequenceForce1P( FirstPersonSequenceStr thread FirstPersonSequence( sequence, thirdPersonProxy, other ) // create the viewpoint entity - entity camera = CreateEntity( "point_viewcontrol" ) - camera.SetParent( ownerProxy, attachment ) - camera.kv.spawnflags = 56 - DispatchSpawn( camera ) - player.SetViewEntity( camera, true ) - cleanupData.camera = camera - + if ( player.IsPlayer() ) // Check if the victim is an NPC + { + entity camera = CreateEntity( "point_viewcontrol" ) + camera.SetParent( ownerProxy, attachment ) + camera.kv.spawnflags = 56 + DispatchSpawn( camera ) + player.SetViewEntity( camera, true ) + cleanupData.camera = camera + } + // note for potential thing that could be done // entity e = CreatePropDynamic($"models/weapons/arms/pov_titan_light_cockpit.mdl"); e.SetParent(GetPlayerArray()[0].GetPetTitan(), "HATCH_HEAD"); e.SetOrigin(<0.75,0,-195>) // this is so we get a cockpit in these anims, issue with it is that the cockpit seems to break alot of rendering stuff // which really sucks since it'd be awesome to have a cockpit over these anims, really makes them better, even the client func to render through cockpits doesn't seem to work for it, just makes stuff rendering through the cockpit invisible rather than rendering in a broken way - Remote_CallFunction_NonReplay( player, "ServerCallback_HideHudForFPHackAnim" ) + if ( player.IsPlayer() ) // Check if the victim is an NPC + Remote_CallFunction_NonReplay( player, "ServerCallback_HideHudForFPHackAnim" ) // play this anim now, so we can cleanup after it's done thread CleanupForced1PSequenceAfterAnimDone( sequence, ownerProxy, other, cleanupData ) return cleanupData @@ -148,7 +152,7 @@ void function CleanupForced1PSequenceAfterAnimDone( FirstPersonSequenceStruct se OnThreadEnd( function() : ( cleanupData ) { - if ( IsValid( cleanupData.player ) ) + if ( IsValid( cleanupData.player ) && cleanupData.player.IsPlayer() ) CleanupForced1PSequence( cleanupData ) }) 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 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 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 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 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 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 ]-- +} 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 } ) -- cgit v1.2.3