From 1d95b7d5f3a4b7176456c94b147f0382de04f18e Mon Sep 17 00:00:00 2001 From: Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> Date: Sat, 2 Sep 2023 17:52:06 +0100 Subject: Progression system (#655) This also can't hurt right? --------- Co-authored-by: uniboi <64006268+uniboi@users.noreply.github.com> --- .../mod/scripts/vscripts/mp/_base_gametype_mp.gnut | 9 + .../mod/scripts/vscripts/mp/_changemap.nut | 4 +- .../mod/scripts/vscripts/mp/_gamestate_mp.nut | 2 + .../mod/scripts/vscripts/mp/_score.nut | 11 +- .../mod/scripts/vscripts/mp/_stats.nut | 1043 +++++++++++++++++++- 5 files changed, 1055 insertions(+), 14 deletions(-) (limited to 'Northstar.CustomServers/mod/scripts/vscripts/mp') 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 3bd0cd690..bab7eaede 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut @@ -134,6 +134,15 @@ void function CodeCallback_OnClientConnectionCompleted( entity player ) Lobby_OnClientConnectionCompleted( player ) return } + else if ( !IsFDMode( GAMETYPE ) ) + { + // reset this for non-fd modes + // for some reason the postgame scoreboard uses this to + // determine if it should show the FD aegis rank one + // FD should either set this in their own mode, or add an else + // to this if statement when it releases + player.SetPersistentVar( "lastFDTitanRef", "" ) + } player.hasConnected = true diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_changemap.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_changemap.nut index 16a3ce922..0ababfc71 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_changemap.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_changemap.nut @@ -58,7 +58,9 @@ void function CodeCallback_MatchIsOver() #if MP void function PopulatePostgameData() { - // something's busted here because this isn't showing automatically on match end, ag + // show the postgame scoreboard summary + SetUIVar( level, "showGameSummary", true ) + foreach ( entity player in GetPlayerArray() ) { int teams = GetCurrentPlaylistVarInt( "max_teams", 2 ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut index 46b39ebc1..3426cec52 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut @@ -800,6 +800,8 @@ void function SetWinner( int team, string winningReason = "", string losingReaso } else SetGameState( eGameState.WinnerDetermined ) + + ScoreEvent_MatchComplete( team ) } } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut index dacd43b0f..0b55e9ffd 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_score.nut @@ -7,6 +7,7 @@ global function ScoreEvent_PlayerKilled global function ScoreEvent_TitanDoomed global function ScoreEvent_TitanKilled global function ScoreEvent_NPCKilled +global function ScoreEvent_MatchComplete global function ScoreEvent_SetEarnMeterValues global function ScoreEvent_SetupEarnMeterValuesForMixedModes @@ -230,7 +231,15 @@ void function ScoreEvent_NPCKilled( entity victim, entity attacker, var damageIn catch ( ex ) {} } - +void function ScoreEvent_MatchComplete( int winningTeam ) +{ + foreach( entity player in GetPlayerArray() ) + { + AddPlayerScore( player, "MatchComplete" ) + if ( player.GetTeam() == winningTeam ) + AddPlayerScore( player, "MatchVictory" ) + } +} void function ScoreEvent_SetEarnMeterValues( string eventName, float earned, float owned, float coreScale = 1.0 ) { diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut index 0e8b58f45..92c0f4014 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_stats.nut @@ -1,3 +1,5 @@ +untyped // because entity.s + global function Stats_Init global function AddStatCallback global function Stats_SaveStatDelayed @@ -12,67 +14,1084 @@ global function PreScoreEventUpdateStats global function PostScoreEventUpdateStats global function Stats_OnPlayerDidDamage +struct { + table< string, array > refs + table< string, array< void functionref( entity, float, string ) > > callbacks + + table< entity, table< string, int > > cachedIntStatChanges + table< table< string, float > > cachedFloatStatChanges + + table< entity, float > playerKills + table< entity, float > playerKillsPvp + table< entity, float > playerDeaths + table< entity, float > playerDeathsPvp + + bool isFirstStrike = true +} file + void function Stats_Init() { + AddCallback_OnPlayerKilled( OnPlayerOrNPCKilled ) + AddCallback_OnNPCKilled( OnPlayerOrNPCKilled ) + AddCallback_OnPlayerRespawned( OnPlayerRespawned ) + AddCallback_OnClientConnected( OnClientConnected ) + AddCallback_OnClientDisconnected( OnClientDisconnected ) + AddCallback_GameStateEnter( eGameState.Epilogue, OnEpilogueStarted ) + thread HandleDistanceAndTimeStats_Threaded() + thread SaveStatsPeriodically_Threaded() } -void function AddStatCallback(string statCategory, string statAlias, string statSubAlias, void functionref(entity, float, string) callback, string subRef) +void function AddStatCallback( string statCategory, string statAlias, string statSubAlias, void functionref( entity, float, string ) callback, string subRef ) { + if ( !IsValidStat( statCategory, statAlias, statSubAlias ) ) + throw format( "INVALID STAT: %s : %s : %s", statCategory, statAlias, statSubAlias ) + + + string statVar = GetStatVar( statCategory, statAlias, statSubAlias ) + if ( statVar in file.refs ) + { + file.refs[ statVar ].append( subRef ) + file.callbacks[ statVar ].append( callback ) + } + else + { + file.refs[ statVar ] <- [ subRef ] + file.callbacks[ statVar ] <- [ callback ] + } } -void function Stats_SaveStatDelayed(entity player, string statCategory, string statAlias, string statSubAlias) +// a lot of this file seems to be doing caching of stats in some way +void function Stats_SaveStatDelayed( entity player, string statCategory, string statAlias, string statSubAlias, float delay = 0.1 ) { + // idk how long the delay is meant to be but whatever + wait delay + + if ( !IsValid( player ) ) + return + + Stats_SaveStat( player, statCategory, statAlias, statSubAlias ) +} + +void function Stats_SaveAllStats( entity player ) +{ + if ( player in file.cachedIntStatChanges ) + { + foreach( string key, int val in file.cachedIntStatChanges[ player ] ) + { + player.SetPersistentVar( key, player.GetPersistentVarAsInt( key ) + val ) + } + + delete file.cachedIntStatChanges[ player ] + } + // save cached float stat change + if ( player in file.cachedFloatStatChanges ) + { + foreach( string key, float val in file.cachedFloatStatChanges[ player ] ) + { + player.SetPersistentVar( key, expect float( player.GetPersistentVar( key ) ) + val ) + } + delete file.cachedFloatStatChanges[ player ] + } } -int function PlayerStat_GetCurrentInt(entity player, string statCategory, string statAlias, string statSubAlias) +void function Stats_SaveStat( entity player, string statCategory, string statAlias, string statSubAlias ) { + string stat = GetStatVar( statCategory, statAlias, statSubAlias ) + // save cached int stat change + if ( player in file.cachedIntStatChanges && stat in file.cachedIntStatChanges[ player ] ) + { + player.SetPersistentVar( stat, player.GetPersistentVarAsInt( stat ) + file.cachedIntStatChanges[ player ][ stat ] ) + delete file.cachedIntStatChanges[ player ][ stat ] + return + } + // save cached float stat change + if ( player in file.cachedFloatStatChanges && stat in file.cachedFloatStatChanges[ player ] ) + { + player.SetPersistentVar( stat, expect float( player.GetPersistentVar( stat ) ) + file.cachedFloatStatChanges[ player ][ stat ] ) + delete file.cachedFloatStatChanges[ player ][ stat ] + return + } +} + +// this gets the cached change, not the actual value +int function PlayerStat_GetCurrentInt( entity player, string statCategory, string statAlias, string statSubAlias ) +{ + string str = GetStatVar( statCategory, statAlias, statSubAlias ) + + if ( player in file.cachedIntStatChanges && str in file.cachedIntStatChanges[ player ] ) + return file.cachedIntStatChanges[ player ][ str ] return 0 } -float function PlayerStat_GetCurrentFloat(entity player, string statCategory, string statAlias, string statSubAlias) +// this gets the cached change, not the actual value +float function PlayerStat_GetCurrentFloat( entity player, string statCategory, string statAlias, string statSubAlias ) { + string str = GetStatVar( statCategory, statAlias, statSubAlias ) + + if ( player in file.cachedFloatStatChanges && str in file.cachedFloatStatChanges[ player ] ) + return file.cachedFloatStatChanges[ player ][ str ] return 0 } -void function UpdatePlayerStat(entity player, string statCategory, string subStat, int count = 0) +void function UpdatePlayerStat( entity player, string statCategory, string subStat, int count = 1 ) { + if ( !IsValid( player ) ) + return + + Stats_IncrementStat( player, statCategory, subStat, "", count.tofloat() ) +} + +void function IncrementPlayerDidPilotExecutionWhileCloaked( entity player ) +{ + UpdatePlayerStat( player, "kills_stats", "pilotExecutePilotWhileCloaked" ) +} + +void function UpdateTitanDamageStat( entity attacker, float savedDamage, var damageInfo ) +{ + if ( !IsValid( attacker ) ) + return + + Stats_IncrementStat( attacker, "titan_stats", "titanDamage", GetTitanCharacterName( attacker ), savedDamage ) +} + +void function UpdateTitanWeaponDamageStat( entity attacker, float savedDamage, var damageInfo ) +{ + if ( !IsValid( attacker ) ) + return + + string weaponName = GetPersistenceRefFromDamageInfo( damageInfo ) + if ( weaponName == "" ) + return + + Stats_IncrementStat( attacker, "weapon_stats", "titanDamage", weaponName, savedDamage ) +} + +void function UpdateTitanCoreEarnedStat( entity player, entity titan, int count = 1 ) +{ + if ( !IsValid( player ) ) + return + + if ( !IsValid( titan ) ) + return + + Stats_IncrementStat( player, "titan_stats", "coresEarned", GetTitanCharacterName( titan ), count.tofloat() ) +} + +void function PreScoreEventUpdateStats( entity attacker, entity ent ) +{ + // used to track kill streaks ending i think ( that stuff gets reset during score event update ) +} + +void function PostScoreEventUpdateStats( entity attacker, entity ent ) +{ + if ( !attacker.IsPlayer() ) + return + // used to track kill streaks starting maybe + if ( attacker.s.currentKillstreak == KILLINGSPREE_KILL_REQUIREMENT ) + { + // killingSpressAs_ + if ( attacker.IsTitan() ) + Stats_IncrementStat( attacker, "kills_stats", "killingSpressAs_" + GetTitanCharacterName( attacker ), "", 1.0 ) + + entity weapon = attacker.GetActiveWeapon() + // I guess if you dont have a valid active weapon when you get awarded a killing spree + // you dont get the stat. Too bad! + if ( !IsValid( weapon ) ) + return + Stats_IncrementStat( attacker, "weapon_kill_stats", "killingSprees", weapon.GetWeaponClassName(), 1.0 ) + } +} + +void function Stats_OnPlayerDidDamage( entity victim, var damageInfo ) +{ + // try and get the player + entity attacker = DamageInfo_GetAttacker( damageInfo ) + // get the player from their titan + if ( attacker.IsTitan() && IsPetTitan( attacker ) ) + attacker = attacker.GetTitanSoul().GetBossPlayer() + + if ( !attacker.IsPlayer() ) + return + + string weaponName = GetPersistenceRefFromDamageInfo( damageInfo ) + if ( weaponName == "" ) + return + + Stats_IncrementStat( attacker, "weapon_stats", "shotsHit", weaponName, 1.0 ) + + if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_CRITICAL ) + Stats_IncrementStat( attacker, "weapon_stats", "critHits", weaponName, 1.0 ) + if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_HEADSHOT ) + Stats_IncrementStat( attacker, "weapon_stats", "headshots", weaponName, 1.0 ) +} + +void function Stats_IncrementStat( entity player, string statCategory, string statAlias, string statSubAlias, float amount ) +{ + if ( !IsValidStat( statCategory, statAlias, statSubAlias ) ) + { + printt( "invalid stat: " + statCategory + " : " + statAlias + " : " + statSubAlias ) + return + } + + string str = GetStatVar( statCategory, statAlias, statSubAlias ) + int type = GetStatVarType( statCategory, statAlias, statSubAlias ) + + // stupid exception because respawn set this up as an int in script + // but it is actually a float, so the game will crash if we don't fix it somewhere + // i dont feel like committing all of sh_stats.gnut so im doing this instead + if ( str == "mapStats[%mapname%].hoursPlayed[%gamemode%]" ) + type = ePlayerStatType.FLOAT + + // this is rather hacky + string mode = GAMETYPE + int difficulty = GetDifficultyLevel() + if ( difficulty >= 5 ) + return + + string saveVar = Stats_GetFixedSaveVar( str, GetMapName(), mode, difficulty.tostring() ) + // check if the map and mode exist in persistence + try + { + PersistenceGetEnumIndexForItemName( "gamemodes", mode ) + PersistenceGetEnumIndexForItemName( "maps", GetMapName() ) + } + catch( ex ) + { + // if we have an invalid mode or map for persistence, and it is used in the + // persistence string, we can't save the persistence so we have to just return + if ( str != saveVar ) + { + printt( ex ) + return + } + } + str = saveVar + + switch ( type ) + { + case ePlayerStatType.INT: + // populate table if needed + if ( !( player in file.cachedIntStatChanges ) ) + file.cachedIntStatChanges[ player ] <- {} + if ( !( str in file.cachedIntStatChanges[ player ] ) ) + file.cachedIntStatChanges[ player ][ str ] <- 0 + + file.cachedIntStatChanges[ player ][ str ] += amount.tointeger() + break + case ePlayerStatType.FLOAT: + // populate table if needed + if ( !( player in file.cachedFloatStatChanges ) ) + file.cachedFloatStatChanges[ player ] <- {} + if ( !( str in file.cachedFloatStatChanges[ player ] ) ) + file.cachedFloatStatChanges[ player ][ str ] <- 0.0 + + file.cachedFloatStatChanges[ player ][ str ] += amount + break + default: + throw "UNIMPLEMENTED STAT TYPE: " + type + } + + // amount here is never used + Stats_RunCallbacks( str, player, amount ) +} + +void function Stats_RunCallbacks( string statVar, entity player, float change ) +{ + if ( !( statVar in file.refs ) ) + return + + for( int i = 0; i < file.refs[ statVar ].len(); i++ ) + { + string ref = file.refs[ statVar ][ i ] + void functionref( entity, float, string ) callback = file.callbacks[ statVar ][ i ] + + callback( player, change, ref ) + } +} + +void function OnClientConnected( entity player ) +{ + Stats_IncrementStat( player, "game_stats", "game_joined", "", 1.0 ) +} + +void function OnClientDisconnected( entity player ) +{ + Stats_SaveAllStats( player ) + // maybe we can save this stuff, but idk if we can access persistence in this callback + if ( player in file.cachedIntStatChanges ) + delete file.cachedIntStatChanges[ player ] + + if ( player in file.cachedFloatStatChanges ) + delete file.cachedFloatStatChanges[ player ] +} + +void function OnPlayerOrNPCKilled( entity victim, entity attacker, var damageInfo ) +{ + if ( victim.IsPlayer() ) + thread SetLastPosForDistanceStatValid_Threaded( victim, false ) + + HandleDeathStats( victim, attacker, damageInfo ) + HandleKillStats( victim, attacker, damageInfo ) + HandleWeaponKillStats( victim, attacker, damageInfo ) + HandleTitanStats( victim, attacker, damageInfo ) +} + +void function HandleDeathStats( entity player, entity attacker, var damageInfo ) +{ + if ( !IsValid( player ) || !player.IsPlayer() ) + return + + if ( player in file.playerDeaths ) + file.playerDeaths[ player ]++ + else + file.playerDeaths[ player ] <- 1.0 + // total + Stats_IncrementStat( player, "deaths_stats", "total", "", 1.0 ) + + // these all rely on the attacker being valid + if ( IsValid( attacker ) ) + { + // totalPVP + // note: I'm not sure if owned entities count towards totalPVP + // such as auto-titans, turrets, etc. + if ( attacker.IsPlayer() || attacker.GetBossPlayer() ) + { + if ( player in file.playerDeathsPvp ) + file.playerDeathsPvp[ player ]++ + else + file.playerDeathsPvp[ player ] <- 1.0 + Stats_IncrementStat( player, "deaths_stats", "totalPVP", "", 1.0 ) + } + + // byPilots + if ( IsPilot( attacker ) ) + Stats_IncrementStat( player, "deaths_stats", "byPilots", "", 1.0 ) + + // byTitan_ + if ( attacker.IsTitan() && attacker.IsPlayer() ) + Stats_IncrementStat( player, "deaths_stats", "byTitan_" + GetTitanCharacterName( attacker ), "", 1.0 ) + + // bySpectres + if ( IsSpectre( attacker ) ) + Stats_IncrementStat( player, "deaths_stats", "bySpectres", "", 1.0 ) + + // byGrunts + if ( IsGrunt( attacker ) ) + Stats_IncrementStat( player, "deaths_stats", "byGrunts", "", 1.0 ) + + // byNPCTitans_ + if ( attacker.IsTitan() && attacker.IsNPC() ) + Stats_IncrementStat( player, "deaths_stats", "byNPCTitans_" + GetTitanCharacterName( attacker ), "", 1.0 ) + } + + // asPilot + if ( IsPilot( player ) ) + Stats_IncrementStat( player, "deaths_stats", "asPilot", "", 1.0 ) + + // asTitan_ + if ( player.IsTitan() ) + Stats_IncrementStat( player, "deaths_stats", "asTitan_" + GetTitanCharacterName( player ), "", 1.0 ) + + // suicides + if ( IsSuicide( attacker, player, DamageInfo_GetDamageSourceIdentifier( damageInfo ) ) ) + Stats_IncrementStat( player, "deaths_stats", "suicides", "", 1.0 ) + + // whileEjecting + if ( player.p.pilotEjecting ) + Stats_IncrementStat( player, "deaths_stats", "whileEjecting", "", 1.0 ) +} + +void function HandleWeaponKillStats( entity victim, entity attacker, var damageInfo ) +{ + if ( !IsValid( attacker ) ) + return + + // get the player and it's pet titan + entity player + entity playerPetTitan + if ( attacker.IsPlayer() ) + { + // the player is just the attacker + player = attacker + playerPetTitan = player.GetPetTitan() + } + else if ( attacker.IsTitan() && IsPetTitan( attacker ) ) + { + // the attacker is the player's auto titan + player = attacker.GetTitanSoul().GetBossPlayer() + playerPetTitan = attacker + } + else + { + // attacker could be something like an NPC, or worldspawn + return + } + + string damageSourceStr = GetPersistenceRefFromDamageInfo( damageInfo ) + // cant do weapon stats for no weapon + if ( damageSourceStr == "" ) + return + + // check things once, for performance + int damageSource = DamageInfo_GetDamageSourceIdentifier( damageInfo ) + bool victimIsPlayer = victim.IsPlayer() + bool victimIsNPC = victim.IsNPC() + bool victimIsPilot = IsPilot( victim ) + bool victimIsTitan = victim.IsTitan() + + // total + Stats_IncrementStat( player, "weapon_kill_stats", "total", damageSourceStr, 1.0 ) + + // pilots + if ( victimIsPilot ) + Stats_IncrementStat( player, "weapon_kill_stats", "pilots", damageSourceStr, 1.0 ) + + // ejecting_pilots + if ( victimIsPilot && victim.p.pilotEjecting ) + Stats_IncrementStat( player, "weapon_kill_stats", "ejecting_pilots", damageSourceStr, 1.0 ) + + // titansTotal + if ( victimIsTitan ) + Stats_IncrementStat( player, "weapon_kill_stats", "titansTotal", damageSourceStr, 1.0 ) + // spectres + if ( IsSpectre( victim ) ) + Stats_IncrementStat( player, "weapon_kill_stats", "spectres", damageSourceStr, 1.0 ) + + // marvins + if ( IsMarvin( victim ) ) + Stats_IncrementStat( player, "weapon_kill_stats", "marvins", damageSourceStr, 1.0 ) + + // grunts + if ( IsGrunt( victim ) ) + Stats_IncrementStat( player, "weapon_kill_stats", "grunts", damageSourceStr, 1.0 ) + + // ai + if ( victimIsNPC ) + Stats_IncrementStat( player, "weapon_kill_stats", "ai", damageSourceStr, 1.0 ) + + // titans_ + if ( victimIsPlayer && victimIsTitan ) + Stats_IncrementStat( player, "weapon_kill_stats", "titans_" + GetTitanCharacterName( victim ), damageSourceStr, 1.0 ) + + // npcTitans_ + if ( victimIsNPC && victimIsTitan ) + Stats_IncrementStat( player, "weapon_kill_stats", "npcTitans_" + GetTitanCharacterName( victim ), damageSourceStr, 1.0 ) } -void function IncrementPlayerDidPilotExecutionWhileCloaked(entity player) +void function HandleKillStats( entity victim, entity attacker, var damageInfo ) { + if ( !IsValid( attacker ) ) + return + // get the player and it's pet titan + entity player + entity playerPetTitan + if ( attacker.IsPlayer() ) + { + // the player is just the attacker + player = attacker + playerPetTitan = player.GetPetTitan() + } + else if ( attacker.IsTitan() && IsPetTitan( attacker ) ) + { + // the attacker is the player's auto titan + player = attacker.GetTitanSoul().GetBossPlayer() + playerPetTitan = attacker + } + else + { + // attacker could be something like an NPC, or worldspawn + return + } + + // check things once, for performance + int damageSource = DamageInfo_GetDamageSourceIdentifier( damageInfo ) + bool victimIsPlayer = victim.IsPlayer() + bool victimIsNPC = victim.IsNPC() + bool victimIsPilot = IsPilot( victim ) + bool victimIsTitan = victim.IsTitan() + + if ( player in file.playerKills ) + file.playerKills[ player ]++ + else + file.playerKills[ player ] <- 1.0 + // total + Stats_IncrementStat( player, "kills_stats", "total", "", 1.0 ) + + // totalPVP + if ( victimIsPlayer ) + { + if ( player in file.playerKillsPvp ) + file.playerKillsPvp[ player ]++ + else + file.playerKillsPvp[ player ] <- 1.0 + Stats_IncrementStat( player, "kills_stats", "totalPVP", "", 1.0 ) + } + + // pilots + if ( victimIsPilot ) + Stats_IncrementStat( player, "kills_stats", "pilots", "", 1.0 ) + + // spectres + if ( IsSpectre( victim ) ) + Stats_IncrementStat( player, "kills_stats", "spectres", "", 1.0 ) + + // marvins + if ( IsMarvin( victim ) ) + Stats_IncrementStat( player, "kills_stats", "marvins", "", 1.0 ) + + // grunts + if ( IsGrunt( victim ) ) + Stats_IncrementStat( player, "kills_stats", "grunts", "", 1.0 ) + + // totalTitans + if ( victimIsTitan ) + Stats_IncrementStat( player, "kills_stats", "totalTitans", "", 1.0 ) + + // totalPilots + if ( victimIsPilot ) + Stats_IncrementStat( player, "kills_stats", "totalPilots", "", 1.0 ) + + // totalNPC + if ( victimIsNPC ) + Stats_IncrementStat( player, "kills_stats", "totalNPC", "", 1.0 ) + + // totalTitansWhileDoomed + if ( victimIsTitan && attacker.IsTitan() && GetDoomedState( attacker ) ) + Stats_IncrementStat( player, "kills_stats", "totalTitansWhileDoomed", "", 1.0 ) + + // asPilot + if ( IsPilot( attacker ) ) + Stats_IncrementStat( player, "kills_stats", "asPilot", "", 1.0 ) + + // totalAssists + // assistsTotal ( weapon_kill_stats ) + // note: eww + table alreadyAssisted + foreach( DamageHistoryStruct attackerInfo in victim.e.recentDamageHistory ) + { + if ( !IsValid( attackerInfo.attacker ) || !attackerInfo.attacker.IsPlayer() || attackerInfo.attacker == victim ) + continue + + bool exists = attackerInfo.attacker.GetEncodedEHandle() in alreadyAssisted ? true : false + if( attackerInfo.attacker != attacker && !exists ) + { + alreadyAssisted[ attackerInfo.attacker.GetEncodedEHandle() ] <- true + Stats_IncrementStat( attackerInfo.attacker, "kills_stats", "totalAssists", "", 1.0 ) + + string source = DamageSourceIDToString( attackerInfo.damageSourceId ) + + if ( IsValidStatItemString( source ) ) + Stats_IncrementStat( attacker, "weapon_kill_stats", "assistsTotal", source, 1.0 ) + } + } + + // asTitan_ + if ( player.IsTitan() ) + Stats_IncrementStat( player, "kills_stats", "asTitan_" + GetTitanCharacterName( player ), "", 1.0 ) + + // firstStrikes + if ( file.isFirstStrike && attacker.IsPlayer() && victimIsPlayer ) + { + Stats_IncrementStat( player, "kills_stats", "firstStrikes", "", 1.0 ) + file.isFirstStrike = false + } + + // ejectingPilots + if ( victimIsPilot && victim.p.pilotEjecting ) + Stats_IncrementStat( player, "kills_stats", "ejectingPilots", "", 1.0 ) + + // whileEjecting + if ( attacker.IsPlayer() && attacker.p.pilotEjecting ) + Stats_IncrementStat( player, "kills_stats", "whileEjecting", "", 1.0 ) + + // cloakedPilots + if ( victimIsPilot && IsCloaked( victim ) ) + Stats_IncrementStat( player, "kills_stats", "cloakedPilots", "", 1.0 ) + + // whileCloaked + if ( attacker == player && IsCloaked( attacker ) ) + Stats_IncrementStat( player, "kills_stats", "whileCloaked", "", 1.0 ) + + // wallrunningPilots + if ( victimIsPilot && victim.IsWallRunning() ) + Stats_IncrementStat( player, "kills_stats", "wallrunningPilots", "", 1.0 ) + + // whileWallrunning + if ( attacker == player && attacker.IsWallRunning() ) + Stats_IncrementStat( player, "kills_stats", "whileWallrunning", "", 1.0 ) + + // wallhangingPilots + if ( victimIsPilot && victim.IsWallHanging() ) + Stats_IncrementStat( player, "kills_stats", "wallhangingPilots", "", 1.0 ) + + // whileWallhanging + if ( attacker == player && attacker.IsWallHanging() ) + Stats_IncrementStat( player, "kills_stats", "whileWallhanging", "", 1.0 ) + + // pilotExecution + if ( damageSource == eDamageSourceId.human_execution ) + Stats_IncrementStat( player, "kills_stats", "pilotExecution", "", 1.0 ) + + // pilotExecutePilot + if ( victimIsPilot && damageSource == eDamageSourceId.human_execution ) + Stats_IncrementStat( player, "kills_stats", "pilotExecutePilot", "", 1.0 ) + + // pilotKillsWithHoloPilotActive + if ( victimIsPilot && GetDecoyActiveCountForPlayer( player ) > 0 ) + Stats_IncrementStat( player, "kills_stats", "pilotKillsWithHoloPilotActive", "", 1.0 ) + + // pilotKillsWithAmpedWallActive + if ( victimIsPilot && GetAmpedWallsActiveCountForPlayer( player ) > 0 ) + Stats_IncrementStat( player, "kills_stats", "pilotKillsWithAmpedWallActive", "", 1.0 ) + + // pilotExecutePilotUsing_ + if ( victimIsPilot && damageSource == eDamageSourceId.human_execution ) + Stats_IncrementStat( player, "kills_stats", "pilotExecutePilotUsing_" + player.p.lastExecutionUsed, "", 1.0 ) + + // pilotKickMelee + if ( damageSource == eDamageSourceId.human_melee ) + Stats_IncrementStat( player, "kills_stats", "pilotKickMelee", "", 1.0 ) + + // pilotKickMeleePilot + if ( victimIsPilot && damageSource == eDamageSourceId.human_melee ) + Stats_IncrementStat( player, "kills_stats", "pilotKickMeleePilot", "", 1.0 ) + + // titanMelee + if ( DamageIsTitanMelee( damageSource ) ) + Stats_IncrementStat( player, "kills_stats", "titanMelee", "", 1.0 ) + + // titanMeleePilot + if ( victimIsPilot && DamageIsTitanMelee( damageSource ) ) + Stats_IncrementStat( player, "kills_stats", "titanMeleePilot", "", 1.0 ) + + // titanStepCrush + if ( IsTitanCrushDamage( damageInfo ) ) + Stats_IncrementStat( player, "kills_stats", "titanStepCrush", "", 1.0 ) + + // titanStepCrushPilot + if ( victimIsPilot && IsTitanCrushDamage( damageInfo ) ) + Stats_IncrementStat( player, "kills_stats", "titanStepCrushPilot", "", 1.0 ) + + // titanExocution + // note: RESPAWN WHY? EXPLAIN + if ( damageSource == eDamageSourceId.titan_execution ) + { + string titanName = GetTitanCharacterName( player ) + titanName = titanName.slice( 0, 1 ).toupper() + titanName.slice( 1, titanName.len() ) + Stats_IncrementStat( player, "kills_stats", "titanExocution" + titanName, "", 1.0 ) + } + + // titanFallKill + if ( damageSource == eDamageSourceId.damagedef_titan_fall ) + Stats_IncrementStat( player, "kills_stats", "titanFallKill", "", 1.0 ) + + // petTitanKillsFollowMode + if ( attacker == playerPetTitan && player.GetPetTitanMode() == eNPCTitanMode.FOLLOW ) + Stats_IncrementStat( player, "kills_stats", "petTitanKillsFollowMode", "", 1.0 ) + + // petTitanKillsGuardMode + if ( attacker == playerPetTitan && player.GetPetTitanMode() == eNPCTitanMode.STAY ) + Stats_IncrementStat( player, "kills_stats", "petTitanKillsGuardMode", "", 1.0 ) + + // rodeo_total + if ( damageSource == eDamageSourceId.rodeo_battery_removal ) + Stats_IncrementStat( player, "kills_stats", "rodeo_total", "", 1.0 ) + + // pilot_headshots_total + if ( victimIsPilot && DamageInfo_GetCustomDamageType( damageInfo ) & DF_HEADSHOT ) + Stats_IncrementStat( player, "kills_stats", "pilot_headshots_total", "", 1.0 ) + + // evacShips + if ( IsEvacDropship( victim ) ) + Stats_IncrementStat( player, "kills_stats", "evacShips", "", 1.0 ) + + // nuclearCore + if ( damageSource == eDamageSourceId.damagedef_nuclear_core ) + Stats_IncrementStat( player, "kills_stats", "nuclearCore", "", 1.0 ) + + // meleeWhileCloaked + if ( IsCloaked( attacker ) && damageSource == eDamageSourceId.human_melee ) + Stats_IncrementStat( player, "kills_stats", "meleeWhileCloaked", "", 1.0 ) + + // titanKillsAsPilot + if ( victimIsTitan && IsPilot( attacker ) ) + Stats_IncrementStat( player, "kills_stats", "titanKillsAsPilot", "", 1.0 ) + + // pilotKillsWhileStimActive + if ( victimIsPilot && StatusEffect_Get( attacker, eStatusEffect.stim_visual_effect ) <= 0 ) + Stats_IncrementStat( player, "kills_stats", "pilotKillsWhileStimActive", "", 1.0 ) + + // pilotKillsAsTitan + if ( victimIsPilot && attacker.IsTitan() ) + Stats_IncrementStat( player, "kills_stats", "pilotKillsAsTitan", "", 1.0 ) + + // pilotKillsAsPilot + if ( victimIsPilot && IsPilot( attacker ) ) + Stats_IncrementStat( player, "kills_stats", "pilotKillsAsPilot", "", 1.0 ) + + // titanKillsAsTitan + if ( victimIsTitan && attacker.IsTitan() ) + Stats_IncrementStat( player, "kills_stats", "titanKillsAsTitan", "", 1.0 ) } -void function UpdateTitanDamageStat(entity attacker, float savedDamage, var damageInfo) +void function HandleTitanStats( entity victim, entity attacker, var damageInfo ) { + if ( !IsValid( attacker ) ) + return + + // get the player and it's pet titan + entity player + entity playerPetTitan + if ( attacker.IsPlayer() ) + { + // the player is just the attacker + player = attacker + playerPetTitan = player.GetPetTitan() + } + else if ( attacker.IsTitan() && IsPetTitan( attacker ) ) + { + // the attacker is the player's auto titan + player = attacker.GetTitanSoul().GetBossPlayer() + playerPetTitan = attacker + } + else + { + // attacker could be something like an NPC, or worldspawn + return + } + + int damageSource = DamageInfo_GetDamageSourceIdentifier( damageInfo ) + bool victimIsPlayer = victim.IsPlayer() + bool victimIsNPC = victim.IsNPC() + bool victimIsPilot = IsPilot( victim ) + bool victimIsTitan = victim.IsTitan() + bool titanIsPrime = IsTitanPrimeTitan( player ) + // pilots + if ( victimIsPilot && attacker.IsTitan() ) + Stats_IncrementStat( player, "titan_stats", "pilots", GetTitanCharacterName( attacker ), 1.0 ) + + // titansTotal + if ( victimIsTitan && attacker.IsTitan() ) + Stats_IncrementStat( player, "titan_stats", "titansTotal", GetTitanCharacterName( attacker ), 1.0 ) + + // pilotsAsPrime + if ( victimIsPilot && attacker.IsTitan() && titanIsPrime ) + Stats_IncrementStat( player, "titan_stats", "pilotsAsPrime", GetTitanCharacterName( attacker ), 1.0 ) + + // titansAsPrime + if ( victimIsTitan && attacker.IsTitan() && titanIsPrime ) + Stats_IncrementStat( player, "titan_stats", "titansAsPrime", GetTitanCharacterName( attacker ), 1.0 ) + + // executionsAsPrime + if ( damageSource == eDamageSourceId.titan_execution && attacker.IsTitan() && titanIsPrime ) + Stats_IncrementStat( player, "titan_stats", "executionsAsPrime", GetTitanCharacterName( attacker ), 1.0 ) } -void function UpdateTitanWeaponDamageStat(entity attacker, float savedDamage, var damageInfo) +void function OnPlayerRespawned( entity player ) { + thread SetLastPosForDistanceStatValid_Threaded( player, true ) +} + +void function OnEpilogueStarted() +{ + // award players for match completed, wins, and losses + foreach ( entity player in GetPlayerArray() ) + { + Stats_IncrementStat( player, "game_stats", "game_completed", "", 1.0 ) + + if ( player.GetTeam() == GetWinningTeam() ) + Stats_IncrementStat( player, "game_stats", "game_won", "", 1.0 ) + else + Stats_IncrementStat( player, "game_stats", "game_lost", "", 1.0 ) + } + + if ( IsValidGamemodeString( GAMETYPE ) ) + { + // award players with matches played on the mode + foreach ( entity player in GetPlayerArray() ) + { + Stats_IncrementStat( player, "game_stats", "mode_played", GAMETYPE, 1.0 ) + + if ( player.GetTeam() == GetWinningTeam() ) + Stats_IncrementStat( player, "game_stats", "mode_won", GAMETYPE, 1.0 ) + } + } + + // update player's KD + foreach ( entity player in GetPlayerArray() ) + { + // kd stats + // index 0 is most recent game + // index 9 is least recent game + float playerKills = ( player in file.playerKills ) ? file.playerKills[ player ] : 0.0 + float playerDeaths = ( player in file.playerDeaths ) ? file.playerDeaths[ player ] : 0.0 + float kdratio_match + if ( playerDeaths == 0.0 ) + kdratio_match = playerKills + else + kdratio_match = playerKills / playerDeaths + + float playerKillsPvp = ( player in file.playerKillsPvp ) ? file.playerKillsPvp[ player ] : 0.0 + float playerDeathsPvp = ( player in file.playerDeathsPvp ) ? file.playerDeathsPvp[ player ] : 0.0 + float kdratiopvp_match + if ( playerDeathsPvp == 0.0 ) + kdratiopvp_match = playerKillsPvp + else + kdratiopvp_match = playerKillsPvp / playerDeathsPvp + + float totalDeaths = player.GetPersistentVarAsInt( "deathStats.total" ).tofloat() + float totalKills = player.GetPersistentVarAsInt( "killStats.total" ).tofloat() + float totalDeathsPvp = player.GetPersistentVarAsInt( "deathStats.totalPVP" ).tofloat() + float totalKillsPvp = player.GetPersistentVarAsInt( "killStats.totalPVP" ).tofloat() + float kdratio_lifetime + if ( totalDeaths == 0.0 ) + kdratio_lifetime = totalKills + else + kdratio_lifetime = totalKills / totalDeaths + float kdratio_lifetimepvp + if ( totalDeathsPvp == 0.0 ) + kdratio_lifetimepvp = totalKillsPvp + else + kdratio_lifetimepvp = totalKillsPvp / totalDeathsPvp + + // shift stats by 1 to make room for new game data + for ( int i = NUM_GAMES_TRACK_KDRATIO - 2; i >= 0; --i ) + { + player.SetPersistentVar( format( "kdratio_match[%i]", ( i + 1 ) ), player.GetPersistentVar( format("kdratio_match[%i]", i ) ) ) + player.SetPersistentVar( format( "kdratiopvp_match[%i]", ( i + 1 ) ), player.GetPersistentVar( format( "kdratiopvp_match[%i]", i ) ) ) + } + // add new game data + player.SetPersistentVar( "kdratio_match[0]", kdratio_match ) + player.SetPersistentVar( "kdratiopvp_match[0]", kdratiopvp_match ) + player.SetPersistentVar( "kdratio_lifetime", kdratio_lifetime ) + player.SetPersistentVar( "kdratio_lifetime_pvp", kdratio_lifetimepvp ) + } + + // award mvp and top 3 in each team + if ( !IsFFAGame() ) + { + string gamemode = GameRules_GetGameMode() + int functionref( entity, entity ) compareFunc = GameMode_GetScoreCompareFunc( gamemode ) + + for( int team = 0; team < MAX_TEAMS; team++ ) + { + array players = GetPlayerArrayOfTeam( team ) + if ( compareFunc == null ) + { + printt( "gamemode doesn't have a compare func to get the top 3" ) + return + } + players.sort( compareFunc ) + int maxAwards = int( min( players.len(), 3 ) ) + for ( int i = 0; i < maxAwards; i++ ) + { + if ( i == 0 ) + Stats_IncrementStat( players[ i ], "game_stats", "mvp", "", 1.0 ) + Stats_IncrementStat( players[ i ], "game_stats", "top3OnTeam", "", 1.0 ) + } + } + + } +} +void function SetLastPosForDistanceStatValid_Threaded( entity player, bool val ) +{ + WaitFrame() + if ( !IsValid( player ) ) + return + player.p.lastPosForDistanceStatValid = val } -void function UpdateTitanCoreEarnedStat( entity player, entity titan ) +// Respawn did this through stuff found in _entitystructs.gnut (stuff like stats_wallrunTime) +// but their implementation seems kinda bad. The advantage it has over this method is not polling +// every 0.25 seconds, and using movement callbacks and stuff instead. However, since i can't find +// callbacks for things like changing weapon, i would have to poll for that *anyway* and thus, +// there is no point in doing things Respawn's way here +void function HandleDistanceAndTimeStats_Threaded() { + // just to be safe + if ( IsLobby() ) + return + + while ( GetGameState() < eGameState.Playing ) + WaitFrame() + + float lastTickTime = Time() + + while( true ) + { + // track distance stats + foreach ( entity player in GetPlayerArray() ) + { + if ( player.p.lastPosForDistanceStatValid ) + { + // not 100% sure on using Distance2D over Distance tbh + float distInches = Distance2D( player.p.lastPosForDistanceStat, player.GetOrigin() ) + float distMiles = distInches / 63360.0 + + // more generic distance stats + Stats_IncrementStat( player, "distance_stats", "total", "", distMiles ) + if ( player.IsTitan() ) + { + Stats_IncrementStat( player, "distance_stats", "asTitan_" + GetTitanCharacterName( player ), "", distMiles ) + Stats_IncrementStat( player, "distance_stats", "asTitan", "", distMiles ) + } + else + Stats_IncrementStat( player, "distance_stats", "asPilot", "", distMiles ) + + + string state = "" + // specific distance stats + if ( player.IsWallRunning() ) + state = "wallrunning" + else if ( PlayerIsRodeoingTitan( player ) ) + { + if ( player.GetTitanSoulBeingRodeoed().GetTeam() == player.GetTeam() ) + state = "onFriendlyTitan" + else + state = "onEnemyTitan" + } + else if ( player.IsZiplining() ) + state = "ziplining" + else if ( !player.IsOnGround() ) + state = "inAir" + + if ( state != "" ) + Stats_IncrementStat( player, "distance_stats", state, "", distMiles ) + } + + player.p.lastPosForDistanceStat = player.GetOrigin() + } + + float timeSeconds = Time() - lastTickTime + float timeHours = timeSeconds / 3600.0 + + // track time stats + foreach ( entity player in GetPlayerArray() ) + { + // first tick i dont count + if ( timeSeconds == 0 ) + break + + // more generic time stats + Stats_IncrementStat( player, "time_stats", "hours_total", "", timeHours ) + if ( player.IsTitan() ) + { + Stats_IncrementStat( player, "time_stats", "hours_as_titan_" + GetTitanCharacterName( player ), "", timeHours ) + Stats_IncrementStat( player, "time_stats", "hours_as_titan", "", timeHours ) + } + else + Stats_IncrementStat( player, "time_stats", "hours_as_pilot", "", timeHours ) + + string state = "" + // specific time stats + if ( !IsAlive( player ) ) + state = "hours_dead" + else if ( player.IsWallHanging() ) + state = "hours_wallhanging" + else if ( player.IsWallRunning() ) + state = "hours_wallrunning" + else if ( !player.IsOnGround() ) + state = "hours_inAir" + if ( state != "" ) + Stats_IncrementStat( player, "time_stats", state, "", timeHours ) + + // weapon time stats + entity activeWeapon = player.GetActiveWeapon() + if ( IsValid( activeWeapon ) ) + { + if ( IsValidStatItemString( activeWeapon.GetWeaponClassName() ) ) + Stats_IncrementStat( player, "weapon_stats", "hoursUsed", activeWeapon.GetWeaponClassName(), timeHours ) + + foreach( entity weapon in player.GetMainWeapons() ) + { + if ( IsValidStatItemString( weapon.GetWeaponClassName() ) ) + Stats_IncrementStat( player, "weapon_stats", "hoursEquipped", weapon.GetWeaponClassName(), timeHours ) + } + } + + // map time stats + Stats_IncrementStat( player, "game_stats", "hoursPlayed", "", timeHours ) + } + + lastTickTime = Time() + // not rly worth doing this every frame, just a couple of times per second should be fine + wait 0.25 + } +} + +// this is kinda shit +void function SaveStatsPeriodically_Threaded() +{ + while( true ) + { + foreach( entity player in GetPlayerArray() ) + Stats_SaveAllStats( player ) + wait 5 + } } -void function PreScoreEventUpdateStats(entity attacker, entity ent) +bool function IsValidGamemodeString( string mode ) { + int gameModeCount = PersistenceGetEnumCount( "gameModes" ) + for ( int modeIndex = 0; modeIndex < gameModeCount; modeIndex++ ) + { + string gameModeName = PersistenceGetEnumItemNameForIndex( "gameModes", modeIndex ) + + if ( gameModeName == mode ) + return true + } + return false } -void function PostScoreEventUpdateStats(entity attacker, entity ent) +bool function IsValidStatItemString( string item ) { + foreach( str in shGlobalMP.statsItemsList ) + { + if ( str == item ) + return true + } + return false } -void function Stats_OnPlayerDidDamage(entity player, var damageInfo) +string function GetPersistenceRefFromDamageInfo( var damageInfo ) { + string damageSourceString = DamageSourceIDToString( DamageInfo_GetDamageSourceIdentifier( damageInfo ) ) + + foreach( str in shGlobalMP.statsItemsList ) + { + if ( str == damageSourceString ) + return damageSourceString + } + return "" +} + +bool function DamageIsTitanMelee( int damageSourceId ) +{ + switch( damageSourceId ) + { + case eDamageSourceId.melee_titan_punch: + case eDamageSourceId.melee_titan_punch_ion: + case eDamageSourceId.melee_titan_punch_legion: + case eDamageSourceId.melee_titan_punch_tone: + case eDamageSourceId.melee_titan_punch_scorch: + case eDamageSourceId.melee_titan_punch_northstar: + case eDamageSourceId.melee_titan_punch_fighter: + case eDamageSourceId.melee_titan_sword: + case eDamageSourceId.melee_titan_sword_aoe: + return true + default: + return false + } + unreachable } -- cgit v1.2.3