aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut439
1 files changed, 439 insertions, 0 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut
new file mode 100644
index 000000000..26e4c7135
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/spawn.nut
@@ -0,0 +1,439 @@
+untyped
+
+global function InitRatings // temp for testing
+
+global function Spawn_Init
+global function SetSpawnsUseFrontline
+global function SetRespawnsEnabled
+global function RespawnsEnabled
+global function SetSpawnpointGamemodeOverride
+global function GetSpawnpointGamemodeOverride
+global function CreateNoSpawnArea
+global function DeleteNoSpawnArea
+
+global function GetCurrentFrontline
+global function FindSpawnPoint
+
+global function RateSpawnpoints_Generic
+
+struct NoSpawnArea
+{
+ string id
+ int blockedTeam
+ int blockOtherTeams
+ vector position
+ float lifetime
+ float radius
+}
+
+struct {
+ bool respawnsEnabled = true
+ string spawnpointGamemodeOverride
+
+ array<vector> preferSpawnNodes
+ table<string, NoSpawnArea> noSpawnAreas
+ bool sidesSwitched = false
+
+ bool frontlineBased = false
+ float lastImcFrontlineRatingTime
+ float lastMilitiaFrontlineRatingTime
+ Frontline& lastImcFrontline
+ Frontline& lastMilitiaFrontline
+} file
+
+void function Spawn_Init()
+{
+ AddCallback_GameStateEnter( eGameState.SwitchingSides, OnSwitchingSides )
+ AddCallback_EntitiesDidLoad( InitPreferSpawnNodes )
+
+ AddSpawnCallback( "info_spawnpoint_human", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_human_start", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_titan", InitSpawnpoint )
+ AddSpawnCallback( "info_spawnpoint_titan_start", InitSpawnpoint )
+}
+
+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
+
+ file.preferSpawnNodes.append( hardpoint.GetOrigin() )
+ }
+
+ //foreach ( entity frontline in GetEntArrayByClass_Expensive( "info_frontline" ) )
+ // file.preferSpawnNodes.append( frontline.GetOrigin() )
+}
+
+void function InitSpawnpoint( entity spawnpoint )
+{
+ spawnpoint.s.lastUsedTime <- -999
+}
+
+void function SetRespawnsEnabled( bool enabled )
+{
+ file.respawnsEnabled = enabled
+}
+
+bool function RespawnsEnabled()
+{
+ return file.respawnsEnabled
+}
+
+string function CreateNoSpawnArea( int blockSpecificTeam, int blockEnemiesOfTeam, vector position, float lifetime, float radius )
+{
+ NoSpawnArea noSpawnArea
+ noSpawnArea.blockedTeam = blockSpecificTeam
+ noSpawnArea.blockOtherTeams = blockEnemiesOfTeam
+ noSpawnArea.position = position
+ noSpawnArea.lifetime = lifetime
+ noSpawnArea.radius = radius
+
+ // generate an id
+ noSpawnArea.id = UniqueString( "noSpawnArea" )
+
+ thread NoSpawnAreaLifetime( noSpawnArea )
+
+ return noSpawnArea.id
+}
+
+void function NoSpawnAreaLifetime( NoSpawnArea noSpawnArea )
+{
+ wait noSpawnArea.lifetime
+ DeleteNoSpawnArea( noSpawnArea.id )
+}
+
+void function DeleteNoSpawnArea( string noSpawnIdx )
+{
+ try // unsure if the trycatch is necessary but better safe than sorry
+ {
+ delete file.noSpawnAreas[ noSpawnIdx ]
+ }
+ catch ( exception )
+ {}
+}
+
+void function SetSpawnpointGamemodeOverride( string gamemode )
+{
+ file.spawnpointGamemodeOverride = gamemode
+}
+
+string function GetSpawnpointGamemodeOverride()
+{
+ if ( file.spawnpointGamemodeOverride != "" )
+ return file.spawnpointGamemodeOverride
+ else
+ return GAMETYPE
+
+ unreachable
+}
+
+void function SetSpawnsUseFrontline( bool useFrontline )
+{
+ file.frontlineBased = useFrontline
+}
+
+bool function InitRatings( entity player, int team )
+{
+ Frontline frontline = GetCurrentFrontline( team )
+ print( team )
+ print( frontline.friendlyCenter )
+
+ vector offsetOrigin = frontline.friendlyCenter + frontline.combatDir * 256
+ SpawnPoints_InitFrontlineData( offsetOrigin, frontline.combatDir, frontline.line, frontline.friendlyCenter, 2.0 ) // temp
+
+ if ( player != null )
+ SpawnPoints_InitRatings( player, team ) // no idea what the second arg supposed to be lol
+
+ return frontline.friendlyCenter == < 0, 0, 0 > && file.frontlineBased // if true, use startspawns
+}
+
+Frontline function GetCurrentFrontline( int team )
+{
+ float lastFrontlineRatingTime
+ Frontline lastFrontline
+ if ( team == TEAM_IMC )
+ {
+ lastFrontline = file.lastImcFrontline
+ lastFrontlineRatingTime = file.lastImcFrontlineRatingTime
+ }
+ else
+ {
+ lastFrontline = file.lastMilitiaFrontline
+ lastFrontlineRatingTime = file.lastMilitiaFrontlineRatingTime
+ }
+
+ // current frontline is old, get a new one
+ if ( lastFrontlineRatingTime + 20.0 < Time() || lastFrontline.friendlyCenter == < 0, 0, 0 > )
+ {
+ print( "rerating frontline..." )
+ Frontline frontline = GetFrontline( team )
+
+ // this doesn't work lol
+ /*if ( frontline.friendlyCenter == < 0, 0, 0 > )
+ {
+ // recalculate to startspawnpoint positions
+ array<entity> startSpawns = SpawnPoints_GetPilotStart( team )
+
+ vector averagePos
+ vector averageDir
+ foreach ( entity spawnpoint in startSpawns )
+ {
+ averagePos.x += spawnpoint.GetOrigin().x
+ averagePos.y += spawnpoint.GetOrigin().y
+ averagePos.z += spawnpoint.GetOrigin().z
+
+ averageDir.x += spawnpoint.GetAngles().x
+ averageDir.y += spawnpoint.GetAngles().y
+ averageDir.z += spawnpoint.GetAngles().z
+ }
+
+ averagePos.x /= startSpawns.len()
+ averagePos.y /= startSpawns.len()
+ averagePos.z /= startSpawns.len()
+
+ averageDir.x /= startSpawns.len()
+ averageDir.y /= startSpawns.len()
+ averageDir.z /= startSpawns.len()
+
+ print( "average " + averagePos )
+
+ frontline.friendlyCenter = averagePos
+ frontline.origin = averagePos
+ frontline.combatDir = averageDir * -1
+ }*/
+
+ if ( team == TEAM_IMC )
+ {
+ file.lastImcFrontlineRatingTime = Time()
+ file.lastImcFrontline = frontline
+ }
+ else
+ {
+ file.lastMilitiaFrontlineRatingTime = Time()
+ file.lastMilitiaFrontline = frontline
+ }
+
+ lastFrontline = frontline
+ }
+
+ return lastFrontline
+}
+
+entity function FindSpawnPoint( entity player, bool isTitan, bool useStartSpawnpoint )
+{
+ int team = player.GetTeam()
+ if ( file.sidesSwitched )
+ team = GetOtherTeam( team )
+
+ useStartSpawnpoint = InitRatings( player, player.GetTeam() ) || useStartSpawnpoint // force startspawns if no frontline
+ print( "useStartSpawnpoint: " + useStartSpawnpoint )
+
+ array<entity> spawnpoints
+ if ( useStartSpawnpoint )
+ spawnpoints = isTitan ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetPilotStart( team )
+ else
+ spawnpoints = isTitan ? SpawnPoints_GetTitan() : SpawnPoints_GetPilot()
+
+ void functionref( int, array<entity>, int, entity ) ratingFunc = isTitan ? GameMode_GetTitanSpawnpointsRatingFunc( GAMETYPE ) : GameMode_GetPilotSpawnpointsRatingFunc( GAMETYPE )
+ ratingFunc( isTitan ? TD_TITAN : TD_PILOT, spawnpoints, team, player )
+
+ if ( isTitan )
+ {
+ if ( useStartSpawnpoint )
+ SpawnPoints_SortTitanStart()
+ else
+ SpawnPoints_SortTitan()
+
+ spawnpoints = useStartSpawnpoint ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetTitan()
+ }
+ else
+ {
+ if ( useStartSpawnpoint )
+ SpawnPoints_SortPilotStart()
+ else
+ SpawnPoints_SortPilot()
+
+ spawnpoints = useStartSpawnpoint ? SpawnPoints_GetPilotStart( team ) : SpawnPoints_GetPilot()
+ }
+
+ entity spawnpoint = GetBestSpawnpoint( player, spawnpoints )
+
+ spawnpoint.s.lastUsedTime = Time()
+ player.SetLastSpawnPoint( spawnpoint )
+
+ return spawnpoint
+}
+
+entity function GetBestSpawnpoint( entity player, array<entity> spawnpoints )
+{
+ // not really 100% sure on this randomisation, needs some thought
+ array<entity> validSpawns
+ 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 )
+ {
+ // no valid spawns, very bad, so dont care about spawns being valid anymore
+ print( "found no valid spawns! spawns may be subpar!" )
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ validSpawns.append( spawnpoint )
+
+ if ( validSpawns.len() == 3 ) // arbitrary small sample size
+ break
+ }
+ }
+
+ return validSpawns[ RandomInt( validSpawns.len() ) ] // slightly randomize it
+}
+
+bool function IsSpawnpointValid( entity spawnpoint, int team )
+{
+ //if ( !spawnpoint.HasKey( "ignoreGamemode" ) || ( spawnpoint.HasKey( "ignoreGamemode" ) && spawnpoint.kv.ignoreGamemode == "0" ) ) // used by script-spawned spawnpoints
+ //{
+ // if ( file.spawnpointGamemodeOverride != "" )
+ // {
+ // string gamemodeKey = "gamemode_" + file.spawnpointGamemodeOverride
+ // if ( spawnpoint.HasKey( gamemodeKey ) && ( spawnpoint.kv[ gamemodeKey ] == "0" || spawnpoint.kv[ gamemodeKey ] == "" ) )
+ // return false
+ // }
+ // else if ( GameModeRemove( spawnpoint ) )
+ // return false
+ //}
+
+ if ( Riff_FloorIsLava() && spawnpoint.GetOrigin().z < GetLethalFogTop() )
+ return false
+
+ int compareTeam = spawnpoint.GetTeam()
+ if ( file.sidesSwitched && ( compareTeam == TEAM_MILITIA || compareTeam == TEAM_IMC ) )
+ compareTeam = GetOtherTeam( compareTeam )
+
+ if ( spawnpoint.GetTeam() > 0 && compareTeam != team && !IsFFAGame() )
+ return false
+
+ if ( spawnpoint.IsOccupied() )
+ return false
+
+ foreach ( k, NoSpawnArea noSpawnArea in file.noSpawnAreas )
+ {
+ 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
+ }
+
+ array<entity> projectiles = GetProjectileArrayEx( "any", TEAM_ANY, TEAM_ANY, spawnpoint.GetOrigin(), 400 )
+ foreach ( entity projectile in projectiles )
+ if ( projectile.GetTeam() != team )
+ return false
+
+ if ( Time() - spawnpoint.s.lastUsedTime <= 1.0 )
+ return false
+
+ return true
+}
+
+void function RateSpawnpoints_Generic( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ // 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 file.preferSpawnNodes )
+ {
+ 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 )
+
+ 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
+ }
+
+ 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 < file.preferSpawnNodes.len(); i++ )
+ {
+ // bonus if autotitan is nearish
+ if ( IsAlive( player.GetPetTitan() ) && Distance( player.GetPetTitan().GetOrigin(), file.preferSpawnNodes[ i ] ) < 1200.0 )
+ petTitanModifier += 10.0
+
+ float dist = Distance2D( spawnpoint.GetOrigin(), file.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
+ }
+
+ 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 ( rating != 0.0 || currentRating != 0.0 )
+ print( "rating = " + rating + ", internal rating = " + currentRating )
+ }
+}
+
+void function OnSwitchingSides()
+{
+ file.sidesSwitched = true
+} \ No newline at end of file