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 preferSpawnNodes table 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 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 spawnpoints if ( useStartSpawnpoint ) spawnpoints = isTitan ? SpawnPoints_GetTitanStart( team ) : SpawnPoints_GetPilotStart( team ) else spawnpoints = isTitan ? SpawnPoints_GetTitan() : SpawnPoints_GetPilot() void functionref( int, array, 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 spawnpoints ) { // not really 100% sure on this randomisation, needs some thought array 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 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 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 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 }