aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts/mp
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/mp')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut2
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_challenges.gnut270
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut190
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut25
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut530
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut19
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut39
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut4
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut771
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