420 lines
17 KiB
Plaintext
420 lines
17 KiB
Plaintext
@using SharedLibraryCore.Configuration
|
|
@using Data.Models.Client.Stats
|
|
@using Stats.Helpers
|
|
@using Data.Models.Client
|
|
@using Data.Models.Client.Stats.Reference
|
|
@using Humanizer
|
|
@using Humanizer.Localisation
|
|
@using IW4MAdmin.Plugins.Stats
|
|
@using WebfrontCore.ViewModels
|
|
@model Stats.Dtos.AdvancedStatsInfo
|
|
|
|
@{
|
|
ViewBag.Title = "Advanced Client Statistics";
|
|
ViewBag.Description = Model.ClientName.StripColors();
|
|
|
|
const string headshotKey = "MOD_HEAD_SHOT";
|
|
const string headshotKey2 = "headshot";
|
|
const string meleeKey = "MOD_MELEE";
|
|
|
|
var suicideKeys = new[] { "MOD_SUICIDE", "MOD_FALLING" };
|
|
// if they've not copied default settings config this could be null
|
|
var config = (GameStringConfiguration)ViewBag.Config ?? new GameStringConfiguration();
|
|
|
|
string GetWeaponNameForHit(EFClientHitStatistic stat)
|
|
{
|
|
if (stat == null)
|
|
{
|
|
return null;
|
|
}
|
|
var rebuiltName = stat.RebuildWeaponName();
|
|
var name = config.GetStringForGame(rebuiltName, stat.Weapon?.Game);
|
|
return !rebuiltName.Equals(name, StringComparison.InvariantCultureIgnoreCase)
|
|
? name
|
|
: config.GetStringForGame(stat.Weapon.Name, stat.Weapon.Game);
|
|
}
|
|
|
|
string GetWeaponAttachmentName(EFWeaponAttachmentCombo attachment)
|
|
{
|
|
if (attachment == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var attachmentText = string.Join(" + ", new[]
|
|
{
|
|
config.GetStringForGame(attachment.Attachment1.Name, attachment.Attachment1.Game),
|
|
config.GetStringForGame(attachment.Attachment2?.Name, attachment.Attachment2?.Game),
|
|
config.GetStringForGame(attachment.Attachment3?.Name, attachment.Attachment3?.Game)
|
|
}.Where(attach => !string.IsNullOrWhiteSpace(attach)));
|
|
|
|
return attachmentText;
|
|
}
|
|
|
|
var weapons = Model.ByWeapon
|
|
.Where(hit => hit.DamageInflicted > 0 || (hit.DamageInflicted == 0 && hit.HitCount > 0))
|
|
.GroupBy(hit => new { hit.WeaponId })
|
|
.Select(group =>
|
|
{
|
|
var withoutAttachments = group.FirstOrDefault(hit => hit.WeaponAttachmentComboId == null);
|
|
var mostUsedAttachment = group.Except(new[] { withoutAttachments })
|
|
.OrderByDescending(g => g.DamageInflicted)
|
|
.GroupBy(g => g.WeaponAttachmentComboId)
|
|
.FirstOrDefault()
|
|
?.FirstOrDefault();
|
|
|
|
if (withoutAttachments == null || mostUsedAttachment == null)
|
|
{
|
|
return withoutAttachments;
|
|
}
|
|
|
|
withoutAttachments.WeaponAttachmentComboId = mostUsedAttachment.WeaponAttachmentComboId;
|
|
withoutAttachments.WeaponAttachmentCombo = mostUsedAttachment.WeaponAttachmentCombo;
|
|
|
|
return withoutAttachments;
|
|
})
|
|
.Where(hit => hit != null)
|
|
.OrderByDescending(hit => hit.KillCount)
|
|
.ToList();
|
|
|
|
var allPerServer = Model.All.Where(hit => hit.ServerId == Model.ServerId).ToList();
|
|
|
|
// if the serverId is supplied we want all the entries with serverID but nothing else
|
|
var aggregate = Model.ServerId == null
|
|
? Model.Aggregate
|
|
: allPerServer.Where(hit => hit.WeaponId == null)
|
|
.Where(hit => hit.HitLocation == null)
|
|
.Where(hit => hit.ServerId == Model.ServerId)
|
|
.Where(hit => hit.WeaponAttachmentComboId == null)
|
|
.FirstOrDefault(hit => hit.MeansOfDeathId == null);
|
|
|
|
var filteredHitLocations = Model.ByHitLocation
|
|
.Where(hit => hit.HitCount > 0)
|
|
.Where(hit => hit.HitLocation.Name != "none")
|
|
.Where(hit => hit.HitLocation.Name != "neck")
|
|
.Where(hit => hit.ServerId == Model.ServerId)
|
|
.OrderByDescending(hit => hit.HitCount)
|
|
.ThenBy(hit => hit.HitLocationId)
|
|
.ToList();
|
|
|
|
var uniqueWeapons = allPerServer.Any()
|
|
? Model.ByWeapon.Where(hit => hit.ServerId == Model.ServerId)
|
|
.Where(weapon => weapon.DamageInflicted > 0)
|
|
.GroupBy(weapon => weapon.WeaponId)
|
|
.Count()
|
|
: (int?)null; // want to default to -- in ui instead of 0
|
|
|
|
var activeTime = weapons.Any()
|
|
? TimeSpan.FromSeconds(weapons.Sum(weapon => weapon.UsageSeconds ?? 0))
|
|
: (TimeSpan?)null; // want to default to -- in ui instead of 0
|
|
|
|
var kdr = aggregate == null
|
|
? null
|
|
: Math.Round(aggregate.KillCount / (float)aggregate.DeathCount, 2).ToString(Utilities.CurrentLocalization.Culture);
|
|
|
|
var serverLegacyStat = Model.LegacyStats
|
|
.FirstOrDefault(stat => stat.ServerId == Model.ServerId);
|
|
|
|
// legacy stats section
|
|
var performance = Model.Performance;
|
|
var skill = Model.ServerId != null ? serverLegacyStat?.Skill.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.Skill), 0).ToNumericalString();
|
|
var elo = Model.ServerId != null ? serverLegacyStat?.EloRating.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.EloRating), 0).ToNumericalString();
|
|
var spm = Model.ServerId != null ? serverLegacyStat?.SPM.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.SPM), 0).ToNumericalString();
|
|
|
|
var performanceHistory = Model.Ratings
|
|
.Select(rating => rating.PerformanceMetric);
|
|
|
|
if (performance != null)
|
|
{
|
|
performanceHistory = performanceHistory.Append(performance.Value);
|
|
}
|
|
|
|
var score = allPerServer.Any()
|
|
? allPerServer.Sum(stat => stat.Score)
|
|
: null;
|
|
|
|
var headShots = allPerServer.Any()
|
|
? allPerServer.Where(hit => hit.MeansOfDeath?.Name == headshotKey || hit.HitLocation?.Name == headshotKey2).Sum(hit => hit.HitCount)
|
|
: (int?)null; // want to default to -- in ui instead of 0
|
|
|
|
var meleeKills = allPerServer.Any()
|
|
? allPerServer.Where(hit => hit.MeansOfDeath?.Name == meleeKey).Sum(hit => hit.KillCount)
|
|
: (int?)null;
|
|
|
|
var suicides = allPerServer.Any()
|
|
? allPerServer.Where(hit => suicideKeys.Contains(hit.MeansOfDeath?.Name ?? "")).Sum(hit => hit.KillCount)
|
|
: (int?)null;
|
|
|
|
var statCards = new[]
|
|
{
|
|
new
|
|
{
|
|
Name = (ViewBag.Localization["PLUGINS_STATS_TEXT_KILLS"] as string).Titleize(),
|
|
Value = aggregate?.KillCount.ToNumericalString()
|
|
},
|
|
new
|
|
{
|
|
Name = (ViewBag.Localization["PLUGINS_STATS_TEXT_DEATHS"] as string).Titleize(),
|
|
Value = aggregate?.DeathCount.ToNumericalString()
|
|
},
|
|
new
|
|
{
|
|
Name = (ViewBag.Localization["PLUGINS_STATS_TEXT_KDR"] as string).Titleize(),
|
|
Value = kdr
|
|
},
|
|
new
|
|
{
|
|
Name = (ViewBag.Localization["WEBFRONT_ADV_STATS_SCORE"] as string).Titleize(),
|
|
Value = score.ToNumericalString()
|
|
},
|
|
new
|
|
{
|
|
Name = (ViewBag.Localization["WEBFRONT_ADV_STATS_ZSCORE"] as string),
|
|
Value = Model.ZScore.ToNumericalString(2)
|
|
},
|
|
new
|
|
{
|
|
Name = (ViewBag.Localization["PLUGINS_STATS_TEXT_SKILL"] as string).ToLower().Titleize(),
|
|
Value = skill
|
|
},
|
|
new
|
|
{
|
|
Name = (ViewBag.Localization["WEBFRONT_ADV_STATS_ELO"] as string).Titleize(),
|
|
Value = elo
|
|
},
|
|
new
|
|
{
|
|
Name = (ViewBag.Localization["PLUGINS_STATS_META_SPM"] as string).Titleize(),
|
|
Value = spm
|
|
},
|
|
new
|
|
{
|
|
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_TOTAL_DAMAGE"] as string,
|
|
Value = aggregate?.DamageInflicted.ToNumericalString()
|
|
},
|
|
new
|
|
{
|
|
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_SUICIDES"] as string,
|
|
Value = suicides.ToNumericalString()
|
|
},
|
|
new
|
|
{
|
|
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_HEADSHOTS"] as string,
|
|
Value = headShots.ToNumericalString()
|
|
},
|
|
new
|
|
{
|
|
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_MELEES"] as string,
|
|
Value = meleeKills.ToNumericalString()
|
|
},
|
|
new
|
|
{
|
|
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_WEAP"] as string,
|
|
Value = GetWeaponNameForHit(weapons.FirstOrDefault())
|
|
},
|
|
new
|
|
{
|
|
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_ATTACHMENTS"] as string,
|
|
Value = GetWeaponAttachmentName(weapons.FirstOrDefault()?.WeaponAttachmentCombo)
|
|
},
|
|
new
|
|
{
|
|
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_TOTAL_WEAPONS_USED"] as string,
|
|
Value = uniqueWeapons.ToNumericalString()
|
|
},
|
|
new
|
|
{
|
|
Name = ViewBag.Localization["WEBFRONT_ADV_STATS_TOTAL_ACTIVE_TIME"] as string,
|
|
Value = activeTime?.HumanizeForCurrentCulture()
|
|
}
|
|
};
|
|
}
|
|
|
|
<div class="content row mt-20">
|
|
<!-- main content -->
|
|
<div class="col-12 col-lg-9 col-xl-10 mt-0">
|
|
<h2 class="content-title mb-0">Player Stats</h2>
|
|
<span class="text-muted">
|
|
<color-code value="@(Model.Servers.FirstOrDefault(server => server.Endpoint == Model.ServerEndpoint)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
|
|
</span>
|
|
|
|
<!-- top card -->
|
|
<div class="card p-20 m-0 mt-15 mb-15">
|
|
<div class="align-self-center d-flex flex-column flex-lg-row flex-fill mb-15">
|
|
<!-- rank icon -->
|
|
<img class="img-fluid align-self-center w-75" id="rank_icon" src="~/images/stats/ranks/rank_@(Model.ZScore.RankIconIndexForZScore()).png" alt="@performance"/>
|
|
<!-- summary -->
|
|
<div class="d-flex flex-column align-self-center m-10 text-center text-lg-left" id="client_stats_summary">
|
|
<div class="font-size-20 mb-0 font-weight-bold">
|
|
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.ClientId" class="no-decoration">@Model.ClientName.StripColors()</a>
|
|
</div>
|
|
@if (Model.Level == EFClient.Permission.Banned)
|
|
{
|
|
<div class="h5 mb-0 text-danger">@ViewBag.Localization["GLOBAL_PERMISSION_BANNED"]</div>
|
|
}
|
|
else if (Model.ZScore != null)
|
|
{
|
|
if (Model.Ranking > 0)
|
|
{
|
|
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED_V2"] as string).FormatExt(Model.Ranking?.ToString("#,##0"), Model.TotalRankedClients.ToString("#,##0")))</div>
|
|
}
|
|
|
|
else
|
|
{
|
|
<div class="h5 mb-0">@ViewBag.Localization["WEBFRONT_ADV_STATS_EXPIRED"]</div>
|
|
}
|
|
if (Model.ServerId != null)
|
|
{
|
|
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_PERFORMANCE"] as string).FormatExt($"<span class=\"text-primary\">{performance.ToNumericalString()}</span>"))</div>
|
|
}
|
|
|
|
else
|
|
{
|
|
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RATING"] as string).FormatExt($"<span class=\"text-primary\">{Model.Rating.ToNumericalString()}</span>"))</div>
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
<div class="h5 mb-0">@ViewBag.Localization["WEBFRONT_STATS_INDEX_UNRANKED"]</div>
|
|
}
|
|
</div>
|
|
<!-- history graph -->
|
|
@if (performanceHistory.Count() > 5)
|
|
{
|
|
<div class="w-half m-auto ml-lg-auto " id="client_performance_history_container">
|
|
<canvas id="client_performance_history" data-history="@Html.Raw(Json.Serialize(performanceHistory))"></canvas>
|
|
</div>
|
|
}
|
|
</div>
|
|
<hr class="m-10"/>
|
|
<div class="d-flex flex-row flex-wrap rounded">
|
|
@foreach (var card in statCards)
|
|
{
|
|
<div class="stat-card bg-very-dark-dm bg-light-ex-lm p-15 m-md-5 w-half w-md-200 rounded flex-fill">
|
|
@if (string.IsNullOrWhiteSpace(card.Value))
|
|
{
|
|
<div class="m-0">—</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="m-0 font-size-16 text-primary">@card.Value</div>
|
|
}
|
|
<div class="font-size-12 text-muted">@card.Name</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap flex-column-reverse flex-xl-row">
|
|
<!-- hit locations -->
|
|
@{
|
|
var totalHits = filteredHitLocations.Sum(hit => hit.HitCount);
|
|
|
|
var hitLocationsTable = new TableInfo(5)
|
|
{
|
|
Header = ViewBag.Localization["WEBFRONT_ADV_STATS_HIT_LOCATIONS"]
|
|
};
|
|
|
|
hitLocationsTable.WithColumns(new string[]
|
|
{
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_LOCATION"],
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"],
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_PERCENTAGE"],
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"],
|
|
}).WithRows(filteredHitLocations, hitLocation => new[]
|
|
{
|
|
config.GetStringForGame(hitLocation.HitLocation.Name, hitLocation.HitLocation.Game),
|
|
hitLocation.HitCount.ToString(),
|
|
$"{Math.Round((hitLocation.HitCount / (float)totalHits) * 100.0).ToString(Utilities.CurrentLocalization.Culture)}%",
|
|
hitLocation.DamageInflicted.ToNumericalString()
|
|
});
|
|
}
|
|
|
|
<div class="mr-0 mr-xl-20 flex-fill flex-xl-grow-1">
|
|
<partial name="_DataTable" for="@hitLocationsTable"></partial>
|
|
|
|
<div class="h-250 p-15 card m-0 d-flex justify-content-center rounded-bottom" id="hitlocation_container">
|
|
<canvas id="hitlocation_model">
|
|
</canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- weapons used -->
|
|
@{
|
|
var weaponsUsedTable = new TableInfo(10)
|
|
{
|
|
Header = ViewBag.Localization["WEBFRONT_ADV_STATS_WEAP_USAGE"]
|
|
};
|
|
|
|
weaponsUsedTable.WithColumns(new string[]
|
|
{
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_WEAPON"],
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_ATTACHMENTS"],
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"],
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"],
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"],
|
|
ViewBag.Localization["WEBFRONT_ADV_STATS_USAGE"]
|
|
}).WithRows(weapons, weapon => new[]
|
|
{
|
|
GetWeaponNameForHit(weapon),
|
|
GetWeaponAttachmentName(weapon.WeaponAttachmentCombo) ?? "--",
|
|
weapon.KillCount.ToNumericalString(),
|
|
weapon.HitCount.ToNumericalString(),
|
|
weapon.DamageInflicted.ToNumericalString(),
|
|
TimeSpan.FromSeconds(weapon.UsageSeconds ?? 0).HumanizeForCurrentCulture(minUnit: TimeUnit.Second)
|
|
});
|
|
}
|
|
|
|
<div class="flex-fill flex-xl-grow-1">
|
|
<partial name="_DataTable" for="@weaponsUsedTable"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- side context menu -->
|
|
@{
|
|
var menuItems = new SideContextMenuItems
|
|
{
|
|
MenuTitle = "Game", Items = Model.Servers.Select(server => new SideContextMenuItem
|
|
{
|
|
IsLink = true,
|
|
Reference = Url.Action("Advanced", "ClientStatistics", new { serverId = server.Endpoint }),
|
|
Title = server.Name.StripColors(),
|
|
IsActive = Model.ServerEndpoint == server.Endpoint
|
|
}).Prepend(new SideContextMenuItem
|
|
{
|
|
IsLink = true,
|
|
Reference = Url.Action("Advanced", "ClientStatistics"),
|
|
Title = ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"],
|
|
IsActive = Model.ServerEndpoint is null
|
|
}).ToList()
|
|
};
|
|
}
|
|
<partial name="_SideContextMenu" for="@menuItems"></partial>
|
|
</div>
|
|
|
|
|
|
@{
|
|
var projection = filteredHitLocations.Select(loc => new
|
|
{
|
|
name = loc.HitLocation.Name,
|
|
// we want to count head and neck as the same
|
|
percentage = (loc.HitLocation.Name == "head"
|
|
? filteredHitLocations.FirstOrDefault(c => c.HitLocation.Name == "neck")?.HitCount ?? 0 + loc.HitCount
|
|
: loc.HitCount) / (float)totalHits
|
|
}).ToList();
|
|
var maxPercentage = projection.Any() ? projection.Max(p => p.percentage) : 0;
|
|
}
|
|
|
|
@section scripts
|
|
{
|
|
<script type="text/javascript">
|
|
const hitLocationData = @Html.Raw(Json.Serialize(projection));
|
|
const maxPercentage = @maxPercentage;
|
|
</script>
|
|
|
|
<environment include="Development">
|
|
<script type="text/javascript" src="~/js/advanced_stats.js"></script>
|
|
</environment>
|
|
}
|