diff options
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/mp')
9 files changed, 1133 insertions, 717 deletions
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 9288f75e..b77a37b2 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut @@ -43,6 +43,8 @@ void function BaseGametype_Init_MPSP() AddCallback_OnPlayerKilled( CheckForAutoTitanDeath ) RegisterSignal( "PlayerRespawnStarted" ) RegisterSignal( "KillCamOver" ) + + FlagInit( "WeaponDropsAllowed", true ) } void function SetIntermissionCamera( entity camera ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut index 466a5042..016097f2 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut @@ -1,6 +1,276 @@ global function InitChallenges +global function SetPlayerChallengeEvacState //Hooked in _evac.gnut +global function SetPlayerChallengeMatchWon //Hooked in _score.nut +global function SetPlayerChallengeMatchComplete //Hooked in _score.nut +global function SetPlayerChallengeMeritScore //Up to gamemodes to use this directly if needed +global function IncrementPlayerChallengeTitanLeveledUp //Hooked in titan_xp.gnut +global function IncrementPlayerChallengeWeaponLeveledUp //Hooked in weapon_xp.gnut +global function IncrementPlayerChallengeFactionLeveledUp //Hooked in faction_xp.gnut (invisible but necessary for post-summary menu) +global function RegisterChallenges_OnMatchEnd //Hooked in _gamestate_mp.gnut + +global function HasPlayerCompletedMeritScore //Check from gamemodes to not reapply SetPlayerChallengeMeritScore +global function SetupGenericTDMChallenge //Used by gamemodes which simply adopts the: "Kill 3 Pilots without dying." Challenge +global function SetupGenericFFAChallenge //Used by gamemodes which simply adopts the: "Kill 5 Pilots." Challenge + +struct +{ + table< entity, int > playerTotalMeritCount + table< entity, bool > playerChallenge + table< entity, int > pilotstreak + bool isHappyHourActive +} file + + + + + + +/*============================================================================================================= + __ __ _ _ ____ _ _ _ + | \/ | __ _ | |_ ___ | |__ / ___|| |__ __ _ | || | ___ _ __ __ _ ___ ___ + | |\/| | / _` || __|/ __|| '_ \ | | | '_ \ / _` || || | / _ \| '_ \ / _` | / _ \/ __| + | | | || (_| || |_| (__ | | | | | |___ | | | || (_| || || || __/| | | || (_| || __/\__ \ + |_| |_| \__,_| \__|\___||_| |_| \____||_| |_| \__,_||_||_| \___||_| |_| \__, | \___||___/ + |___/ +=============================================================================================================*/ void function InitChallenges() { +#if (UI && CLIENT) + + SCB_SetCompleteMeritState( 4 ) + SCB_SetEvacMeritState( 4 ) + SCB_SetMeritCount( 4 ) + SCB_SetScoreMeritState( 4 ) + SCB_SetWinMeritState( 4 ) + SCB_SetWeaponMeritCount( -1 ) + SCB_SetTitanMeritCount( -1 ) + +#elseif (SERVER && MP) + + AddCallback_OnClientConnected( SetupPlayerMenuChallenges ) + AddCallback_OnClientDisconnected( RemovePlayerFromChallengePool ) + +#endif +} + +void function SetupPlayerMenuChallenges( entity player ) +{ + file.playerTotalMeritCount[ player ] <- 0 + file.pilotstreak[ player ] <- 0 + file.playerChallenge[ player ] <- false + + thread SetupChallenges_Threaded( player ) +} +void function SetupChallenges_Threaded( entity player ) +{ + player.EndSignal( "OnDestroy" ) + + WaitFrame() + + Remote_CallFunction_UI( player, "SCB_SetCompleteMeritState", 0 ) + Remote_CallFunction_UI( player, "SCB_SetEvacMeritState", 4 ) //4 tells RUI to hide it + Remote_CallFunction_UI( player, "SCB_SetMeritCount", 0 ) + Remote_CallFunction_UI( player, "SCB_SetScoreMeritState", 0 ) + Remote_CallFunction_UI( player, "SCB_SetWinMeritState", 0 ) + Remote_CallFunction_UI( player, "SCB_SetWeaponMeritCount", 0 ) + Remote_CallFunction_UI( player, "SCB_SetTitanMeritCount", 0 ) +} + +void function SetupGenericTDMChallenge() +{ + AddCallback_OnPlayerKilled( TDMChallenges_OnPlayerKilled ) +} + +void function SetupGenericFFAChallenge() +{ + AddCallback_OnPlayerKilled( FFAChallenges_OnPlayerKilled ) +} + +void function RemovePlayerFromChallengePool( entity player ) +{ + if( player in file.playerChallenge ) + delete file.playerChallenge[ player ] + if( player in file.playerTotalMeritCount ) + delete file.playerTotalMeritCount[ player ] + if( player in file.pilotstreak ) + delete file.pilotstreak[ player ] +} + +void function RegisterChallenges_OnMatchEnd() +{ + bool eliteWarpaintRNG = false + + if( RandomIntRange( 0, 100 ) <= 30 ) //30% Chance to trigger akin to vanilla, apply always since all players have paid cosmetics unlocked + eliteWarpaintRNG = true + + foreach( player in GetPlayerArray() ) + { + player.SetPersistentVar( "isPostGameScoreboardValid", true ) + player.SetPersistentVar( "isFDPostGameScoreboardValid", false ) //FD itself overrides this right after when match ends + SetUIVar( level, "showGameSummary", true ) + + if( eliteWarpaintRNG ) + SetPlayerChallengeSquadLeader( player ) + + if( ShouldAwardHappyHourBonus( player ) ) + { + AddPlayerScore( player, "HappyHourBonus" ) + player.SetPersistentVar( "xp_match[" + XP_TYPE.HAPPY_HOUR + "]", 5 ) //The XP Given from Happy Hour Score is 5 merits + } + } +} + +void function TDMChallenges_OnPlayerKilled( entity victim, entity attacker, var damageInfo ) +{ + if ( victim == attacker || !attacker.IsPlayer() || GetGameState() != eGameState.Playing ) + return + + if ( victim.IsPlayer() ) + { + if( victim in file.pilotstreak ) + file.pilotstreak[victim] = 0 + if( attacker in file.pilotstreak ) + { + file.pilotstreak[attacker]++ + if( file.pilotstreak[attacker] >= 3 && !HasPlayerCompletedMeritScore( attacker ) ) + { + AddPlayerScore( attacker, "ChallengeTDM" ) + SetPlayerChallengeMeritScore( attacker ) + } + } + } +} + +void function FFAChallenges_OnPlayerKilled( entity victim, entity attacker, var damageInfo ) +{ + if ( victim == attacker || !attacker.IsPlayer() || GetGameState() != eGameState.Playing ) + return + + if ( victim.IsPlayer() && attacker in file.pilotstreak ) + { + file.pilotstreak[attacker]++ + if( file.pilotstreak[attacker] >= 5 && !HasPlayerCompletedMeritScore( attacker ) ) + { + AddPlayerScore( attacker, "ChallengePVPKillCount" ) + SetPlayerChallengeMeritScore( attacker ) + } + } +} + +bool function HasPlayerCompletedMeritScore( entity player ) +{ + Assert( player in file.playerChallenge, player + " is not registered in the challenge pool hooks." ) + return file.playerChallenge[ player ] +} + + + + + + + +/*============================================================================================================= + ____ _ _ _ _ + / ___| __ _ _ __ ___ ___ _ __ ___ ___ __| | ___ | | | | ___ ___ | | __ ___ + | | _ / _` || '_ ` _ \ / _ \| '_ ` _ \ / _ \ / _` | / _ \ | |_| | / _ \ / _ \ | |/ // __| + | |_| || (_| || | | | | || __/| | | | | || (_) || (_| || __/ | _ || (_) || (_) || < \__ \ + \____| \__,_||_| |_| |_| \___||_| |_| |_| \___/ \__,_| \___| |_| |_| \___/ \___/ |_|\_\|___/ + +=============================================================================================================*/ + +void function SetPlayerChallengeEvacState( entity player, int successEvac = 0 ) +{ + if( successEvac == 0 ) //Evac Ship destroyed + Remote_CallFunction_UI( player, "SCB_SetEvacMeritState", 2 ) + + else if( successEvac == 1 ) //Player itself managed to evac + { + file.playerTotalMeritCount[ player ]++ + Remote_CallFunction_UI( player, "SCB_SetEvacMeritState", 1 ) + player.SetPersistentVar( "xp_match[" + XP_TYPE.EVAC + "]", 1 ) + Remote_CallFunction_UI( player, "SCB_SetMeritCount", file.playerTotalMeritCount[ player ] ) + } + + else if( successEvac == 2 ) //Team managed to evac + Remote_CallFunction_UI( player, "SCB_SetEvacMeritState", 3 ) +} + +void function SetPlayerChallengeMatchWon( entity player, bool playerWon ) +{ + if( playerWon ) + { + file.playerTotalMeritCount[ player ]++ + Remote_CallFunction_UI( player, "SCB_SetWinMeritState", 1 ) + player.SetPersistentVar( "xp_match[" + XP_TYPE.MATCH_VICTORY + "]", 1 ) + player.SetPersistentVar( "matchWin", true ) + Remote_CallFunction_UI( player, "SCB_SetMeritCount", file.playerTotalMeritCount[ player ] ) + } + else + Remote_CallFunction_UI( player, "SCB_SetWinMeritState", -1 ) +} + +void function SetPlayerChallengeMatchComplete( entity player ) +{ + file.playerTotalMeritCount[ player ]++ + Remote_CallFunction_UI( player, "SCB_SetCompleteMeritState", 1 ) + player.SetPersistentVar( "xp_match[" + XP_TYPE.MATCH_COMPLETED + "]", 1 ) + player.SetPersistentVar( "matchComplete", true ) + Remote_CallFunction_UI( player, "SCB_SetMeritCount", file.playerTotalMeritCount[ player ] ) +} + +void function SetPlayerChallengeSquadLeader( entity player ) +{ + if( !ProgressionEnabledForPlayer( player ) ) + return + + Remote_CallFunction_NonReplay( player, "ServerCallback_SquadLeaderDoubleXP" ) + Remote_CallFunction_NonReplay( player, "ServerCallback_SquadLeaderBonus", player.GetEncodedEHandle() ) + player.SetPersistentVar( "matchSquadBonus", true ) + Player_GiveDoubleXP( player, 1 ) + foreach( entity teamplayer in GetPlayerArrayOfTeam( player.GetTeam() ) ) + { + if( teamplayer == player ) + continue + + Remote_CallFunction_NonReplay( player, "ServerCallback_SquadLeaderBonus", teamplayer.GetEncodedEHandle() ) + } +} + +void function SetPlayerChallengeMeritScore( entity player ) +{ + if( !HasPlayerCompletedMeritScore( player ) ) + { + file.playerChallenge[ player ] = true + file.playerTotalMeritCount[ player ]++ + Remote_CallFunction_UI( player, "SCB_SetScoreMeritState", 1 ) + player.SetPersistentVar( "xp_match[" + XP_TYPE.SCORE_MILESTONE + "]", 1 ) + player.SetPersistentVar( "matchScoreEvent", true ) + Remote_CallFunction_UI( player, "SCB_SetMeritCount", file.playerTotalMeritCount[ player ] ) + } +} + +void function IncrementPlayerChallengeTitanLeveledUp( entity player ) +{ + player.p.meritData.titanMerits++ + file.playerTotalMeritCount[ player ]++ + + Remote_CallFunction_UI( player, "SCB_SetTitanMeritCount", player.p.meritData.titanMerits++ ) + Remote_CallFunction_UI( player, "SCB_SetMeritCount", file.playerTotalMeritCount[ player ] ) +} + +void function IncrementPlayerChallengeWeaponLeveledUp( entity player ) +{ + player.p.meritData.weaponMerits++ + file.playerTotalMeritCount[ player ]++ + + Remote_CallFunction_UI( player, "SCB_SetWeaponMeritCount", player.p.meritData.weaponMerits ) + Remote_CallFunction_UI( player, "SCB_SetMeritCount", file.playerTotalMeritCount[ player ] ) +} + +void function IncrementPlayerChallengeFactionLeveledUp( entity player ) +{ + file.playerTotalMeritCount[ player ]++ + Remote_CallFunction_UI( player, "SCB_SetMeritCount", file.playerTotalMeritCount[ player ] ) }
\ No newline at end of file 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 23ae37a1..c3bdf01c 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 @@ -26,18 +26,9 @@ const int MAX_DROPSHIP_PLAYERS = 4 global const float DROPSHIP_INTRO_LENGTH = 15.0 // TODO tweak this -struct IntroDropship -{ - entity dropship - - int playersInDropship - entity[MAX_DROPSHIP_PLAYERS] players -} - struct { - // these used to be IntroDropship[2]s but i wanted to be able to use array.getrandom so they have to be actual arrays - array<IntroDropship> militiaDropships - array<IntroDropship> imcDropships + table< entity, array<entity> > militiaDropships + table< entity, array<entity> > imcDropships float introStartTime } file @@ -52,7 +43,12 @@ void function ClassicMP_DefaultDropshipIntro_Setup() void function DropshipIntro_OnClientConnected( entity player ) { if ( GetGameState() == eGameState.Prematch ) - thread SpawnPlayerIntoDropship( player ) + { + if( PlayerCanSpawn( player ) ) + DoRespawnPlayer( player, null ) + + PutPlayerInDropship( player ) + } } void function OnPrematchStart() @@ -62,11 +58,11 @@ void function OnPrematchStart() print( "starting dropship intro!" ) file.introStartTime = Time() - // make 2 empty dropship structs per team - IntroDropship emptyDropship + // Clear Dropship arrays of Teams for Match Restarts (i.e Half-Times) file.militiaDropships.clear() file.imcDropships.clear() + // Try to gather all possible Dropship spawn points for Team array<entity> validDropshipSpawns array<entity> dropshipSpawns = GetEntArrayByClass_Expensive( "info_spawnpoint_dropship_start" ) foreach ( entity dropshipSpawn in dropshipSpawns ) @@ -78,47 +74,47 @@ void function OnPrematchStart() validDropshipSpawns.append( dropshipSpawn ) } - // if no dropship spawns for this mode, just allow any dropship spawns + // Use any spawn point if not enough valid for this Gamemode exists if ( validDropshipSpawns.len() < 2 ) validDropshipSpawns = dropshipSpawns // spawn dropships foreach ( entity dropshipSpawn in validDropshipSpawns ) { - // todo: possibly make this only spawn dropships if we've got enough players to need them int createTeam = HasSwitchedSides() ? GetOtherTeam( dropshipSpawn.GetTeam() ) : dropshipSpawn.GetTeam() - array<IntroDropship> teamDropships = createTeam == TEAM_MILITIA ? file.militiaDropships : file.imcDropships + table< entity, array<entity> > teamDropships = createTeam == TEAM_MILITIA ? file.militiaDropships : file.imcDropships if ( teamDropships.len() >= 2 ) - continue + break - // create entity entity dropship = CreateDropship( createTeam, dropshipSpawn.GetOrigin(), dropshipSpawn.GetAngles() ) - - teamDropships.append( clone emptyDropship ) - teamDropships[ teamDropships.len() - 1 ].dropship = dropship - AddAnimEvent( dropship, "dropship_warpout", WarpoutEffect ) + dropship.SetValueForModelKey( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) - dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) + if ( dropshipSpawn.GetTeam() == TEAM_IMC ) + dropship.SetValueForModelKey( $"models/vehicle/goblin_dropship/goblin_dropship_hero.mdl" ) DispatchSpawn( dropship ) - // have to do this after dispatch otherwise it won't work for some reason - // 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 + dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) 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" ) + + teamDropships[ dropship ] <- [ null, null, null, null ] thread PlayAnim( dropship, "dropship_classic_mp_flyin" ) } + // Populate Dropships foreach ( entity player in GetPlayerArray() ) { if ( !IsPrivateMatchSpectator( player ) ) - thread SpawnPlayerIntoDropship( player ) + { + if( PlayerCanSpawn( player ) ) + DoRespawnPlayer( player, null ) + + PutPlayerInDropship( player ) + } else RespawnPrivateMatchSpectator( player ) } @@ -128,76 +124,79 @@ void function OnPrematchStart() void function EndIntroWhenFinished() { - wait 15.0 + wait DROPSHIP_INTRO_LENGTH ClassicMP_OnIntroFinished() } -void function SpawnPlayerIntoDropship( entity player ) +void function PutPlayerInDropship( entity player ) { - player.EndSignal( "OnDestroy" ) + //Find the player's dropship and seat + table< entity, array<entity> > teamDropships + if ( player.GetTeam() == TEAM_MILITIA ) + teamDropships = file.militiaDropships + else + teamDropships = file.imcDropships + + entity playerDropship + array< int > availableShipSlots + array< entity > introDropships + int playerDropshipIndex = RandomInt( MAX_DROPSHIP_PLAYERS ) + foreach( dropship, playerslot in teamDropships ) + { + introDropships.append( dropship ) + for ( int i = 0; i < MAX_DROPSHIP_PLAYERS; i++ ) + { + if ( !IsValidPlayer( playerslot[i] ) ) + availableShipSlots.append( i ) + } + + if( !availableShipSlots.len() ) + continue + + int slotPick = availableShipSlots.getrandom() + playerslot[slotPick] = player + playerDropship = dropship + playerDropshipIndex = slotPick + break + } + + if( !IsAlive( playerDropship ) ) //If we're at this point, we have more players than we do dropships, so just pick a random one + playerDropship = introDropships.getrandom() + + thread SpawnPlayerIntoDropship( player, playerDropshipIndex, playerDropship ) +} - if ( IsAlive( player ) ) - player.Die() // kill them so we don't have any issues respawning them later +void function SpawnPlayerIntoDropship( entity player, int playerDropshipIndex, entity playerDropship ) +{ + player.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDeath" ) - player.s.dropshipIntroIsJumping <- false - OnThreadEnd( function() : ( player ) + OnThreadEnd( function() : ( player, playerDropshipIndex, playerDropship ) { if ( IsValid( player ) ) { player.ClearParent() ClearPlayerAnimViewEntity( player ) - - if ( !player.s.dropshipIntroIsJumping ) - { - player.MovementEnable() - player.EnableWeaponViewModel() - RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING ) - } + } + if( IsAlive( playerDropship ) ) + { + if ( playerDropship.GetTeam() == TEAM_MILITIA ) + file.militiaDropships[ playerDropship ][ playerDropshipIndex ] = null + else + file.imcDropships[ playerDropship ][ playerDropshipIndex ] = null } }) - WaitFrame() - - player.EndSignal( "OnDeath" ) - - // find the player's dropship and seat - array<IntroDropship> teamDropships - if ( player.GetTeam() == TEAM_MILITIA ) - teamDropships = file.militiaDropships - else - teamDropships = file.imcDropships - - IntroDropship playerDropship - int playerDropshipIndex = -1 - foreach ( IntroDropship dropship in teamDropships ) - for ( int i = 0; i < dropship.players.len(); i++ ) - if ( dropship.players[ i ] == null ) - { - playerDropship = dropship - playerDropshipIndex = i - - dropship.players[ i ] = player - break - } - - if ( playerDropship.dropship == null ) - { - // if we're at this point, we have more players than we do dropships, so just pick a random one - playerDropship = teamDropships.getrandom() - playerDropshipIndex = RandomInt( MAX_DROPSHIP_PLAYERS ) - } - - // respawn player and holster their weapons so they aren't out - if ( !IsAlive( player ) ) - player.RespawnPlayer( null ) - HolsterAndDisableWeapons(player) + HolsterAndDisableWeapons( player ) player.DisableWeaponViewModel() + UnMuteAll( player ) + StopSoundOnEntity( player, "Duck_For_FrontierDefenseTitanSelectScreen" ) // hide hud and fade screen out from black AddCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING ) ScreenFadeFromBlack( player, 0.5, 0.5 ) // faction leaders are done clientside, spawn them here - Remote_CallFunction_NonReplay( player, "ServerCallback_SpawnFactionCommanderInDropship", playerDropship.dropship.GetEncodedEHandle(), file.introStartTime ) + Remote_CallFunction_NonReplay( player, "ServerCallback_SpawnFactionCommanderInDropship", playerDropship.GetEncodedEHandle(), file.introStartTime ) // do firstperson sequence FirstPersonSequenceStruct idleSequence @@ -208,9 +207,7 @@ void function SpawnPlayerIntoDropship( entity player ) idleSequence.viewConeFunction = ViewConeRampFree idleSequence.hideProxy = true idleSequence.setInitialTime = Time() - file.introStartTime - thread FirstPersonSequence( idleSequence, player, playerDropship.dropship ) - WaittillAnimDone( player ) - + waitthread FirstPersonSequence( idleSequence, player, playerDropship ) // todo: possibly rework this to actually get the time the idle anim takes and start the starttime of the jump sequence for very late joiners using that // jump sequence @@ -218,13 +215,17 @@ void function SpawnPlayerIntoDropship( entity player ) jumpSequence.firstPersonAnim = DROPSHIP_JUMP_ANIMS_POV[ playerDropshipIndex ] jumpSequence.thirdPersonAnim = DROPSHIP_JUMP_ANIMS[ playerDropshipIndex ] jumpSequence.attachment = "ORIGIN" + jumpSequence.viewConeFunction = ViewConeFree jumpSequence.setInitialTime = max( 0.0, Time() - ( file.introStartTime + 11.0 ) ) // pretty sure you should do this with GetScriptedAnimEventCycleFrac? // idk unsure how to use that, all i know is getsequenceduration > the length it actually should be - thread FirstPersonSequence( jumpSequence, player, playerDropship.dropship ) - WaittillAnimDone( player ) // somehow this is better than just waiting for the blocking FirstPersonSequence call? + #if BATTLECHATTER_ENABLED + if( playerDropshipIndex == 0 ) + PlayBattleChatterLine( player, "bc_pIntroChat" ) + #endif + + waitthread FirstPersonSequence( jumpSequence, player, playerDropship ) - player.s.dropshipIntroIsJumping <- true thread PlayerJumpsFromDropship( player ) } @@ -240,20 +241,21 @@ void function PlayerJumpsFromDropship( entity player ) // show weapon viewmodel and hud and let them move again player.MovementEnable() player.EnableWeaponViewModel() - DeployAndEnableWeapons(player) + DeployAndEnableWeapons( player ) RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING ) } }) - - // wait for intro timer to be fully done - wait ( file.introStartTime + DROPSHIP_INTRO_LENGTH ) - Time() - player.MovementDisable() // disable all movement but let them look around still - player.ConsumeDoubleJump() // movementdisable doesn't prevent double jumps // wait for player to hit the ground - wait 0.1 // assume players will never actually hit ground before this + player.ClearParent() + WaitFrame() + player.SetVelocity( < 0, 0, -100 > ) // Toss players a bit down so it makes a smoother transition when jumping off the Dropship + player.MovementDisable() // Disable all movement but let them look around still + player.ConsumeDoubleJump() // MovementDisable doesn't prevent double jumps + WaitFrame() while ( !player.IsOnGround() && !player.IsWallRunning() && !player.IsWallHanging() ) // todo this needs tweaking WaitFrame() - TryGameModeAnnouncement( player ) + if ( GetRoundsPlayed() == 0 ) //Intro is announced only for the first round in Vanilla as certain gamemodes have different announcements for rounds restarts + TryGameModeAnnouncement( player ) } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut index ff281d6e..0d1b42b7 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut @@ -21,6 +21,8 @@ global function SetTitanMeterGainScale #if MP global function CodeCallback_OnServerAnimEvent +global function CodeCallback_WeaponDropped +global function AddCallback_OnWeaponDropped #endif struct AccumulatedDamageData @@ -43,6 +45,7 @@ struct ] table<entity, AccumulatedDamageData> playerAccumulatedDamageData + array< void functionref( entity ) > weaponDroppedCallbacks } file void function CodeCallback_Init() @@ -1030,4 +1033,26 @@ void function CodeCallback_OnServerAnimEvent( entity ent, string eventName ) PerfEnd( PerfIndexServer.CB_OnServerAnimEvent ) } + +void function AddCallback_OnWeaponDropped( void functionref( entity ) callback ) +{ + file.weaponDroppedCallbacks.append( callback ) +} + +void function CodeCallback_WeaponDropped( entity weapon ) +{ + // shamelessly taken form SP + if ( !IsValid( weapon ) ) + return + + // Might look a bit hacky to put it here, but thats how SP does it + if ( !Flag( "WeaponDropsAllowed" ) ) + { + weapon.Destroy() + return + } + + foreach( callback in file.weaponDroppedCallbacks ) + callback( weapon ) +} #endif // #if MP
\ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut index 4c52a9bf..0c66f5a9 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut @@ -59,18 +59,21 @@ struct { bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) shouldTryUseProjectileReplayCallback } file -void function SetCallback_TryUseProjectileReplay( bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) callback ) -{ - file.shouldTryUseProjectileReplayCallback = callback -} -bool function ShouldTryUseProjectileReplay( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) -{ - if ( file.shouldTryUseProjectileReplayCallback != null ) - return file.shouldTryUseProjectileReplayCallback( victim, attacker, damageInfo, isRoundEnd ) - // default to true (vanilla behaviour) - return true -} + + + + + + + +/* + ██████ █████ ███ ███ ███████ ███████ ████████ █████ ████████ ███████ ███████ +██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███ ███████ ██ ████ ██ █████ ███████ ██ ███████ ██ █████ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██████ ██ ██ ██ ██ ███████ ███████ ██ ██ ██ ██ ███████ ███████ +*/ void function PIN_GameStart() { @@ -96,10 +99,66 @@ void function PIN_GameStart() AddCallback_OnPlayerKilled( OnPlayerKilled ) AddDeathCallback( "npc_titan", OnTitanKilled ) + AddCallback_EntityChangedTeam( "player", OnPlayerChangedTeam ) RegisterSignal( "CleanUpEntitiesForRoundEnd" ) } +void function GameState_EntitiesDidLoad() +{ + if ( GetClassicMPMode() || ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() ) + ClassicMP_SetupIntro() +} + +void function WaittillGameStateOrHigher( int gameState ) +{ + while ( GetGameState() < gameState ) + svGlobal.levelEnt.WaitSignal( "GameStateChanged" ) +} + +bool function ShouldTryUseProjectileReplay( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) +{ + if ( file.shouldTryUseProjectileReplayCallback != null ) + return file.shouldTryUseProjectileReplayCallback( victim, attacker, damageInfo, isRoundEnd ) + // default to true (vanilla behaviour) + return true +} + +/// This is to move all NPCs that a player owns from one team to the other during a match +/// Auto-Titans, Turrets, Ticks and Hacked Spectres will all move along together with the player to the new Team +/// Also possibly prevents mods that spawns other types of NPCs that players can own from breaking when switching (i.e Drones, Hacked Reapers) +void function OnPlayerChangedTeam( entity player ) +{ + if ( !player.hasConnected ) // Prevents players who just joined to trigger below code, as server always pre setups their teams + return + + if( IsIMCOrMilitiaTeam( player.GetTeam() ) ) + NotifyClientsOfTeamChange( player, GetOtherTeam( player.GetTeam() ), player.GetTeam() ) + + foreach( npc in GetNPCArray() ) + { + entity bossPlayer = npc.GetBossPlayer() + if ( IsValidPlayer( bossPlayer ) && bossPlayer == player && IsAlive( npc ) ) + SetTeam( npc, player.GetTeam() ) + } +} + + + + + + + + + +/* + ██████ █████ ███ ███ ███████ ███████ ███████ ████████ ██ ██ ██████ +██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███ ███████ ██ ████ ██ █████ ███████ █████ ██ ██ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██████ ██ ██ ██ ██ ███████ ███████ ███████ ██ ██████ ██ +*/ + void function SetGameState( int newState ) { if ( newState == GetGameState() ) @@ -114,23 +173,166 @@ void function SetGameState( int newState ) callbackFunc() } -void function GameState_EntitiesDidLoad() +void function AddTeamScore( int team, int amount ) { - if ( GetClassicMPMode() || ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() ) - ClassicMP_SetupIntro() + GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + amount ) + GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + amount ) + + int scoreLimit + if ( IsRoundBased() ) + scoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE ) + else + scoreLimit = GameMode_GetScoreLimit( GAMETYPE ) + + int score = GameRules_GetTeamScore( team ) + if ( score >= scoreLimit || GetGameState() == eGameState.SuddenDeath ) + SetWinner( team ) + else if ( ( file.switchSidesBased && !file.hasSwitchedSides ) && score >= ( scoreLimit.tofloat() / 2.0 ) ) + SetGameState( eGameState.SwitchingSides ) } -void function WaittillGameStateOrHigher( int gameState ) +void function SetWinner( int team, string winningReason = "", string losingReason = "" ) +{ + SetServerVar( "winningTeam", team ) + + file.gameWonThisFrame = true + thread UpdateGameWonThisFrameNextFrame() + + if ( winningReason.len() == 0 ) + file.announceRoundWinnerWinningSubstr = 0 + else + file.announceRoundWinnerWinningSubstr = GetStringID( winningReason ) + + if ( losingReason.len() == 0 ) + file.announceRoundWinnerLosingSubstr = 0 + else + file.announceRoundWinnerLosingSubstr = GetStringID( losingReason ) + + if ( GamePlayingOrSuddenDeath() ) + { + if ( IsRoundBased() ) + { + if ( team != TEAM_UNASSIGNED ) + { + GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + 1 ) + GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + 1 ) + } + + SetGameState( eGameState.WinnerDetermined ) + ScoreEvent_RoundComplete( team ) + } + else + { + SetGameState( eGameState.WinnerDetermined ) + ScoreEvent_MatchComplete( team ) + + array<entity> players = GetPlayerArray() + int functionref( entity, entity ) compareFunc = GameMode_GetScoreCompareFunc( GAMETYPE ) + if ( compareFunc != null ) + { + players.sort( compareFunc ) + int playerCount = players.len() + int currentPlace = 1 + for ( int i = 0; i < 3; i++ ) + { + if ( i >= playerCount ) + continue + + if ( i > 0 && compareFunc( players[i - 1], players[i] ) != 0 ) + currentPlace += 1 + + switch( currentPlace ) + { + case 1: + UpdatePlayerStat( players[i], "game_stats", "mvp" ) + UpdatePlayerStat( players[i], "game_stats", "mvp_total" ) + UpdatePlayerStat( players[i], "game_stats", "top3OnTeam" ) + break + case 2: + UpdatePlayerStat( players[i], "game_stats", "top3OnTeam" ) + break + case 3: + UpdatePlayerStat( players[i], "game_stats", "top3OnTeam" ) + break + } + } + } + } + } +} + +void function SetTimeoutWinnerDecisionFunc( int functionref() callback ) { - while ( GetGameState() < gameState ) - svGlobal.levelEnt.WaitSignal( "GameStateChanged" ) + file.timeoutWinnerDecisionFunc = callback +} + +void function SetCallback_TryUseProjectileReplay( bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) callback ) +{ + file.shouldTryUseProjectileReplayCallback = callback +} + +void function AddCallback_OnRoundEndCleanup( void functionref() callback ) +{ + file.roundEndCleanupCallbacks.append( callback ) +} + +void function SetShouldUsePickLoadoutScreen( bool shouldUse ) +{ + file.usePickLoadoutScreen = shouldUse +} + +void function SetSwitchSidesBased( bool switchSides ) +{ + file.switchSidesBased = switchSides +} + +void function SetSuddenDeathBased( bool suddenDeathBased ) +{ + file.suddenDeathBased = suddenDeathBased } +void function SetTimerBased( bool timerBased ) +{ + file.timerBased = timerBased +} -// logic for individual gamestates: +void function SetShouldUseRoundWinningKillReplay( bool shouldUse ) +{ + SetServerVar( "roundWinningKillReplayEnabled", shouldUse ) +} +void function SetRoundWinningKillReplayKillClasses( bool pilot, bool titan ) +{ + file.roundWinningKillReplayTrackPilotKills = pilot + file.roundWinningKillReplayTrackTitanKills = titan // player kills in titans should get tracked anyway, might be worth renaming this +} + +void function SetRoundWinningKillReplayAttacker( entity attacker, int inflictorEHandle = -1 ) +{ + file.roundWinningKillReplayTime = Time() + file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker ) + file.roundWinningKillReplayAttacker = attacker + file.roundWinningKillReplayInflictorEHandle = inflictorEHandle == -1 ? attacker.GetEncodedEHandle() : inflictorEHandle + file.roundWinningKillReplayTimeOfDeath = Time() +} + + + + + + + + + + +/* + ██████ ██ ██ ███████ ████████ ██████ ███ ███ ███████ ████████ █████ ██████ ████████ +██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██ ███████ ██ ██ ██ ██ ████ ██ ███████ ██ ███████ ██████ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██████ ██████ ███████ ██ ██████ ██ ██ ███████ ██ ██ ██ ██ ██ ██ +*/ -// eGameState.WaitingForCustomStart void function GameStateEnter_WaitingForCustomStart() { // unused in release, comments indicate this was supposed to be used for an e3 demo @@ -138,7 +340,22 @@ void function GameStateEnter_WaitingForCustomStart() } -// eGameState.WaitingForPlayers + + + + + + + + +/* +██ ██ █████ ██ ████████ ██ ███ ██ ██████ ███████ ██████ ██████ ██████ ██ █████ ██ ██ ███████ ██████ ███████ +██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ █ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ███ █████ ██ ██ ██████ ██████ ██ ███████ ████ █████ ██████ ███████ +██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ███ ███ ██ ██ ██ ██ ██ ██ ████ ██████ ██ ██████ ██ ██ ██ ███████ ██ ██ ██ ███████ ██ ██ ███████ +*/ + void function GameStateEnter_WaitingForPlayers() { foreach ( entity player in GetPlayerArray() ) @@ -170,7 +387,22 @@ void function WaitingForPlayers_ClientConnected( entity player ) ScreenFadeToBlackForever( player, 0.0 ) } -// eGameState.PickLoadout + + + + + + + + +/* +██████ ██ ██████ ██ ██ ██ ██████ █████ ██████ ██████ ██ ██ ████████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██████ ██ ██ █████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██████ ██ ██ ███████ ██████ ██ ██ ██████ ██████ ██████ ██ +*/ + void function GameStateEnter_PickLoadout() { thread GameStateEnter_PickLoadout_Threaded() @@ -189,7 +421,22 @@ void function GameStateEnter_PickLoadout_Threaded() } -// eGameState.Prematch + + + + + + + + +/* +██████ ██████ ███████ ███ ███ █████ ████████ ██████ ██ ██ +██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ +██████ ██████ █████ ██ ████ ██ ███████ ██ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██ ███████ ██ ██ ██ ██ ██ ██████ ██ ██ +*/ + void function GameStateEnter_Prematch() { int timeLimit = GameMode_GetTimeLimit( GAMETYPE ) * 60 @@ -201,6 +448,14 @@ void function GameStateEnter_Prematch() if ( !GetClassicMPMode() && !ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() ) thread StartGameWithoutClassicMP() + + // Initialise any spectators. Hopefully they are all initialised already in CodeCallback_OnClientConnectionCompleted + // (_base_gametype_mp.gnut) but for modes like LTS this doesn't seem to happen late enough to work properly. + foreach ( player in GetPlayerArray() ) + { + if ( IsPrivateMatchSpectator( player ) ) + InitialisePrivateMatchSpectatorPlayer( player ) + } } void function StartGameWithoutClassicMP() @@ -227,7 +482,22 @@ void function StartGameWithoutClassicMP() } -// eGameState.Playing + + + + + + + + +/* +██████ ██ █████ ██ ██ ██ ███ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ +██████ ██ ███████ ████ ██ ██ ██ ██ ██ ███ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ███████ ██ ██ ██ ██ ██ ████ ██████ +*/ + void function GameStateEnter_Playing() { thread GameStateEnter_Playing_Threaded() @@ -270,7 +540,22 @@ void function GameStateEnter_Playing_Threaded() } -// eGameState.WinnerDetermined + + + + + + + + +/* +██ ██ ██ ███ ██ ███ ██ ███████ ██████ ██████ ███████ ████████ ███████ ██████ ███ ███ ██ ███ ██ ███████ ██████ +██ ██ ██ ████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ████ ██ ██ ██ ██ +██ █ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██████ ██ ██ █████ ██ █████ ██████ ██ ████ ██ ██ ██ ██ ██ █████ ██ ██ +██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ███ ███ ██ ██ ████ ██ ████ ███████ ██ ██ ██████ ███████ ██ ███████ ██ ██ ██ ██ ██ ██ ████ ███████ ██████ +*/ + // these are likely innacurate const float ROUND_END_FADE_KILLREPLAY = 1.0 const float ROUND_END_DELAY_KILLREPLAY = 3.0 @@ -387,6 +672,7 @@ void function GameStateEnter_WinnerDetermined_Threaded() } else { + RegisterChallenges_OnMatchEnd() if ( ClassicMP_ShouldRunEpilogue() ) { ClassicMP_SetupEpilogue() @@ -436,7 +722,22 @@ void function PlayerWatchesRoundWinningKillReplay( entity player, float replayLe } -// eGameState.SwitchingSides + + + + + + + + +/* +███████ ██ ██ ██ ████████ ██████ ██ ██ ██ ███ ██ ██████ ███████ ██ ██████ ███████ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██ █ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ███ ███████ ██ ██ ██ █████ ███████ + ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ███ ███ ██ ██ ██████ ██ ██ ██ ██ ████ ██████ ███████ ██ ██████ ███████ ███████ +*/ + void function GameStateEnter_SwitchingSides() { thread GameStateEnter_SwitchingSides_Threaded() @@ -527,7 +828,22 @@ void function PlayerWatchesSwitchingSidesKillReplay( entity player, bool doRepla } -// eGameState.SuddenDeath + + + + + + + + +/* +███████ ██ ██ ██████ ██████ ███████ ███ ██ ██████ ███████ █████ ████████ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ █████ ███████ ██ ███████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██████ ██████ ██████ ███████ ██ ████ ██████ ███████ ██ ██ ██ ██ ██ +*/ + void function GameStateEnter_SuddenDeath() { // disable respawns, suddendeath calling is done on a kill callback @@ -549,7 +865,22 @@ void function GameStateEnter_SuddenDeath() } -// eGameState.Postmatch + + + + + + + + +/* +██████ ██████ ███████ ████████ ███ ███ █████ ████████ ██████ ██ ██ +██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ +██████ ██ ██ ███████ ██ ██ ████ ██ ███████ ██ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██████ ███████ ██ ██ ██ ██ ██ ██ ██████ ██ ██ +*/ + void function GameStateEnter_Postmatch() { foreach ( entity player in GetPlayerArray() ) @@ -582,7 +913,21 @@ void function ForceFadeToBlack( entity player ) } -// shared across multiple gamestates + + + + + + + + +/* +██ ██ ██ ██ ██ ██████ █████ ██ ██ ██████ █████ ██████ ██ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +█████ ██ ██ ██ ██ ███████ ██ ██ ██████ ███████ ██ █████ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██ ██ ███████ ███████ ██████ ██ ██ ███████ ███████ ██████ ██ ██ ██████ ██ ██ ███████ +*/ void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) { @@ -712,10 +1057,22 @@ void function OnTitanKilled( entity victim, var damageInfo ) } } -void function AddCallback_OnRoundEndCleanup( void functionref() callback ) -{ - file.roundEndCleanupCallbacks.append( callback ) -} + + + + + + + + + +/* +████████ ██████ ██████ ██ ███████ ██ ██ ███ ██ ██████ ████████ ██ ██████ ███ ██ ███████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ████ ██ ██ + ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██ ██████ ██████ ███████ ██ ██████ ██ ████ ██████ ██ ██ ██████ ██ ████ ███████ +*/ void function CleanUpEntitiesForRoundEnd() { @@ -753,86 +1110,6 @@ void function CleanUpEntitiesForRoundEnd() SetPlayerDeathsHidden( false ) } - - -// stuff for gamemodes to call - -void function SetShouldUsePickLoadoutScreen( bool shouldUse ) -{ - file.usePickLoadoutScreen = shouldUse -} - -void function SetSwitchSidesBased( bool switchSides ) -{ - file.switchSidesBased = switchSides -} - -void function SetSuddenDeathBased( bool suddenDeathBased ) -{ - file.suddenDeathBased = suddenDeathBased -} - -void function SetTimerBased( bool timerBased ) -{ - file.timerBased = timerBased -} - -void function SetShouldUseRoundWinningKillReplay( bool shouldUse ) -{ - SetServerVar( "roundWinningKillReplayEnabled", shouldUse ) -} - -void function SetRoundWinningKillReplayKillClasses( bool pilot, bool titan ) -{ - file.roundWinningKillReplayTrackPilotKills = pilot - file.roundWinningKillReplayTrackTitanKills = titan // player kills in titans should get tracked anyway, might be worth renaming this -} - -void function SetRoundWinningKillReplayAttacker( entity attacker, int inflictorEHandle = -1 ) -{ - file.roundWinningKillReplayTime = Time() - file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker ) - file.roundWinningKillReplayAttacker = attacker - file.roundWinningKillReplayInflictorEHandle = inflictorEHandle == -1 ? attacker.GetEncodedEHandle() : inflictorEHandle - file.roundWinningKillReplayTimeOfDeath = Time() -} - -void function SetWinner( int team, string winningReason = "", string losingReason = "" ) -{ - SetServerVar( "winningTeam", team ) - - file.gameWonThisFrame = true - thread UpdateGameWonThisFrameNextFrame() - - if ( winningReason.len() == 0 ) - file.announceRoundWinnerWinningSubstr = 0 - else - file.announceRoundWinnerWinningSubstr = GetStringID( winningReason ) - - if ( losingReason.len() == 0 ) - file.announceRoundWinnerLosingSubstr = 0 - else - file.announceRoundWinnerLosingSubstr = GetStringID( losingReason ) - - if ( GamePlayingOrSuddenDeath() ) - { - if ( IsRoundBased() ) - { - if ( team != TEAM_UNASSIGNED ) - { - GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + 1 ) - GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + 1 ) - } - - SetGameState( eGameState.WinnerDetermined ) - } - else - SetGameState( eGameState.WinnerDetermined ) - - ScoreEvent_MatchComplete( team ) - } -} - void function UpdateGameWonThisFrameNextFrame() { WaitFrame() @@ -840,29 +1117,6 @@ void function UpdateGameWonThisFrameNextFrame() file.hasKillForGameWonThisFrame = false } -void function AddTeamScore( int team, int amount ) -{ - GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + amount ) - GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + amount ) - - int scoreLimit - if ( IsRoundBased() ) - scoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE ) - else - scoreLimit = GameMode_GetScoreLimit( GAMETYPE ) - - int score = GameRules_GetTeamScore( team ) - if ( score >= scoreLimit || GetGameState() == eGameState.SuddenDeath ) - SetWinner( team ) - else if ( ( file.switchSidesBased && !file.hasSwitchedSides ) && score >= ( scoreLimit.tofloat() / 2.0 ) ) - SetGameState( eGameState.SwitchingSides ) -} - -void function SetTimeoutWinnerDecisionFunc( int functionref() callback ) -{ - file.timeoutWinnerDecisionFunc = callback -} - int function GetWinningTeamWithFFASupport() { if ( !IsFFAGame() ) @@ -892,8 +1146,6 @@ int function GetWinningTeamWithFFASupport() unreachable } -// idk - float function GameState_GetTimeLimitOverride() { return 100 @@ -923,8 +1175,6 @@ float function GetTimeLimit_ForGameMode() return GetCurrentPlaylistVarFloat( playlistString, 10 ) } -// faction dialogue - void function DialoguePlayNormal() { int totalScore = GameMode_GetScoreLimit( GameRules_GetGameMode() ) @@ -1008,4 +1258,4 @@ void function DialoguePlayWinnerDetermined() PlayFactionDialogueToTeam( "scoring_won", winningTeam ) PlayFactionDialogueToTeam( "scoring_lost", losingTeam ) } -} +}
\ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut index be20982d..2a4c4282 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut @@ -8,6 +8,7 @@ global function ScoreEvent_TitanDoomed global function ScoreEvent_TitanKilled global function ScoreEvent_NPCKilled global function ScoreEvent_MatchComplete +global function ScoreEvent_RoundComplete global function ScoreEvent_SetEarnMeterValues global function ScoreEvent_SetupEarnMeterValuesForMixedModes @@ -287,8 +288,24 @@ void function ScoreEvent_MatchComplete( int winningTeam ) foreach( entity player in GetPlayerArray() ) { AddPlayerScore( player, "MatchComplete" ) + SetPlayerChallengeMatchComplete( player ) if ( player.GetTeam() == winningTeam ) + { AddPlayerScore( player, "MatchVictory" ) + SetPlayerChallengeMatchWon( player, true ) + } + else + SetPlayerChallengeMatchWon( player, false ) + } +} + +void function ScoreEvent_RoundComplete( int winningTeam ) +{ + foreach( entity player in GetPlayerArray() ) + { + AddPlayerScore( player, "RoundComplete" ) + if ( player.GetTeam() == winningTeam ) + AddPlayerScore( player, "RoundVictory" ) } } @@ -304,7 +321,7 @@ void function ScoreEvent_SetupEarnMeterValuesForMixedModes() // mixed modes in t { // todo needs earn/overdrive values // player-controlled stuff - ScoreEvent_SetEarnMeterValues( "KillPilot", 0.07, 0.15 ) + ScoreEvent_SetEarnMeterValues( "KillPilot", 0.07, 0.15, 0.33 ) // 5% for titan cores ScoreEvent_SetEarnMeterValues( "KillTitan", 0.0, 0.15 ) ScoreEvent_SetEarnMeterValues( "TitanKillTitan", 0.0, 0.0 ) // unsure ScoreEvent_SetEarnMeterValues( "PilotBatteryStolen", 0.0, 0.35 ) // this actually just doesn't have overdrive in vanilla even diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut index bd64e4ca..74a9088b 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut @@ -334,6 +334,10 @@ void function OnPlayerOrNPCKilled( entity victim, entity attacker, var damageInf thread SetLastPosForDistanceStatValid_Threaded( victim, false ) HandleDeathStats( victim, attacker, damageInfo ) + + if( victim == attacker ) //Suicides are registering stats, afaik vanilla ignores them + return + HandleKillStats( victim, attacker, damageInfo ) HandleWeaponKillStats( victim, attacker, damageInfo ) HandleTitanStats( victim, attacker, damageInfo ) @@ -489,23 +493,32 @@ void function HandleKillStats( entity victim, entity attacker, var damageInfo ) // get the player and it's pet titan entity player entity playerPetTitan - if ( attacker.IsPlayer() ) + entity inflictor = DamageInfo_GetInflictor( damageInfo ) + + if ( IsValid( inflictor ) ) { - // the player is just the attacker - player = attacker - playerPetTitan = player.GetPetTitan() + if ( inflictor.IsProjectile() && IsValid( inflictor.GetOwner() ) ) // Attackers are always the final entity in the owning hierarchy, projectile owners though migh be a player's NPC minion (i.e Auto-Titans) + attacker = inflictor.GetOwner() + + else if ( inflictor.IsNPC() ) // NPCs are bypassed as Attackers if they are owned by players, instead they become just inflictors + attacker = inflictor } - else if ( attacker.IsTitan() && IsPetTitan( attacker ) ) + + if ( attacker.IsNPC() ) { - // the attacker is the player's auto titan + if ( !attacker.IsTitan() ) // Normal NPCs case + return + + if ( !IsPetTitan( attacker ) ) // NPC Titans case + return + player = attacker.GetTitanSoul().GetBossPlayer() playerPetTitan = attacker } + else if ( attacker.IsPlayer() ) // Still checks this because worldspawn might be the attacker + player = attacker else - { - // attacker could be something like an NPC, or worldspawn return - } // check things once, for performance int damageSource = DamageInfo_GetDamageSourceIdentifier( damageInfo ) @@ -931,6 +944,9 @@ void function HandleDistanceAndTimeStats_Threaded() // track distance stats foreach ( entity player in GetPlayerArray() ) { + if ( !IsValid( player ) ) + continue + if ( player.p.lastPosForDistanceStatValid ) { // not 100% sure on using Distance2D over Distance tbh @@ -1035,7 +1051,10 @@ void function SaveStatsPeriodically_Threaded() while( true ) { foreach( entity player in GetPlayerArray() ) - Stats_SaveAllStats( player ) + { + if ( IsValid( player ) ) + Stats_SaveAllStats( player ) + } wait 5 } } 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 d0ce4f79..49720bf4 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut @@ -236,6 +236,8 @@ void function OnPrematchStart() void function PlayerWatchesWargamesIntro( entity player ) { + player.EndSignal( "OnDestroy" ) + if ( IsAlive( player ) ) player.Die() @@ -258,8 +260,6 @@ void function PlayerWatchesWargamesIntro( entity player ) // we need to wait a frame if we killed ourselves to spawn into this, so just easier to do it all the time to remove any weirdness WaitFrame() - - player.EndSignal( "OnDestroy" ) player.EndSignal( "OnDeath" ) int factionTeam = ConvertPlayerFactionToIMCOrMilitiaTeam( player ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut index 4956375b..c47552b3 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut @@ -1,27 +1,30 @@ untyped -global function InitRatings // temp for testing - global function Spawn_Init -global function SetRespawnsEnabled -global function RespawnsEnabled +global function FindSpawnPoint + global function SetSpawnpointGamemodeOverride global function GetSpawnpointGamemodeOverride global function AddSpawnpointValidationRule + +global function SetRespawnsEnabled +global function RespawnsEnabled global function CreateNoSpawnArea global function DeleteNoSpawnArea - -global function FindSpawnPoint +global function SpawnPointInNoSpawnArea global function RateSpawnpoints_Generic global function RateSpawnpoints_Frontline - -global function SetSpawnZoneRatingFunc -global function SetShouldCreateMinimapSpawnZones -global function CreateTeamSpawnZoneEntity global function RateSpawnpoints_SpawnZones global function DecideSpawnZone_Generic -global function DecideSpawnZone_CTF + +global struct spawnZoneProperties{ + int controllingTeam = TEAM_UNASSIGNED + entity minimapEnt = null + float zoneRating = 0.0 +} + +global table< entity, spawnZoneProperties > mapSpawnZones // Global so other scripts can access this for custom ratings if needed struct NoSpawnArea { @@ -35,30 +38,61 @@ struct NoSpawnArea struct { bool respawnsEnabled = true + array<NoSpawnArea> noSpawnAreas string spawnpointGamemodeOverride array< bool functionref( entity, int ) > customSpawnpointValidationRules - - table<string, NoSpawnArea> noSpawnAreas + bool shouldCreateMinimapSpawnzones } file + + + + + + + + + + +/* +██████ █████ ███████ ███████ ███████ ██ ██ ███ ██ ██████ ████████ ██ ██████ ███ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ████ ██ ██ +██████ ███████ ███████ █████ █████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██████ ██ ██ ███████ ███████ ██ ██████ ██ ████ ██████ ██ ██ ██████ ██ ████ ███████ +*/ + void function Spawn_Init() -{ +{ + // callbacks for generic spawns AddSpawnCallback( "info_spawnpoint_human", InitSpawnpoint ) - AddSpawnCallback( "info_spawnpoint_human_start", InitSpawnpoint ) AddSpawnCallback( "info_spawnpoint_titan", InitSpawnpoint ) + AddSpawnCallback( "info_spawnpoint_droppod", InitSpawnpoint ) + AddSpawnCallback( "info_spawnpoint_dropship", InitSpawnpoint ) + AddSpawnCallback( "info_spawnpoint_human_start", InitSpawnpoint ) AddSpawnCallback( "info_spawnpoint_titan_start", InitSpawnpoint ) - - // callbacks for generic spawns - AddCallback_EntitiesDidLoad( InitPreferSpawnNodes ) + AddSpawnCallback( "info_spawnpoint_droppod_start", InitSpawnpoint ) + AddSpawnCallback( "info_spawnpoint_dropship_start", InitSpawnpoint ) // callbacks for spawnzone spawns AddCallback_GameStateEnter( eGameState.Prematch, ResetSpawnzones ) AddSpawnCallbackEditorClass( "trigger_multiple", "trigger_mp_spawn_zone", AddSpawnZoneTrigger ) -} - -void function InitSpawnpoint( entity spawnpoint ) -{ - spawnpoint.s.lastUsedTime <- -999 + + float friendlyAIValue = 1.75 + if ( GameModeHasCapturePoints() ) + friendlyAIValue = 0.75 + + SpawnPoints_SetRatingMultipliers_Enemy( TD_TITAN, -10.0, -6.0, -1.0 ) + SpawnPoints_SetRatingMultipliers_Enemy( TD_PILOT, -10.0, -6.0, -1.0 ) + SpawnPoints_SetRatingMultipliers_Enemy( TD_AI, -2.0, -0.25, 0.0 ) + + SpawnPoints_SetRatingMultipliers_Friendly( TD_TITAN, 0.25, 1.75, friendlyAIValue ) + SpawnPoints_SetRatingMultipliers_Friendly( TD_PILOT, 0.25, 1.75, friendlyAIValue ) + SpawnPoints_SetRatingMultipliers_Friendly( TD_AI, 0.5, 0.25, 0.0 ) + + SpawnPoints_SetRatingMultiplier_PetTitan( 2.0 ) + + file.shouldCreateMinimapSpawnzones = GetCurrentPlaylistVarInt( "spawn_zone_enabled", 1 ) != 0 } void function SetRespawnsEnabled( bool enabled ) @@ -71,9 +105,10 @@ bool function RespawnsEnabled() return file.respawnsEnabled } -void function AddSpawnpointValidationRule( bool functionref( entity spawn, int team ) rule ) +void function InitSpawnpoint( entity spawnpoint ) { - file.customSpawnpointValidationRules.append( rule ) + spawnpoint.s.lastUsedTime <- -999 + spawnpoint.s.inUse <- false } string function CreateNoSpawnArea( int blockSpecificTeam, int blockEnemiesOfTeam, vector position, float lifetime, float radius ) @@ -85,11 +120,12 @@ string function CreateNoSpawnArea( int blockSpecificTeam, int blockEnemiesOfTeam noSpawnArea.lifetime = lifetime noSpawnArea.radius = radius - // generate an id noSpawnArea.id = UniqueString( "noSpawnArea" ) - thread NoSpawnAreaLifetime( noSpawnArea ) + if ( lifetime > 0 ) + thread NoSpawnAreaLifetime( noSpawnArea ) + file.noSpawnAreas.append( noSpawnArea ) return noSpawnArea.id } @@ -101,8 +137,41 @@ void function NoSpawnAreaLifetime( NoSpawnArea noSpawnArea ) void function DeleteNoSpawnArea( string noSpawnIdx ) { - if ( noSpawnIdx in file.noSpawnAreas ) - delete file.noSpawnAreas[ noSpawnIdx ] + foreach ( noSpawnArea in file.noSpawnAreas ) + { + if ( noSpawnArea.id == noSpawnIdx ) + file.noSpawnAreas.removebyvalue( noSpawnArea ) + } +} + +bool function SpawnPointInNoSpawnArea( vector vec, int team ) +{ + foreach ( noSpawnArea in file.noSpawnAreas ) + { + if ( Distance( noSpawnArea.position, vec ) < noSpawnArea.radius ) + { + if ( noSpawnArea.blockedTeam != TEAM_INVALID && noSpawnArea.blockedTeam == team ) + return true + + if ( noSpawnArea.blockOtherTeams != TEAM_INVALID && noSpawnArea.blockOtherTeams != team ) + return true + } + } + + return false +} + +bool function IsSpawnpointValidDrop( entity spawnpoint, int team ) +{ + if ( spawnpoint.IsOccupied() || spawnpoint.s.inUse ) + return false + + return true +} + +void function AddSpawnpointValidationRule( bool functionref( entity spawn, int team ) rule ) +{ + file.customSpawnpointValidationRules.append( rule ) } void function SetSpawnpointGamemodeOverride( string gamemode ) @@ -114,35 +183,38 @@ string function GetSpawnpointGamemodeOverride() { if ( file.spawnpointGamemodeOverride != "" ) return file.spawnpointGamemodeOverride - else - return GAMETYPE - unreachable + return GAMETYPE } -void function InitRatings( entity player, int team ) -{ - if ( player != null ) - SpawnPoints_InitRatings( player, team ) // no idea what the second arg supposed to be lol -} + + + + + + + + + +/* +███████ ██████ █████ ██ ██ ███ ██ ██████ ██████ ██████ ███████ ██████ ██ ███ ██ ██████ +██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ +███████ ██████ ███████ ██ █ ██ ██ ██ ██ ██ ██ ██████ ██ ██ █████ ██████ ██ ██ ██ ██ ██ ███ + ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +███████ ██ ██ ██ ███ ███ ██ ████ ██████ ██ ██ ██████ ███████ ██ ██ ██ ██ ████ ██████ +*/ entity function FindSpawnPoint( entity player, bool isTitan, bool useStartSpawnpoint ) { int team = player.GetTeam() - if ( HasSwitchedSides() ) - team = GetOtherTeam( team ) - + array<entity> spawnpoints if ( useStartSpawnpoint ) spawnpoints = isTitan ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetPilotStart( team ) else spawnpoints = isTitan ? SpawnPoints_GetTitan() : SpawnPoints_GetPilot() - InitRatings( player, player.GetTeam() ) - - // don't think this is necessary since we call discardratings - //foreach ( entity spawnpoint in spawnpoints ) - // spawnpoint.CalculateRating( isTitan ? TD_TITAN : TD_PILOT, team, 0.0, 0.0 ) + SpawnPoints_InitRatings( player, team ) void functionref( int, array<entity>, int, entity ) ratingFunc = isTitan ? GameMode_GetTitanSpawnpointsRatingFunc( GAMETYPE ) : GameMode_GetPilotSpawnpointsRatingFunc( GAMETYPE ) ratingFunc( isTitan ? TD_TITAN : TD_PILOT, spawnpoints, team, player ) @@ -166,46 +238,46 @@ entity function FindSpawnPoint( entity player, bool isTitan, bool useStartSpawnp spawnpoints = useStartSpawnpoint ? SpawnPoints_GetPilotStart( team ) : SpawnPoints_GetPilot() } - entity spawnpoint = GetBestSpawnpoint( player, spawnpoints ) + entity spawnpoint = GetBestSpawnpoint( player, spawnpoints, isTitan ) spawnpoint.s.lastUsedTime = Time() player.SetLastSpawnPoint( spawnpoint ) + + //SpawnPoints_DiscardRatings() return spawnpoint } -entity function GetBestSpawnpoint( entity player, array<entity> spawnpoints ) +entity function GetBestSpawnpoint( entity player, array<entity> spawnpoints, bool isTitan ) { - // not really 100% sure on this randomisation, needs some thought array<entity> validSpawns + + // I know this looks hacky but the native funcs to get the spawns is returning null arrays for FFA idk why. + if ( IsFFAGame() ) + { + spawnpoints.clear() + if ( isTitan ) + spawnpoints = GetEntArrayByClass_Expensive( "info_spawnpoint_titan" ) + else + spawnpoints = GetEntArrayByClass_Expensive( "info_spawnpoint_human" ) + } + foreach ( entity spawnpoint in spawnpoints ) { if ( IsSpawnpointValid( spawnpoint, player.GetTeam() ) ) - { validSpawns.append( spawnpoint ) - - if ( validSpawns.len() == 3 ) // arbitrary small sample size - break - } } - if ( validSpawns.len() == 0 ) + if ( !validSpawns.len() ) // First validity check { - // no valid spawns, very bad, so dont care about spawns being valid anymore - print( "found no valid spawns! spawns may be subpar!" ) + CodeWarning( "Map has no valid spawn points for " + GAMETYPE + " gamemode, attempting any other possible spawn point" ) foreach ( entity spawnpoint in spawnpoints ) - { validSpawns.append( spawnpoint ) - - if ( validSpawns.len() == 3 ) // arbitrary small sample size - break - } } - // last resort - if ( validSpawns.len() == 0 ) + if ( !validSpawns.len() ) // On all validity check, just gather the most basic spawn { - print( "map has literally 0 spawnpoints, as such everything is fucked probably, attempting to use info_player_start if present" ) + CodeWarning( "Map has no proper spawn points, falling back to info_player_start" ) entity start = GetEnt( "info_player_start" ) if ( IsValid( start ) ) @@ -213,14 +285,19 @@ entity function GetBestSpawnpoint( entity player, array<entity> spawnpoints ) start.s.lastUsedTime <- -999 validSpawns.append( start ) } + else + throw( "Map has no player spawns at all" ) } - return validSpawns[ RandomInt( validSpawns.len() ) ] // slightly randomize it + if ( IsFFAGame() ) + return validSpawns.getrandom() + + return validSpawns[0] // Return first entry in the array because native have already sorted everything through the ratings, so first one is the best one } bool function IsSpawnpointValid( entity spawnpoint, int team ) { - if ( !spawnpoint.HasKey( "ignoreGamemode" ) || ( spawnpoint.HasKey( "ignoreGamemode" ) && spawnpoint.kv.ignoreGamemode == "0" ) ) // used by script-spawned spawnpoints + if ( !spawnpoint.HasKey( "ignoreGamemode" ) || spawnpoint.HasKey( "ignoreGamemode" ) && spawnpoint.kv.ignoreGamemode == "0" ) // used by script-spawned spawnpoints { if ( file.spawnpointGamemodeOverride != "" ) { @@ -232,223 +309,149 @@ bool function IsSpawnpointValid( entity spawnpoint, int team ) return false } - int compareTeam = spawnpoint.GetTeam() - if ( HasSwitchedSides() && ( compareTeam == TEAM_MILITIA || compareTeam == TEAM_IMC ) ) - compareTeam = GetOtherTeam( compareTeam ) - foreach ( bool functionref( entity, int ) customValidationRule in file.customSpawnpointValidationRules ) if ( !customValidationRule( spawnpoint, team ) ) return false - if ( spawnpoint.GetTeam() > 0 && compareTeam != team && !IsFFAGame() ) + if ( !IsSpawnpointValidDrop( spawnpoint, team ) || Time() - spawnpoint.s.lastUsedTime <= 10.0 ) return false - if ( spawnpoint.IsOccupied() ) + if ( SpawnPointInNoSpawnArea( spawnpoint.GetOrigin(), team ) ) return false - - if ( Time() - spawnpoint.s.lastUsedTime <= 10.0 ) - return false - - foreach ( k, NoSpawnArea noSpawnArea in file.noSpawnAreas ) + + // Line of Sight Check, could use IsVisibleToEnemies but apparently that considers only players, not NPCs + array< entity > enemyTitans = GetTitanArrayOfEnemies( team ) + if ( GetConVarBool( "spawnpoint_avoid_npc_titan_sight" ) ) { - if ( Distance( noSpawnArea.position, spawnpoint.GetOrigin() ) > noSpawnArea.radius ) - continue - - if ( noSpawnArea.blockedTeam != TEAM_INVALID && noSpawnArea.blockedTeam == team ) - return false - - if ( noSpawnArea.blockOtherTeams != TEAM_INVALID && noSpawnArea.blockOtherTeams != team ) - return false + foreach ( titan in enemyTitans ) + { + if ( IsAlive( titan ) && titan.IsNPC() && titan.CanSee( spawnpoint ) ) + return false + } } - - const minEnemyDist = 1000.0 // about 20 meters? - // in rsquirrel extend returns null unlike in vanilla squirrel - array< entity > spawnBlockers = GetPlayerArrayEx( "any", TEAM_ANY, TEAM_ANY, spawnpoint.GetOrigin(), minEnemyDist ) - spawnBlockers.extend( GetProjectileArrayEx( "any", TEAM_ANY, TEAM_ANY, spawnpoint.GetOrigin(), minEnemyDist ) ) - foreach ( entity blocker in spawnBlockers ) - if ( blocker.GetTeam() != team ) - return false - // los check return !spawnpoint.IsVisibleToEnemies( team ) } -// SPAWNPOINT RATING FUNCS BELOW -// generic -struct { - array<vector> preferSpawnNodes -} spawnStateGeneric -void function RateSpawnpoints_Generic( int checkClass, array<entity> spawnpoints, int team, entity player ) -{ - if ( !IsFFAGame() ) - { - // use frontline spawns in 2-team modes - RateSpawnpoints_Frontline( checkClass, spawnpoints, team, player ) - return - } - else - { - // todo: ffa spawns :terror: - } - // old algo: keeping until we have a better ffa spawn algo - // i'm not a fan of this func, but i really don't have a better way to do this rn, and it's surprisingly good with los checks implemented now - - // calculate ratings for preferred nodes - // this tries to prefer nodes with more teammates, then activity on them - // todo: in the future it might be good to have this prefer nodes with enemies up to a limit of some sort - // especially in ffa modes i could deffo see this falling apart a bit rn - // perhaps dead players could be used to calculate some sort of activity rating? so high-activity points with an even balance of friendly/unfriendly players are preferred - array<float> preferSpawnNodeRatings - foreach ( vector preferSpawnNode in spawnStateGeneric.preferSpawnNodes ) + + + + +/* +██████ ██████ ██ ███ ██ ████████ ██████ █████ ████████ ██ ███ ██ ██████ +██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ +██████ ██ ██ ██ ██ ██ ██ ██ ██████ ███████ ██ ██ ██ ██ ██ ██ ███ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +██ ██████ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██████ +*/ + +void function RateSpawnpoints_Generic( int checkClass, array<entity> spawnpoints, int team, entity player ) +{ + foreach ( entity spawnpoint in spawnpoints ) { - float currentRating - - // this seems weird, not using rn - //Frontline currentFrontline = GetCurrentFrontline( team ) - //if ( !IsFFAGame() || currentFrontline.friendlyCenter != < 0, 0, 0 > ) - // currentRating += max( 0.0, ( 1000.0 - Distance2D( currentFrontline.origin, preferSpawnNode ) ) / 200 ) + float currentRating = 0.0 - foreach ( entity nodePlayer in GetPlayerArray() ) - { - float currentChange = 0.0 - - // the closer a player is to a node the more they matter - float dist = Distance2D( preferSpawnNode, nodePlayer.GetOrigin() ) - if ( dist > 600.0 ) - continue - - currentChange = ( 600.0 - dist ) / 5 - if ( player == nodePlayer ) - currentChange *= -3 // always try to stay away from places we've already spawned - else if ( !IsAlive( nodePlayer ) ) // dead players mean activity which is good, but they're also dead so they don't matter as much as living ones - currentChange *= 0.6 - if ( nodePlayer.GetTeam() != player.GetTeam() ) // if someone isn't on our team and alive they're probably bad - { - if ( IsFFAGame() ) // in ffa everyone is on different teams, so this isn't such a big deal - currentChange *= -0.2 - else - currentChange *= -0.6 - } - - currentRating += currentChange - } + // Gather friendly scoring first to give positive rating first + currentRating += spawnpoint.NearbyAllyScore( team, "ai" ) + currentRating += spawnpoint.NearbyAllyScore( team, "titan" ) + currentRating += spawnpoint.NearbyAllyScore( team, "pilot" ) - preferSpawnNodeRatings.append( currentRating ) - } - - foreach ( entity spawnpoint in spawnpoints ) - { - float currentRating - float petTitanModifier - // scale how much a given spawnpoint matters to us based on how far it is from each node - bool spawnHasRecievedInitialBonus = false - for ( int i = 0; i < spawnStateGeneric.preferSpawnNodes.len(); i++ ) - { - // bonus if autotitan is nearish - if ( IsAlive( player.GetPetTitan() ) && Distance( player.GetPetTitan().GetOrigin(), spawnStateGeneric.preferSpawnNodes[ i ] ) < 1200.0 ) - petTitanModifier += 10.0 - - float dist = Distance2D( spawnpoint.GetOrigin(), spawnStateGeneric.preferSpawnNodes[ i ] ) - if ( dist > 750.0 ) - continue - - if ( dist < 600.0 && !spawnHasRecievedInitialBonus ) - { - currentRating += 10.0 - spawnHasRecievedInitialBonus = true // should only get a bonus for simply being by a node once to avoid over-rating - } + // Enemies then subtract that rating ( Values already returns negative, so no need to apply subtract again ) + currentRating += spawnpoint.NearbyEnemyScore( team, "ai" ) + currentRating += spawnpoint.NearbyEnemyScore( team, "titan" ) + currentRating += spawnpoint.NearbyEnemyScore( team, "pilot" ) - currentRating += ( preferSpawnNodeRatings[ i ] * ( ( 750.0 - dist ) / 75 ) ) + max( RandomFloat( 1.25 ), 0.9 ) - if ( dist < 250.0 ) // shouldn't get TOO close to an active node - currentRating *= 0.7 - - if ( spawnpoint.s.lastUsedTime < 10.0 ) - currentRating *= 0.7 - } - - float rating = spawnpoint.CalculateRating( checkClass, team, currentRating, currentRating + petTitanModifier ) - //print( "spawnpoint at " + spawnpoint.GetOrigin() + " has rating: " + ) + if ( spawnpoint == player.p.lastSpawnPoint ) // Reduce the rating of the spawn point used previously + currentRating += GetConVarFloat( "spawnpoint_last_spawn_rating" ) - if ( rating != 0.0 || currentRating != 0.0 ) - print( "rating = " + rating + ", internal rating = " + currentRating ) - } -} - -void function InitPreferSpawnNodes() -{ - foreach ( entity hardpoint in GetEntArrayByClass_Expensive( "info_hardpoint" ) ) - { - if ( !hardpoint.HasKey( "hardpointGroup" ) ) - continue - - if ( hardpoint.kv.hardpointGroup != "A" && hardpoint.kv.hardpointGroup != "B" && hardpoint.kv.hardpointGroup != "C" ) - continue - - spawnStateGeneric.preferSpawnNodes.append( hardpoint.GetOrigin() ) + spawnpoint.CalculateRating( checkClass, team, currentRating, currentRating * 0.25 ) } - - //foreach ( entity frontline in GetEntArrayByClass_Expensive( "info_frontline" ) ) - // spawnStateGeneric.preferSpawnNodes.append( frontline.GetOrigin() ) } -// frontline void function RateSpawnpoints_Frontline( int checkClass, array<entity> spawnpoints, int team, entity player ) { + Frontline currentFrontline = GetFrontline( team ) + + vector inverseFrontlineDir = currentFrontline.combatDir * -1 + vector adjustedPosition = currentFrontline.origin + currentFrontline.combatDir * 8000 + + SpawnPoints_InitFrontlineData( adjustedPosition, currentFrontline.combatDir, currentFrontline.origin, currentFrontline.friendlyCenter, 4000 ) + foreach ( entity spawnpoint in spawnpoints ) { - float rating = spawnpoint.CalculateFrontlineRating() - spawnpoint.CalculateRating( checkClass, player.GetTeam(), rating, rating > 0 ? rating * 0.25 : rating ) + float frontlineRating = spawnpoint.CalculateFrontlineRating() + + spawnpoint.CalculateRating( checkClass, team, frontlineRating, frontlineRating * 0.25 ) } } -// spawnzones -struct { - array<entity> mapSpawnzoneTriggers - entity functionref( array<entity>, int ) spawnzoneRatingFunc - bool shouldCreateMinimapSpawnzones = false - - // for DecideSpawnZone_Generic - table<int, entity> activeTeamSpawnzones - table<int, entity> activeTeamSpawnzoneMinimapEnts -} spawnStateSpawnzones + + + + + + + + + +/* +███████ ██████ █████ ██ ██ ███ ██ ███████ ██████ ███ ██ ███████ ███████ +██ ██ ██ ██ ██ ██ ██ ████ ██ ███ ██ ██ ████ ██ ██ ██ +███████ ██████ ███████ ██ █ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ █████ ███████ + ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ +███████ ██ ██ ██ ███ ███ ██ ████ ███████ ██████ ██ ████ ███████ ███████ +*/ void function ResetSpawnzones() { - spawnStateSpawnzones.activeTeamSpawnzones.clear() - - foreach ( int team, entity minimapEnt in spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts ) - if ( IsValid( minimapEnt ) ) - minimapEnt.Destroy() - - spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts.clear() + foreach ( zone, zoneProperties in mapSpawnZones ) + { + if ( IsValid( zoneProperties.minimapEnt ) ) + zoneProperties.minimapEnt.Destroy() + + zoneProperties.controllingTeam = TEAM_UNASSIGNED + zoneProperties.zoneRating = 0.0 + } } void function AddSpawnZoneTrigger( entity trigger ) { - trigger.s.spawnzoneRating <- 0.0 - spawnStateSpawnzones.mapSpawnzoneTriggers.append( trigger ) + spawnZoneProperties zoneProperties + mapSpawnZones[trigger] <- zoneProperties } -void function SetSpawnZoneRatingFunc( entity functionref( array<entity>, int ) ratingFunc ) +bool function TeamHasDirtySpawnzone( int team ) { - spawnStateSpawnzones.spawnzoneRatingFunc = ratingFunc -} - -void function SetShouldCreateMinimapSpawnZones( bool shouldCreateMinimapSpawnzones ) -{ - spawnStateSpawnzones.shouldCreateMinimapSpawnzones = shouldCreateMinimapSpawnzones + foreach ( zone, zoneProperties in mapSpawnZones ) + { + if ( zoneProperties.controllingTeam == team ) + { + int numDeadInZone = 0 + array<entity> teamPlayers = GetPlayerArrayOfTeam( team ) + foreach ( entity player in teamPlayers ) + { + if ( Time() - player.p.postDeathThreadStartTime < 20.0 && zone.ContainsPoint( player.p.deathOrigin ) ) + numDeadInZone++ + } + + if ( numDeadInZone < teamPlayers.len() ) + return false + } + } + + return true } -entity function CreateTeamSpawnZoneEntity( entity spawnzone, int team ) +void function CreateTeamSpawnZoneEntity( entity spawnzone, int team ) { entity minimapObj = CreatePropScript( $"models/dev/empty_model.mdl", spawnzone.GetOrigin() ) SetTeam( minimapObj, team ) - minimapObj.Minimap_SetObjectScale( 100.0 / Distance2D( < 0, 0, 0 >, spawnzone.GetBoundingMaxs() ) ) + minimapObj.Minimap_SetObjectScale( Distance2D( < 0, 0, 0 >, spawnzone.GetBoundingMaxs() ) / 16000 ) // 16000 cuz thats the total space Minimap uses minimapObj.Minimap_SetAlignUpright( true ) minimapObj.Minimap_AlwaysShow( TEAM_IMC, null ) minimapObj.Minimap_AlwaysShow( TEAM_MILITIA, null ) @@ -461,67 +464,58 @@ entity function CreateTeamSpawnZoneEntity( entity spawnzone, int team ) minimapObj.Minimap_SetCustomState( eMinimapObject_prop_script.SPAWNZONE_MIL ) minimapObj.DisableHibernation() - return minimapObj + mapSpawnZones[spawnzone].minimapEnt = minimapObj } void function RateSpawnpoints_SpawnZones( int checkClass, array<entity> spawnpoints, int team, entity player ) { - if ( spawnStateSpawnzones.spawnzoneRatingFunc == null ) - spawnStateSpawnzones.spawnzoneRatingFunc = DecideSpawnZone_Generic - - // don't use spawnzones if we're using start spawns if ( ShouldStartSpawn( player ) ) { RateSpawnpoints_Generic( checkClass, spawnpoints, team, player ) return } - - entity spawnzone = spawnStateSpawnzones.spawnzoneRatingFunc( spawnStateSpawnzones.mapSpawnzoneTriggers, player.GetTeam() ) - if ( !IsValid( spawnzone ) ) // no spawn zone, use generic algo + + array< entity > zoneTriggers + foreach ( zone, zoneProperties in mapSpawnZones ) + zoneTriggers.append( zone ) + + entity spawnzone = DecideSpawnZone_Generic( zoneTriggers, player.GetTeam() ) + if ( !IsValid( spawnzone ) ) { RateSpawnpoints_Generic( checkClass, spawnpoints, team, player ) return } - // rate spawnpoints foreach ( entity spawn in spawnpoints ) { float rating = 0.0 float distance = Distance2D( spawn.GetOrigin(), spawnzone.GetOrigin() ) if ( distance < Distance2D( < 0, 0, 0 >, spawnzone.GetBoundingMaxs() ) ) - rating = 100.0 - else // max 35 rating if not in zone, rate by closest - rating = 35.0 * ( 1 - ( distance / 5000.0 ) ) + rating = 10.0 + else + rating = 2.0 * ( 1 - ( distance / 3000.0 ) ) - spawn.CalculateRating( checkClass, player.GetTeam(), rating, rating ) + spawn.CalculateRating( checkClass, team, rating, rating * 0.25 ) } } entity function DecideSpawnZone_Generic( array<entity> spawnzones, int team ) { - if ( spawnzones.len() == 0 ) + if ( !spawnzones.len() ) return null - // get average team startspawn positions - int spawnCompareTeam = team - if ( HasSwitchedSides() ) - spawnCompareTeam = GetOtherTeam( team ) - - array<entity> startSpawns = SpawnPoints_GetPilotStart( spawnCompareTeam ) - array<entity> enemyStartSpawns = SpawnPoints_GetPilotStart( GetOtherTeam( spawnCompareTeam ) ) + 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 + if ( !startSpawns.len() || !enemyStartSpawns.len() ) return null - - // get average startspawn position and max dist between spawns - // could probably cache this, tbh, not like it should change outside of halftimes - vector averageFriendlySpawns + + vector averageFriendlySpawns foreach ( entity spawn in startSpawns ) averageFriendlySpawns += spawn.GetOrigin() averageFriendlySpawns /= startSpawns.len() - // get average enemy startspawn position vector averageEnemySpawns foreach ( entity spawn in enemyStartSpawns ) averageEnemySpawns += spawn.GetOrigin() @@ -530,250 +524,87 @@ entity function DecideSpawnZone_Generic( array<entity> spawnzones, int team ) float baseDistance = Distance2D( averageFriendlySpawns, averageEnemySpawns ) - bool needNewZone = true - if ( team in spawnStateSpawnzones.activeTeamSpawnzones ) - { - foreach ( entity player in GetPlayerArray() ) - { - // couldn't get IsTouching, GetTouchingEntities or enter callbacks to work in testing, so doing this - if ( player.GetTeam() != team && spawnStateSpawnzones.activeTeamSpawnzones[ team ].ContainsPoint( player.GetOrigin() ) ) - break - } - - int numDeadInZone = 0 - array<entity> teamPlayers = GetPlayerArrayOfTeam( team ) - foreach ( entity player in teamPlayers ) - { - // check if they died in the zone recently, get a new zone if too many died - if ( Time() - player.p.postDeathThreadStartTime < 15.0 && spawnStateSpawnzones.activeTeamSpawnzones[ team ].ContainsPoint( player.p.deathOrigin ) ) - numDeadInZone++ - } - - // cast to float so result is float - if ( float( numDeadInZone ) / teamPlayers.len() <= 0.1 ) - needNewZone = false - } - - if ( needNewZone ) + if ( TeamHasDirtySpawnzone( team ) ) { - // find new zone array<entity> possibleZones - foreach ( entity spawnzone in spawnStateSpawnzones.mapSpawnzoneTriggers ) + foreach ( zone, zoneProperties in mapSpawnZones ) { - // don't remember if you can do a "value in table.values" sorta thing in squirrel so doing manual lookup - bool spawnzoneTaken = false - foreach ( int otherTeam, entity otherSpawnzone in spawnStateSpawnzones.activeTeamSpawnzones ) - { - if ( otherSpawnzone == spawnzone ) - { - spawnzoneTaken = true - break - } - } - - if ( spawnzoneTaken ) + if ( zoneProperties.controllingTeam == GetOtherTeam( team ) ) continue - // check zone validity - bool spawnzoneEvil = false - foreach ( entity player in GetPlayerArray() ) + bool spawnzoneHasEnemies = false + foreach ( entity enemy in GetPlayerArrayOfEnemies_Alive( team ) ) { - // couldn't get IsTouching, GetTouchingEntities or enter callbacks to work in testing, so doing this - if ( player.GetTeam() != team && spawnzone.ContainsPoint( player.GetOrigin() ) ) + if ( zone.ContainsPoint( enemy.GetOrigin() ) ) { - spawnzoneEvil = true + spawnzoneHasEnemies = true break } } - // don't choose spawnzones that are closer to enemy base than friendly base - // note: vanilla spawns might not necessarily require this, worth checking - if ( !spawnzoneEvil && Distance2D( spawnzone.GetOrigin(), averageFriendlySpawns ) > Distance2D( spawnzone.GetOrigin(), averageEnemySpawns ) ) - spawnzoneEvil = true + if ( !spawnzoneHasEnemies && Distance2D( zone.GetOrigin(), averageFriendlySpawns ) > Distance2D( zone.GetOrigin(), averageEnemySpawns ) ) + spawnzoneHasEnemies = true - if ( spawnzoneEvil ) + if ( spawnzoneHasEnemies ) continue - // rate spawnzone based on distance to frontline Frontline frontline = GetFrontline( team ) - - // prefer spawns close to base pos - float rating = 10 * ( 1.0 - Distance2D( averageFriendlySpawns, spawnzone.GetOrigin() ) / baseDistance ) + float rating = 10 * ( 1.0 - Distance2D( averageFriendlySpawns, zone.GetOrigin() ) / baseDistance ) if ( frontline.friendlyCenter != < 0, 0, 0 > ) { - // 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( spawnzone.GetOrigin(), frontline.friendlyCenter ) / baseDistance ) ) - rating *= fabs( frontline.combatDir.y - Normalize( spawnzone.GetOrigin() - averageFriendlySpawns ).y ) + rating += rating * ( 1.0 - ( Distance2D( zone.GetOrigin(), frontline.friendlyCenter ) / baseDistance ) ) + rating *= fabs( frontline.combatDir.y - Normalize( zone.GetOrigin() - averageFriendlySpawns ).y ) } - spawnzone.s.spawnzoneRating = rating - possibleZones.append( spawnzone ) + zoneProperties.zoneRating = rating + possibleZones.append( zone ) } - if ( possibleZones.len() == 0 ) + if ( !possibleZones.len() ) return null - possibleZones.sort( int function( entity a, entity b ) - { - if ( a.s.spawnzoneRating > b.s.spawnzoneRating ) - return -1 - - if ( b.s.spawnzoneRating > a.s.spawnzoneRating ) - return 1 - - return 0 - } ) - entity chosenZone = possibleZones[ minint( RandomInt( 3 ), possibleZones.len() - 1 ) ] + possibleZones.sort( SortPossibleZones ) - if ( spawnStateSpawnzones.shouldCreateMinimapSpawnzones ) - { - entity oldEnt - if ( team in spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts ) - oldEnt = spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts[ team ] - - spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts[ team ] <- CreateTeamSpawnZoneEntity( chosenZone, team ) - if ( IsValid( oldEnt ) ) - oldEnt.Destroy() - } - - spawnStateSpawnzones.activeTeamSpawnzones[ team ] <- chosenZone - } - - return spawnStateSpawnzones.activeTeamSpawnzones[ team ] -} - -// ideally this should be in the gamemode_ctf file, but would need refactors to expose more stuff that's not available there rn -entity function DecideSpawnZone_CTF( array<entity> spawnzones, int team ) -{ - if ( spawnzones.len() == 0 ) - return null - - int otherTeam = GetOtherTeam( team ) - array<entity> enemyPlayers = GetPlayerArrayOfTeam( otherTeam ) - - // get average team startspawn positions - int spawnCompareTeam = team - if ( HasSwitchedSides() ) - spawnCompareTeam = GetOtherTeam( team ) - - array<entity> startSpawns = SpawnPoints_GetPilotStart( spawnCompareTeam ) - array<entity> enemyStartSpawns = SpawnPoints_GetPilotStart( GetOtherTeam( spawnCompareTeam ) ) - - if ( startSpawns.len() == 0 || enemyStartSpawns.len() == 0 ) // ensure we don't crash - return null - - // get average startspawn position and max dist between spawns - // could probably cache this, tbh, not like it should change outside of halftimes - vector averageFriendlySpawns - foreach ( entity spawn in startSpawns ) - averageFriendlySpawns += spawn.GetOrigin() - - averageFriendlySpawns /= startSpawns.len() - - // get average enemy startspawn position - vector averageEnemySpawns - foreach ( entity spawn in enemyStartSpawns ) - averageEnemySpawns += spawn.GetOrigin() - - averageEnemySpawns /= enemyStartSpawns.len() - - float baseDistance = Distance2D( averageFriendlySpawns, averageEnemySpawns ) - - // find new zone - array<entity> possibleZones - foreach ( entity spawnzone in spawnStateSpawnzones.mapSpawnzoneTriggers ) - { - // can't choose zone if another team has it - if ( otherTeam in spawnStateSpawnzones.activeTeamSpawnzones && spawnStateSpawnzones.activeTeamSpawnzones[ otherTeam ] == spawnzone ) - continue + entity chosenZone = possibleZones[ minint( RandomInt( 3 ), possibleZones.len() - 1 ) ] - // check zone validity - bool spawnzoneEvil = false - foreach ( entity player in enemyPlayers ) + if ( file.shouldCreateMinimapSpawnzones ) { - // couldn't get IsTouching, GetTouchingEntities or enter callbacks to work in testing, so doing this - if ( spawnzone.ContainsPoint( player.GetOrigin() ) ) + foreach ( zone, zoneProperties in mapSpawnZones ) { - spawnzoneEvil = true - break + if ( chosenZone == zone ) + continue + + if ( IsValid( zoneProperties.minimapEnt ) && zoneProperties.controllingTeam == team ) + zoneProperties.minimapEnt.Destroy() } + + CreateTeamSpawnZoneEntity( chosenZone, team ) } - // don't choose spawnzones that are closer to enemy base than friendly base - if ( !spawnzoneEvil && Distance2D( spawnzone.GetOrigin(), averageFriendlySpawns ) > Distance2D( spawnzone.GetOrigin(), averageEnemySpawns ) ) - spawnzoneEvil = true - - if ( spawnzoneEvil ) - continue - - // rate spawnzone based on distance to frontline - Frontline frontline = GetFrontline( team ) - - // prefer spawns close to base pos - float rating = 10 * ( 1.0 - Distance2D( averageFriendlySpawns, spawnzone.GetOrigin() ) / baseDistance ) - - if ( frontline.friendlyCenter != < 0, 0, 0 > ) + foreach ( zone, zoneProperties in mapSpawnZones ) { - // 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( spawnzone.GetOrigin(), frontline.friendlyCenter ) / baseDistance ) ) - rating *= fabs( frontline.combatDir.y - Normalize( spawnzone.GetOrigin() - averageFriendlySpawns ).y ) - - // reduce rating based on players that can currently see the zone - bool hasAppliedInitialLoss = false - foreach ( entity player in enemyPlayers ) - { - // don't trace here, just do an angle check - if ( PlayerCanSee( player, spawnzone, false, 65 ) && Distance2D( player.GetOrigin(), spawnzone.GetOrigin() ) <= 2000.0 ) - { - float distFrac = TraceLineSimple( player.GetOrigin(), spawnzone.GetOrigin(), player ) - - if ( distFrac >= 0.65 ) - { - // give a fairly large loss if literally anyone can see it - if ( !hasAppliedInitialLoss ) - { - rating *= 0.8 - hasAppliedInitialLoss = true - } - - rating *= ( 1.0 / enemyPlayers.len() ) * distFrac - } - } - } + if ( chosenZone == zone ) + continue + + if ( zoneProperties.controllingTeam == team ) + zoneProperties.controllingTeam = TEAM_UNASSIGNED } - spawnzone.s.spawnzoneRating = rating - possibleZones.append( spawnzone ) + mapSpawnZones[chosenZone].controllingTeam = team + return chosenZone } - if ( possibleZones.len() == 0 ) - return null - - possibleZones.sort( int function( entity a, entity b ) - { - if ( a.s.spawnzoneRating > b.s.spawnzoneRating ) - return -1 + return null +} + +int function SortPossibleZones( entity a, entity b ) +{ + if ( mapSpawnZones[a].zoneRating > mapSpawnZones[b].zoneRating ) + return -1 - if ( b.s.spawnzoneRating > a.s.spawnzoneRating ) - return 1 + if ( mapSpawnZones[b].zoneRating > mapSpawnZones[a].zoneRating ) + return 1 - return 0 - } ) - entity chosenZone = possibleZones[ minint( RandomInt( 3 ), possibleZones.len() - 1 ) ] - - if ( spawnStateSpawnzones.shouldCreateMinimapSpawnzones ) - { - entity oldEnt - if ( team in spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts ) - oldEnt = spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts[ team ] - - spawnStateSpawnzones.activeTeamSpawnzoneMinimapEnts[ team ] <- CreateTeamSpawnZoneEntity( chosenZone, team ) - if ( IsValid( oldEnt ) ) - oldEnt.Destroy() - } - - spawnStateSpawnzones.activeTeamSpawnzones[ team ] <- chosenZone - - return spawnStateSpawnzones.activeTeamSpawnzones[ team ] + return 0 }
\ No newline at end of file |