diff options
author | RoyalBlue1 <malte.hoermeyer@web.de> | 2022-07-15 00:41:36 +0200 |
---|---|---|
committer | RoyalBlue1 <malte.hoermeyer@web.de> | 2022-07-15 00:41:36 +0200 |
commit | 1d9b24faf5280db4b57eea42904f24ff4fdd16ba (patch) | |
tree | 558c8c8aa6b3d044f5fd9e17284177f96ba354f3 /Northstar.CustomServers | |
parent | 88d765b41f4ff94c59fa535d53e708274fa22d26 (diff) | |
parent | 85eb27362bd295a9e1560982a3369a688e13ded0 (diff) | |
download | NorthstarMods-1d9b24faf5280db4b57eea42904f24ff4fdd16ba.tar.gz NorthstarMods-1d9b24faf5280db4b57eea42904f24ff4fdd16ba.zip |
Merge remote-tracking branch 'upsteam/main' into gamemode_fd
Diffstat (limited to 'Northstar.CustomServers')
21 files changed, 1320 insertions, 96 deletions
diff --git a/Northstar.CustomServers/mod.json b/Northstar.CustomServers/mod.json index 4fbb248d..dba29bc5 100644 --- a/Northstar.CustomServers/mod.json +++ b/Northstar.CustomServers/mod.json @@ -1,7 +1,7 @@ { "Name": "Northstar.CustomServers", "Description": "Attempts to recreate the behaviour of vanilla Titanfall 2 servers, as well as changing some scripts to allow better support for mods", - "Version": "1.5.0", + "Version": "1.9.0", "LoadPriority": 0, "ConVars": [ { @@ -49,6 +49,10 @@ { "Name": "ns_should_log_unknown_clientcommands", "DefaultValue": "1" + }, + { + "Name": "ns_allow_kill_commands", + "DefaultValue": "0" } ], "Scripts": [ @@ -60,6 +64,13 @@ } }, { + "Path": "_northstar_cheatcommands.nut", + "RunOn": "SERVER", + "ServerCallback": { + "After": "NorthstarCheatCommands_Init" + } + }, + { "Path": "_chat.gnut", "RunOn": "SERVER" }, @@ -149,4 +160,4 @@ "RunOn": "( SERVER ) && MP" } ] -}
\ No newline at end of file +} diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_custom_codecallbacks.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_custom_codecallbacks.gnut index 9e46f99a..4a7f8189 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/_custom_codecallbacks.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/_custom_codecallbacks.gnut @@ -22,10 +22,11 @@ void function CServerGameDLL_ProcessMessageStartThread(int playerIndex, string m void function CServerGameDLL_OnReceivedSayTextMessageCallback(int playerIndex, string message, bool isTeam) { entity player = GetPlayerByIndex(playerIndex) - if (player == null) { + if (player == null || !player.hasConnected) { print("Ignored chat message from invalid player index " + playerIndex + ": " + message) return } + print("Received message from " + player + "(" + player.GetUID() + "): " + message) ClServer_MessageStruct localMessage localMessage.message = message @@ -55,4 +56,4 @@ void function AddCallback_OnReceivedSayTextMessage( ClServer_MessageStruct funct void function NSSetupChathooksServer() { getroottable().rawset("CServerGameDLL_ProcessMessageStartThread", CServerGameDLL_ProcessMessageStartThread) -}
\ No newline at end of file +} diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut index a31963cf..3e8ac9ea 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/_loadouts_mp.gnut @@ -3,11 +3,24 @@ global function SvLoadoutsMP_Init global function SetLoadoutGracePeriodEnabled global function SetWeaponDropsEnabled + +global function AddCallback_OnTryGetTitanLoadout global function GetTitanLoadoutForPlayer +global struct sTryGetTitanLoadoutCallbackReturn +{ + bool wasChanged = false + bool runMoreCallbacks = true + TitanLoadoutDef& loadout +} + +typedef TryGetTitanLoadoutCallbackType sTryGetTitanLoadoutCallbackReturn functionref( entity player, TitanLoadoutDef loadout, bool wasChanged ) + struct { bool loadoutGracePeriodEnabled = true bool weaponDropsEnabled = true + array< TryGetTitanLoadoutCallbackType > onTryGetTitanLoadoutCallbacks + array<entity> dirtyLoadouts } file @@ -65,16 +78,40 @@ void function DelayDestroyDroppedWeapon( entity weapon ) weapon.Destroy() } +void function AddCallback_OnTryGetTitanLoadout( TryGetTitanLoadoutCallbackType callback ) +{ + file.onTryGetTitanLoadoutCallbacks.append( callback ) +} + TitanLoadoutDef function GetTitanLoadoutForPlayer( entity player ) { SetActiveTitanLoadout( player ) // set right loadout - + TitanLoadoutDef loadout = GetActiveTitanLoadout( player ) + // fix bug with titan weapons having null mods // null mods aren't valid and crash if we try to give them to npc - TitanLoadoutDef def = GetActiveTitanLoadout( player ) - def.primaryMods.removebyvalue( "null" ) + loadout.primaryMods.removebyvalue( "null" ) + + // allow scripts to modify loadouts + bool wasChanged = false + foreach ( TryGetTitanLoadoutCallbackType callback in file.onTryGetTitanLoadoutCallbacks ) + { + sTryGetTitanLoadoutCallbackReturn callbackRet = callback( player, loadout, wasChanged ) + + // whether the callback has changed the player's titan loadout + wasChanged = wasChanged || callbackRet.wasChanged + if ( callbackRet.wasChanged ) + loadout = callbackRet.loadout + + // whether the callback has indicated that we should run no more callbacks ( e.g. if we're forcing a given loadout to be chosen, we shouldn't run any more ) + if ( !callbackRet.runMoreCallbacks ) + break + } + + // do this again just in case + loadout.primaryMods.removebyvalue( "null" ) - return def + return loadout } void function LoadoutsMPInitPlayer( entity player ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_northstar_cheatcommands.nut b/Northstar.CustomServers/mod/scripts/vscripts/_northstar_cheatcommands.nut new file mode 100644 index 00000000..c79265ac --- /dev/null +++ b/Northstar.CustomServers/mod/scripts/vscripts/_northstar_cheatcommands.nut @@ -0,0 +1,69 @@ +untyped +global function NorthstarCheatCommands_Init + +void function NorthstarCheatCommands_Init() +{ + AddClientCommandCallback( "noclip", ClientCommandCallbackToggleNoclip ) + AddClientCommandCallback( "notarget", ClientCommandCallbackToggleNotarget ) + AddClientCommandCallback( "demigod", ClientCommandCallbackToggleDemigod ) + AddClientCommandCallback( "kill", ClientCommandCallbackKill ) + AddClientCommandCallback( "explode", ClientCommandCallbackExplode ) +} + +bool function ClientCommandCallbackToggleNoclip( entity player, array<string> args ) +{ + if ( !GetConVarBool( "sv_cheats" ) ) + return true + + print( player + " TOGGLED NOCLIP" ) + + if ( player.IsNoclipping() ) + player.SetPhysics( MOVETYPE_WALK ) + else + player.SetPhysics( MOVETYPE_NOCLIP ) + + return true +} + +bool function ClientCommandCallbackToggleNotarget( entity player, array<string> args ) +{ + if ( !GetConVarBool( "sv_cheats" ) ) + return true + + print( player + " TOGGLED NOTARGET" ) + + player.SetNoTarget( !player.GetNoTarget() ) + player.SetNoTargetSmartAmmo( player.GetNoTarget() ) + return true +} + +bool function ClientCommandCallbackToggleDemigod( entity player, array<string> args ) +{ + if ( !GetConVarBool( "sv_cheats" ) ) + return true + + print( player + " TOGGLED DEMIGOD" ) + + if ( IsDemigod( player ) ) + DisableDemigod( player ) + else + EnableDemigod( player ) + + return true +} + +bool function ClientCommandCallbackKill( entity player, array<string> args ) +{ + if ( IsAlive( player ) && ( GetConVarBool( "sv_cheats" ) || GetConVarBool( "ns_allow_kill_commands" ) ) ) + player.Die() + + return true +} + +bool function ClientCommandCallbackExplode( entity player, array<string> args ) +{ + if ( IsAlive( player ) && ( GetConVarBool( "sv_cheats" ) || GetConVarBool( "ns_allow_kill_commands" ) ) ) + player.Die( null, null, { scriptType = DF_GIB } ) + + return true +} diff --git a/Northstar.CustomServers/mod/scripts/vscripts/burnmeter/_burnmeter.gnut b/Northstar.CustomServers/mod/scripts/vscripts/burnmeter/_burnmeter.gnut index e2ca9917..056f0313 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/burnmeter/_burnmeter.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/burnmeter/_burnmeter.gnut @@ -82,10 +82,16 @@ void function BurnMeter_SetBoostRewardCount( string burnRef, int rewardCount ) file.boostRewardCount[burnRef] <- rewardCount
}
-int function BurnMeter_GetLimitedRewardCount( entity player )
+int function BurnMeter_GetLimitedRewardCount( entity player, string burnRef = "" )
{
- EarnObject earnObject = PlayerEarnMeter_GetReward( player )
- string burnRef = earnObject.ref
+ // added burnRef as a parameter, used for dice roll
+ // means we dont call two lots of BurnReward_GetRandom() whilst also being able to give multiple items per dice roll (ticks)
+ if (burnRef == "")
+ {
+ EarnObject earnObject = PlayerEarnMeter_GetReward( player )
+ burnRef = earnObject.ref
+ }
+
int limit = -1
int rewardCount = 1
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut index 935dc4d0..a20e7aa0 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/earn_meter/sv_earn_meter_mp.gnut @@ -182,7 +182,7 @@ void function EarnMeterMP_BoostEarned( entity player ) while ( burncard.ref == "burnmeter_random_foil" ) burncard = BurnReward_GetRandom() - for ( int i = 0; i < BurnMeter_GetLimitedRewardCount( player ); i++ ) + for ( int i = 0; i < BurnMeter_GetLimitedRewardCount( player, burncard.ref ); i++ ) BurnMeter_GiveRewardDirect( player, burncard.ref ) PlayerEarnMeter_DisableReward( player ) 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..9a94b848 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut @@ -1,6 +1,510 @@ +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 Aitdm_SpawnDropShip( node, team ) + continue + } + } + + points = SpawnPoints_GetDropPod() + entity node = points[ GetSpawnPointIndex( points, team ) ] + waitthread AiGameModes_SpawnDropPod( node.GetOrigin(), node.GetAngles(), team, ent, SquadHandler ) + } + + WaitFrame() + } +} + +void function Aitdm_SpawnDropShip( entity node, int team ) +{ + thread AiGameModes_SpawnDropShip( node.GetOrigin(), node.GetAngles(), team, 4, SquadHandler ) + wait 20 +} + +// 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/gamemodes/_gamemode_cp.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut index 74e6b58c..705b7836 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut @@ -150,8 +150,8 @@ void function GamemodeCP_OnPlayerKilled(entity victim, entity attacker, var dama void function removePlayerFromCapperArray_threaded(array<entity> capperArray,entity player) { WaitFrame() - if(capperArray.contains(player)) - capperArray.remove(capperArray.find(player)) + FindAndRemove(capperArray,player) + } void function RateSpawnpoints_CP( int checkClass, array<entity> spawnpoints, int team, entity player ) @@ -589,12 +589,12 @@ void function HardpointThink( HardpointStruct hardpoint ) foreach(entity player in hardpoint.imcCappers) { if(DistanceSqr(player.GetOrigin(),hardpointEnt.GetOrigin())>1200000) - hardpoint.imcCappers.remove(hardpoint.imcCappers.find(player)) + FindAndRemove(hardpoint.imcCappers,player) } foreach(entity player in hardpoint.militiaCappers) { if(DistanceSqr(player.GetOrigin(),hardpointEnt.GetOrigin())>1200000) - hardpoint.militiaCappers.remove(hardpoint.militiaCappers.find(player)) + FindAndRemove(hardpoint.militiaCappers,player) } @@ -656,7 +656,7 @@ void function OnHardpointLeft( entity trigger, entity player ) hardpoint = hardpointStruct if ( player.GetTeam() == TEAM_IMC ) - hardpoint.imcCappers.remove( hardpoint.imcCappers.find( player ) ) + FindAndRemove( hardpoint.imcCappers, player ) else FindAndRemove( hardpoint.militiaCappers, player ) foreach(CP_PlayerStruct playerStruct in file.players) @@ -702,4 +702,4 @@ string function GetHardpointGroup(entity hardpoint) //Hardpoint Entity B on Home return "B" return string(hardpoint.kv.hardpointGroup) -}
\ No newline at end of file +} diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut index b4ab26ea..99f34164 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut @@ -36,6 +36,7 @@ void function CaptureTheFlag_Init() AddCallback_OnClientConnected( CTFInitPlayer ) AddCallback_GameStateEnter( eGameState.Prematch, CreateFlags ) + AddCallback_GameStateEnter( eGameState.Epilogue, RemoveFlags ) AddCallback_OnTouchHealthKit( "item_flag", OnFlagCollected ) AddCallback_OnPlayerKilled( OnPlayerKilled ) AddCallback_OnPilotBecomesTitan( DropFlagForBecomingTitan ) @@ -109,6 +110,8 @@ void function CTFInitPlayer( entity player ) void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) { + if ( !IsValid( GetFlagForTeam( GetOtherTeam( victim.GetTeam() ) ) ) ) // getting a crash idk + return if ( GetFlagForTeam( GetOtherTeam( victim.GetTeam() ) ).GetParent() == victim ) { if ( victim != attacker && attacker.IsPlayer() ) @@ -137,8 +140,13 @@ void function CreateFlags() // likely this is because respawn uses distance checks from spawns to check this in official // but i don't like doing that so just using a list of maps to swap them on lol bool switchedSides = HasSwitchedSides() == 1 - bool shouldSwap = SWAP_FLAG_MAPS.contains( GetMapName() ) ? !switchedSides : switchedSides - + + // i dont know why this works and whatever we had before didn't, but yeah + bool shouldSwap = switchedSides + if (!shouldSwap && SWAP_FLAG_MAPS.contains( GetMapName() )) + shouldSwap = !shouldSwap + + int flagTeam = spawn.GetTeam() if ( shouldSwap ) { @@ -199,10 +207,35 @@ void function CreateFlags() } } + // reset the flag states, prevents issues where flag is home but doesnt think it's home when halftime goes + SetFlagStateForTeam( TEAM_MILITIA, eFlagState.None ) + SetFlagStateForTeam( TEAM_IMC, eFlagState.None ) + foreach ( entity player in GetPlayerArray() ) CTFInitPlayer( player ) } +void function RemoveFlags() +{ + // destroy all the flag related things + if ( IsValid( file.imcFlagSpawn ) ) + { + file.imcFlagSpawn.Destroy() + file.imcFlag.Destroy() + file.imcFlagReturnTrigger.Destroy() + } + if ( IsValid( file.militiaFlagSpawn ) ) + { + file.militiaFlagSpawn.Destroy() + file.militiaFlag.Destroy() + file.militiaFlagReturnTrigger.Destroy() + } + + // unsure if this is needed, since the flags are destroyed? idk + SetFlagStateForTeam( TEAM_MILITIA, eFlagState.None ) + SetFlagStateForTeam( TEAM_IMC, eFlagState.None ) +} + void function TrackFlagReturnTrigger( entity flag, entity returnTrigger ) { // this is a bit of a hack, it seems parenting the return trigger to the flag actually sets the pickup radius of the flag to be the same as the trigger @@ -270,10 +303,11 @@ void function DropFlagIfPhased( entity player, entity flag ) OnThreadEnd( function() : ( player ) { - DropFlag( player, true ) + if (GetGameState() == eGameState.Playing || GetGameState() == eGameState.SuddenDeath) + DropFlag( player, true ) }) - - while( flag.GetParent() == player ) + // the IsValid check is purely to prevent a crash due to a destroyed flag (epilogue) + while( IsValid(flag) && flag.GetParent() == player ) WaitFrame() } @@ -331,6 +365,9 @@ void function TrackFlagDropTimeout( entity flag ) void function ResetFlag( entity flag ) { + // prevents crash when flag is reset after it's been destroyed due to epilogue + if (!IsValid(flag)) + return // ensure we can't pickup the flag after it's been dropped but before it's been reset flag.s.canTake = false @@ -355,6 +392,12 @@ void function ResetFlag( entity flag ) void function CaptureFlag( entity player, entity flag ) { + // can only capture flags during normal play or sudden death + if (GetGameState() != eGameState.Playing && GetGameState() != eGameState.SuddenDeath) + { + printt( player + " tried to capture the flag, but the game state was " + GetGameState() + " not " + eGameState.Playing + " or " + eGameState.SuddenDeath) + return + } // reset flag ResetFlag( flag ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut index 33c0b8e9..ae933b71 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut @@ -25,6 +25,7 @@ void function Lobby_OnClientConnectionStarted( entity player ) void function Lobby_OnClientConnectionCompleted( entity player ) { + player.hasConnected = true FinishClientScriptInitialization( player ) } @@ -35,4 +36,4 @@ bool function ClientCommandCallback_StartPrivateMatchSearch( entity player, arra GameRules_ChangeMap( "mp_lobby", GAMETYPE ) return true -}
\ No newline at end of file +} 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/_base_gametype_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut index df6829c8..1c53167f 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut @@ -35,6 +35,8 @@ void function BaseGametype_Init_MPSP() AddPostDamageCallback( "player", AddToTitanDamageStat ) AddPostDamageCallback( "npc_titan", AddToTitanDamageStat ) + AddCallback_OnNPCKilled( CheckForAutoTitanDeath ) + AddCallback_OnPlayerKilled( CheckForAutoTitanDeath ) RegisterSignal( "PlayerRespawnStarted" ) RegisterSignal( "KillCamOver" ) } @@ -122,6 +124,7 @@ void function CodeCallback_OnClientConnectionStarted( entity player ) void function CodeCallback_OnClientConnectionCompleted( entity player ) { InitPersistentData( player ) + if ( IsLobby() ) { Lobby_OnClientConnectionCompleted( player ) @@ -485,7 +488,12 @@ void function RespawnAsTitan( entity player, bool manualPosition = false ) player.RespawnPlayer( null ) // spawn player as pilot so they get their pilot loadout on embark player.SetOrigin( titan.GetOrigin() ) - PilotBecomesTitan( player, titan ) // make player titan + // don't make player titan when entity batteryContainer is not valid. + // This will prevent a servercrash that sometimes occur when evac is disabled and somebody is calling a titan in the defeat screen. + if( IsValid( titan.GetTitanSoul().soul.batteryContainer ) ) + PilotBecomesTitan( player, titan ) // make player titan + else + print( "batteryContainer is not a valid entity in RespawnAsTitan(). Skipping PilotBecomesTitan()." ) } @@ -528,6 +536,24 @@ void function AddToTitanDamageStat( entity victim, var damageInfo ) attacker.AddToPlayerGameStat( file.titanDamageGameStat, amount ) // titan damage on } +void function CheckForAutoTitanDeath( entity victim, entity attacker, var damageInfo ) +{ + if( !IsValid(victim) || !victim.IsTitan() ) + return + + if( !victim.IsPlayer() ) + { + if (GetPetTitanOwner(victim) && GetPetTitanOwner(victim) != attacker) + foreach(player in GetPlayerArray()) + Remote_CallFunction_NonReplay( player, "ServerCallback_OnTitanKilled", attacker.GetEncodedEHandle(), victim.GetEncodedEHandle(), DamageInfo_GetCustomDamageType( damageInfo ), DamageInfo_GetDamageSourceIdentifier( damageInfo ) ) + } + else + { + if (victim != attacker) + foreach(player in GetPlayerArray()) + Remote_CallFunction_NonReplay( player, "ServerCallback_OnTitanKilled", attacker.GetEncodedEHandle(), victim.GetEncodedEHandle(), DamageInfo_GetCustomDamageType( damageInfo ), DamageInfo_GetDamageSourceIdentifier( damageInfo ) ) + } +} // stuff to change later diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut index 80fae331..0555df9b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut @@ -105,8 +105,12 @@ void function OnPrematchStart() DispatchSpawn( dropship ) // have to do this after dispatch otherwise it won't work for some reason - dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) - // could also use $"models/vehicle/goblin_dropship/goblin_dropship_hero.mdl", unsure which + // weirdly enough, tf2 actually does use different dropships for imc and militia, despite these concepts not really being a thing for players in tf2 + // probably was just missed by devs, but keeping it in for accuracy + if ( dropshipSpawn.GetTeam() == TEAM_IMC ) + dropship.SetModel( $"models/vehicle/goblin_dropship/goblin_dropship_hero.mdl" ) + else + dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) thread PlayAnim( dropship, "dropship_classic_mp_flyin" ) } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut index 2e565142..425a8b8b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut @@ -6,6 +6,9 @@ global function CodeCallback_DamagePlayerOrNPC global function GameModeRulesShouldGiveTimerCredit global function SetGameModeRulesShouldGiveTimerCredit global function SetGameModeRulesEarnMeterOnDamage +global function GameModeRulesRegisterTimerCreditExceptions +global function GameModeRulesRegisterTimerCreditException +global function GameModeRulesGetTimerCreditExceptions global function GetDamageOrigin global function CodeCallBack_ShouldTriggerSniperCam global function CodeCallback_ForceAIMissPlayer @@ -31,6 +34,13 @@ struct float titanMeterGainScale = 0.0001 bool functionref( entity, entity, var ) ShouldGiveTimerCreditGameModeRules void functionref( entity, entity, TitanDamage, float ) earnMeterOnDamageGameModeRulesCallback + array<int> gameModeRulesTimerCreditExceptions = [ + eDamageSourceId.mp_titancore_flame_wave, + eDamageSourceId.mp_titancore_flame_wave_secondary, + eDamageSourceId.mp_titancore_salvo_core, + damagedef_titan_fall, + damagedef_nuclear_core + ] table<entity, AccumulatedDamageData> playerAccumulatedDamageData } file @@ -582,15 +592,8 @@ bool function ShouldGiveTimerCredit_Default( entity player, entity victim, var d return false int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo ) - switch ( damageSourceID ) - { - case eDamageSourceId.mp_titancore_flame_wave: - case eDamageSourceId.mp_titancore_flame_wave_secondary: - case eDamageSourceId.mp_titancore_salvo_core: - case damagedef_titan_fall: - case damagedef_nuclear_core: - return false - } + if ( file.gameModeRulesTimerCreditExceptions.contains( damageSourceID ) ) + return false return true } @@ -605,6 +608,23 @@ void function SetGameModeRulesShouldGiveTimerCredit( bool functionref( entity, e file.ShouldGiveTimerCreditGameModeRules = rules } +void function GameModeRulesRegisterTimerCreditExceptions( array<int> ids ) +{ + foreach( id in ids ) + GameModeRulesRegisterTimerCreditException( id ) +} + +void function GameModeRulesRegisterTimerCreditException( int id ) +{ + if ( !file.gameModeRulesTimerCreditExceptions.contains( id ) ) + file.gameModeRulesTimerCreditExceptions.append( id ) +} + +array<int> function GameModeRulesGetTimerCreditExceptions() +{ + return file.gameModeRulesTimerCreditExceptions +} + function TitanDamageFlinch( entity ent, damageInfo ) { if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut index e8b636ff..bfcd23e0 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut @@ -9,6 +9,7 @@ global function AddCallback_OnRoundEndCleanup global function SetShouldUsePickLoadoutScreen global function SetSwitchSidesBased global function SetSuddenDeathBased +global function SetTimerBased global function SetShouldUseRoundWinningKillReplay global function SetRoundWinningKillReplayKillClasses global function SetRoundWinningKillReplayAttacker @@ -28,6 +29,7 @@ struct { bool usePickLoadoutScreen bool switchSidesBased bool suddenDeathBased + bool timerBased = true int functionref() timeoutWinnerDecisionFunc // for waitingforplayers @@ -228,7 +230,7 @@ void function GameStateEnter_Playing_Threaded() endTime = expect float( GetServerVar( "gameEndTime" ) ) // time's up! - if ( Time() >= endTime ) + if ( Time() >= endTime && file.timerBased ) { int winningTeam if ( file.timeoutWinnerDecisionFunc != null ) @@ -279,9 +281,13 @@ void function GameStateEnter_WinnerDetermined_Threaded() } WaitFrame() // wait a frame so other scripts can setup killreplay stuff + + // set gameEndTime to current time, so hud doesn't display time left in the match + SetServerVar( "gameEndTime", Time() ) + SetServerVar( "roundEndTime", Time() ) entity replayAttacker = file.roundWinningKillReplayAttacker - bool doReplay = Replay_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker ) + bool doReplay = Replay_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker ) && !ClassicMP_ShouldRunEpilogue() && Time() - file.roundWinningKillReplayTime <= ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY && winningTeam != TEAM_UNASSIGNED float replayLength = 2.0 // extra delay if no replay @@ -676,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 } ) @@ -715,6 +721,11 @@ void function SetSuddenDeathBased( bool suddenDeathBased ) file.suddenDeathBased = suddenDeathBased } +void function SetTimerBased( bool timerBased ) +{ + file.timerBased = timerBased +} + void function SetShouldUseRoundWinningKillReplay( bool shouldUse ) { SetServerVar( "roundWinningKillReplayEnabled", shouldUse ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02.nut index 83338c8e..02b11010 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_colony02.nut @@ -2,6 +2,11 @@ global function CodeCallback_MapInit void function CodeCallback_MapInit() { + AddCallback_EntitiesDidLoad( CreateEvacNodes ) +} + +void function CreateEvacNodes() +{ AddEvacNode( CreateScriptRef( < -475.129913, 1480.167847, 527.363953 >, < 8.841560, 219.338501, 0 > ) ) AddEvacNode( CreateScriptRef( < 1009.315186, 3999.888916, 589.914917 >, < 23.945116, -146.680725, 0 > ) ) AddEvacNode( CreateScriptRef( < 2282.868896, -1363.706543, 846.188660 >, < 23.945116, -146.680725, 0 > ) ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut index 592422ee..d5162f0b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_lf_deck.nut @@ -14,9 +14,11 @@ void function CodeCallback_MapInit() void function DeckSpawnWorkerDrone( entity spawnpoint ) { - - entity drone = CreateWorkerDrone( TEAM_UNASSIGNED, spawnpoint.GetOrigin(), spawnpoint.GetAngles() ) + entity drone = CreateWorkerDrone( TEAM_UNASSIGNED, spawnpoint.GetOrigin() - < 0, 0, 150 >, spawnpoint.GetAngles() ) DispatchSpawn( drone ) + + // this seems weird for drones + thread AssaultMoveTarget( drone, spawnpoint ) } void function DeckSpawnMarvinForIdleNode( entity node ) @@ -24,7 +26,5 @@ void function DeckSpawnMarvinForIdleNode( entity node ) entity marvin = CreateMarvin( TEAM_UNASSIGNED, node.GetOrigin(), node.GetAngles() ) DispatchSpawn( marvin ) - // doing this because no ai rn - if ( GetAINScriptVersion() == -1 ) - thread PlayAnim( marvin, node.kv.leveled_animation ) + thread AssaultMoveTarget( marvin, node ) }
\ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut index c1290bb0..4a53c4fe 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut @@ -88,10 +88,12 @@ void function SpawnMarvinsForRound() entity marvin = CreateMarvin( TEAM_UNASSIGNED, spawner.GetOrigin(), spawner.GetAngles() ) marvin.kv.health = 1 marvin.kv.max_health = 1 - marvin.kv.spawnflags = 516 - marvin.kv.contents = (int(marvin.kv.contents) | CONTENTS_NOGRAPPLE) + //marvin.kv.spawnflags = 516 + marvin.kv.contents = ( int( marvin.kv.contents ) | CONTENTS_NOGRAPPLE ) DispatchSpawn( marvin ) HideName( marvin ) + + thread MarvinJobThink( marvin ) } } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut index 84317c6e..5bf150c0 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut @@ -396,55 +396,10 @@ void function InitPreferSpawnNodes() // frontline void function RateSpawnpoints_Frontline( int checkClass, array<entity> spawnpoints, int team, entity player ) { - Frontline frontline = GetFrontline( player.GetTeam() ) - - // heavily based on ctf spawn algo iteration 4, only changes it at the end - array<entity> startSpawns = SpawnPoints_GetPilotStart( team ) - array<entity> enemyStartSpawns = SpawnPoints_GetPilotStart( GetOtherTeam( team ) ) - - if ( startSpawns.len() == 0 || enemyStartSpawns.len() == 0 ) // ensure we don't crash - return - - // get average startspawn position and max dist between spawns - // could probably cache this, tbh, not like it should change outside of halftimes - vector averageFriendlySpawns - float maxFriendlySpawnDist - - foreach ( entity spawn in startSpawns ) - { - foreach ( entity otherSpawn in startSpawns ) - { - float dist = Distance2D( spawn.GetOrigin(), otherSpawn.GetOrigin() ) - if ( dist > maxFriendlySpawnDist ) - maxFriendlySpawnDist = dist - } - - averageFriendlySpawns += spawn.GetOrigin() - } - - averageFriendlySpawns /= startSpawns.len() - - // get average enemy startspawn position - vector averageEnemySpawns - - foreach ( entity spawn in enemyStartSpawns ) - averageEnemySpawns += spawn.GetOrigin() - - averageEnemySpawns /= enemyStartSpawns.len() - - // from here, rate spawns - float baseDistance = Distance2D( averageFriendlySpawns, averageEnemySpawns ) - foreach ( entity spawn in spawnpoints ) + foreach ( entity spawnpoint in spawnpoints ) { - // ratings should max/min out at 100 / -100 - // start by prioritizing closer spawns, but not so much that enemies won't really affect them - float rating = 10 * ( 1.0 - Distance2D( averageFriendlySpawns, spawn.GetOrigin() ) / baseDistance ) - - // rate based on distance to frontline, and then prefer spawns in the same dir from the frontline as the combatdir - rating += rating * ( 1.0 - ( Distance2D( spawn.GetOrigin(), frontline.friendlyCenter ) / baseDistance ) ) - rating *= fabs( frontline.combatDir.y - Normalize( spawn.GetOrigin() - averageFriendlySpawns ).y ) - - spawn.CalculateRating( checkClass, player.GetTeam(), rating, rating ) + float rating = spawnpoint.CalculateFrontlineRating() + spawnpoint.CalculateRating( checkClass, player.GetTeam(), rating, rating > 0 ? rating * 0.25 : rating ) } } |