huge commit for advanced stats feature.

broke data out into its own library.
may be breaking changes with existing plugins
This commit is contained in:
RaidMax
2021-03-22 11:09:25 -05:00
parent db2e1deb2f
commit c5375b661b
505 changed files with 13671 additions and 3271 deletions

View File

@ -1,6 +1,6 @@
@model IList<SharedLibraryCore.Dtos.PlayerInfo>
@{
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
var loc = Utilities.CurrentLocalization.LocalizationIndex;
}
<div class="row d-none d-lg-block ">

View File

@ -0,0 +1,39 @@
@using SharedLibraryCore.Dtos.Meta.Responses
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<MessageResponse>
@if (ViewBag.Error != null)
{
<h4 class="text-red">@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_INVALID_QUERY"], ViewBag.Error.Message)</h4>
}
else
{
<h4 class="pb-3 text-center">@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], Model.TotalResultCount.ToString("N0"))</h4>
<table class="table table-striped table-hover">
<thead class="d-none d-lg-table-header-group">
<tr class="bg-primary pt-2 pb-2">
<th scope="col">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
<th scope="col">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
<th scope="col">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
<th scope="col" class="text-right">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
</tr>
</thead>
<tbody id="message_table_body" class="border-bottom bg-dark">
<partial name="Message/_Item" model="@Model.Results" />
</tbody>
</table>
<span id="load_more_messages_button" class="loader-load-more oi oi-chevron-bottom text-center text-primary w-100 h3 pb-0 mb-0 d-none d-lg-block"></span>
@section scripts {
<environment include="Development">
<script type="text/javascript" src="~/js/loader.js"></script>
</environment>
<script>
$(document).ready(function () {
initLoader('/Message/FindNext?query=@ViewBag.Query', '#message_table_body', @Model.RetrievedResultCount, @ViewBag.QueryLimit);
});
</script>
}
}

View File

@ -0,0 +1,68 @@
@using SharedLibraryCore.Dtos.Meta.Responses
@model IEnumerable<MessageResponse>
@foreach (var message in Model)
{
<!-- desktop -->
<tr class="d-none d-lg-table-row">
<td>
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@message.ClientId" class="link-inverse">
<color-code value="@message.ClientName" allow="@ViewBag.EnableColorCodes"></color-code>
</a>
</td>
<td class="text-light w-50 text-break">
@if (message.IsHidden && !ViewBag.Authorized)
{
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
}
else
{
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
}
</td>
<td class="text-light">
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
</td>
<td class="text-right text-light">
@message.When
</td>
</tr>
<!-- mobile -->
<tr class="d-table-row d-lg-none bg-dark">
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
<td class="text-light">
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@message.ClientId" class="link-inverse">
<color-code value="@message.ClientName" allow="@ViewBag.EnableColorCodes"></color-code>
</a>
</td>
</tr>
<tr class="d-table-row d-lg-none bg-dark">
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
<td class="text-light">
@if (message.IsHidden && !ViewBag.Authorized)
{
<color-code value="@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_CLIENT_META_CHAT_HIDDEN"], message.HiddenMessage)" allow="@ViewBag.EnableColorCodes"></color-code>
}
else
{
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
}
</td>
</tr>
<tr class="d-table-row d-lg-none bg-dark">
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
<td class="text-light">
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
</td>
</tr>
<tr class="d-table-row d-lg-none bg-dark">
<th scope="row" class="bg-primary" style="border-bottom: 1px solid #222">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
<td class="text-light mb-2 border-bottom">
@message.When
</td>
</tr>
}

View File

@ -1,17 +1,16 @@
@using SharedLibraryCore.Database.Models
@using SharedLibraryCore.Interfaces
@using SharedLibraryCore
@using SharedLibraryCore.Interfaces
@using Data.Models
@model SharedLibraryCore.Dtos.PlayerInfo
@{
string match = System.Text.RegularExpressions.Regex.Match(Model.Name.ToUpper(), "[A-Z]").Value;
string shortCode = match == string.Empty ? "?" : match;
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
string gravatarUrl = Model.Meta.FirstOrDefault(m => m.Key == "GravatarEmail")?.Value;
bool isFlagged = Model.LevelInt == (int)SharedLibraryCore.Database.Models.EFClient.Permission.Flagged;
bool isPermBanned = Model.LevelInt == (int)SharedLibraryCore.Database.Models.EFClient.Permission.Banned;
bool isFlagged = Model.LevelInt == (int) SharedLibraryCore.Database.Models.EFClient.Permission.Flagged;
bool isPermBanned = Model.LevelInt == (int) SharedLibraryCore.Database.Models.EFClient.Permission.Banned;
bool isTempBanned = Model.ActivePenalty?.Type == EFPenalty.PenaltyType.TempBan;
string translationKey = $"WEBFRONT_PROFILE_{Model.ActivePenalty?.Type.ToString().ToUpper()}_INFO";
var ignoredMetaTypes = new[] { MetaType.Information, MetaType.Other, MetaType.QuickMessage };
var ignoredMetaTypes = new[] {MetaType.Information, MetaType.Other, MetaType.QuickMessage};
}
<div id="profile_wrapper" class="pb-3 row d-flex flex-column flex-lg-row">
@ -25,7 +24,9 @@
<!-- Name/Level Column -->
<div class="w-75 d-block d-lg-inline-flex flex-column flex-fill text-center text-lg-left pb-3 pb-lg-0 pt-3 pt-lg-0 pl-3 pr-3 ml-auto mr-auto" style="overflow-wrap: anywhere">
<div class="mt-n2 flex-fill d-block d-lg-inline-flex">
<div id="profile_name" class="client-name h1 mb-0"><color-code value="@Model.Name" allow="@ViewBag.EnableColorCodes"></color-code></div>
<div id="profile_name" class="client-name h1 mb-0">
<color-code value="@Model.Name" allow="@ViewBag.EnableColorCodes"></color-code>
</div>
@if (ViewBag.Authorized)
{
<div id="profile_aliases_btn" class="oi oi-caret-bottom h3 ml-0 ml-lg-2 mb-0 pt-lg-2 mt-lg-1"></div>
@ -37,16 +38,18 @@
<div id="profile_aliases" class="text-muted pt-0 pt-lg-2 pb-2">
@foreach (var linked in Model.LinkedAccounts)
{
@Html.ActionLink(linked.Value.ToString("X"), "ProfileAsync", "Client", new { id = linked.Key }, new { @class = "link-inverse" })<br />
@Html.ActionLink(linked.Value.ToString("X"), "ProfileAsync", "Client", new {id = linked.Key}, new {@class = "link-inverse"})<br/>
}
@foreach (string alias in Model.Aliases)
@foreach (var alias in Model.Aliases)
{
<color-code value="@alias" allow="@ViewBag.EnableColorCodes"></color-code><br />
<color-code value="@alias" allow="@ViewBag.EnableColorCodes"></color-code>
<br/>
}
@foreach (string ip in Model.IPs)
{
<a class="ip-locate-link" href="#" data-ip="@ip">@ip</a><br />
<a class="ip-locate-link" href="#" data-ip="@ip">@ip</a>
<br/>
}
</div>
}
@ -62,7 +65,7 @@
break;
case "time":
<span class="text-white font-weight-lighter">
@Utilities.HumanizeForCurrentCulture(Model.ActivePenalty.Expires.Value - DateTime.UtcNow)
@((Model.ActivePenalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture())
</span>
break;
default:
@ -89,20 +92,21 @@
}
}
</div>
@if (ViewBag.Authorized)
{
<div class="pr-lg-0 text-center text-lg-right">
<div class="pr-lg-0 text-center text-lg-right">
@if (ViewBag.Authorized)
{
@if (!isPermBanned)
{
<div class="profile-action oi oi-flag h3 ml-2 @(isFlagged ? "text-secondary" : "text-success")" data-action="@(isFlagged ? "unflag" : "flag")" aria-hidden="true"></div>
}
@if (Model.LevelInt < (int)ViewBag.User.Level && !Model.HasActivePenalty)
@if (Model.LevelInt < (int) ViewBag.User.Level && !Model.HasActivePenalty)
{
<div id="profile_action_ban_btn" class="profile-action oi oi-lock-unlocked text-success h3 ml-2" title="Ban Client" data-action="ban" aria-hidden="true"></div>
}
@if (Model.LevelInt < (int)ViewBag.User.Level && Model.HasActivePenalty)
@if (Model.LevelInt < (int) ViewBag.User.Level && Model.HasActivePenalty)
{
@if (isTempBanned)
{
@ -120,12 +124,16 @@
{
<div id="profile_action_edit_btn" class="profile-action oi oi-cog text-muted h3 ml-2" title="Client Options" data-action="edit" aria-hidden="true"></div>
}
</div>
}
}
@if (ViewBag.UseNewStats)
{
<a asp-controller="ClientStatistics" asp-action="Advanced" asp-route-id="@Model.ClientId" class="oi oi-graph text-primary h3 ml-2" title="Stats" aria-hidden="true"></a>
}
</div>
</div>
<div id="profile_info" class="row d-block d-lg-flex flex-row border-bottom border-top pt-2 pb-2">
<partial name="Meta/_Information.cshtml" model="@Model.Meta" />
<partial name="Meta/_Information.cshtml" model="@Model.Meta"/>
</div>
<div class="row border-bottom">
@ -136,7 +144,9 @@
<div class="d-none d-md-flex flex-fill" id="filter_meta_container">
<a asp-action="ProfileAsync" asp-controller="Client"
class="nav-link p-2 pl-3 pr-3 text-center col-12 col-md-auto text-md-left @(!Model.MetaFilterType.HasValue ? "btn-primary text-white" : "text-muted")"
asp-route-id="@Model.ClientId">@ViewBag.Localization["META_TYPE_ALL_NAME"]</a>
asp-route-id="@Model.ClientId">
@ViewBag.Localization["META_TYPE_ALL_NAME"]
</a>
@foreach (MetaType type in Enum.GetValues(typeof(MetaType)))
{
@ -146,7 +156,9 @@
class="nav-link p-2 pl-3 pr-3 text-center col-12 col-md-auto text-md-left @(Model.MetaFilterType.HasValue && Model.MetaFilterType.Value.ToString() == type.ToString() ? "btn-primary text-white" : "text-muted")"
asp-route-id="@Model.ClientId"
asp-route-metaFilterType="@type"
data-meta-type="@type">@type.ToTranslatedName()</a>
data-meta-type="@type">
@type.ToTranslatedName()
</a>
}
}
</div>
@ -166,7 +178,7 @@
</div>
@section targetid {
<input type="hidden" name="targetId" value="@Model.ClientId" />
<input type="hidden" name="targetId" value="@Model.ClientId"/>
}
@section scripts {
@ -175,4 +187,4 @@
<script type="text/javascript" src="~/js/profile.js"></script>
</environment>
<script>initLoader('/Client/Meta/@Model.ClientId', '#profile_events', 30, 30, [{ 'name': 'metaFilterType', 'value': '@Model.MetaFilterType' }]);</script>
}
}

View File

@ -26,7 +26,7 @@
else if (match.MatchValue == "reason")
{
<span class="text-white">
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Warning)
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning)
{
<span>@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense)</span>
<span class="oi oi-list-rich align-top text-primary automated-penalty-info-detailed" data-penalty-id="@Model.PenaltyId" style="margin-top: 0.125rem;" title="@ViewBag.Localization["WEBFRONT_CLIENT_META_AC_METRIC"]"></span>

View File

@ -1,5 +1,4 @@
@using SharedLibraryCore.Dtos.Meta.Responses
@using SharedLibraryCore
@model ReceivedPenaltyResponse
@{
@ -28,7 +27,7 @@
else if (match.MatchValue == "reason")
{
<span class="text-white">
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Warning && Model.PenaltyType != SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Kick)
@if (ViewBag.Authorized && !string.IsNullOrEmpty(Model.AutomatedOffense) && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Warning && Model.PenaltyType != Data.Models.EFPenalty.PenaltyType.Kick)
{
<span>@Utilities.FormatExt(ViewBag.Localization["WEBFRONT_PROFILE_ANTICHEAT_DETECTION"], Model.AutomatedOffense)</span>
<span class="oi oi-list-rich align-top text-primary automated-penalty-info-detailed" data-penalty-id="@Model.PenaltyId" style="margin-top: 0.125rem;" title="@ViewBag.Localization["WEBFRONT_CLIENT_META_AC_METRIC"]"></span>

View File

@ -0,0 +1,457 @@
@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
@model Stats.Dtos.AdvancedStatsInfo
@{
ViewBag.Title = "Advanced Client Statistics";
ViewBag.Description = Model.ClientName;
const int maxItems = 5;
const string headshotKey = "MOD_HEAD_SHOT";
const string meleeKey = "MOD_MELEE";
var suicideKeys = new[] {"MOD_SUICIDE", "MOD_FALLING"};
var config = (GameStringConfiguration) ViewBag.Config;
var headerClass = Model.Level == EFClient.Permission.Banned ? "bg-danger" : "bg-primary";
var textClass = Model.Level == EFClient.Permission.Banned ? "text-danger" : "text-primary";
var borderBottomClass = Model.Level == EFClient.Permission.Banned ? "border-bottom-danger border-top-danger" : "border-bottom border-top";
var borderClass = Model.Level == EFClient.Permission.Banned ? "border-danger" : "border-primary";
var buttonClass = Model.Level == EFClient.Permission.Banned ? "btn-danger" : "btn-primary";
string GetWeaponNameForHit(EFClientHitStatistic stat)
{
if (stat == null)
{
return null;
}
var rebuiltName = stat.RebuildWeaponName();
var name = config.GetStringForGame(rebuiltName);
return !rebuiltName.Equals(name, StringComparison.InvariantCultureIgnoreCase)
? name
: config.GetStringForGame(stat.Weapon.Name);
}
string GetWeaponAttachmentName(EFWeaponAttachmentCombo attachment)
{
if (attachment == null)
{
return null;
}
var attachmentText = string.Join('+', new[]
{
config.GetStringForGame(attachment.Attachment1.Name),
config.GetStringForGame(attachment.Attachment2?.Name),
config.GetStringForGame(attachment.Attachment3?.Name)
}.Where(attach => !string.IsNullOrWhiteSpace(attach)));
return attachmentText;
}
var weapons = Model.ByWeapon
.Where(hit => hit.DamageInflicted > 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).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="w-100 @headerClass mb-1">
<select class="w-100 @headerClass text-white pl-4 pr-4 pt-2 pb-2 m-auto h5 @borderClass"
id="server_selector"
onchange="if (this.value) window.location.href=this.value">
@if (Model.ServerId == null)
{
<option value="@Url.Action("Advanced", "ClientStatistics")" selected>@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</option>
}
else
{
<option value="@Url.Action("Advanced", "ClientStatistics")">@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</option>
}
@foreach (var server in Model.Servers)
{
if (server.Endpoint == Model.ServerEndpoint)
{
<option value="@Url.Action("Advanced", "ClientStatistics", new {serverId = server.Endpoint})" selected>@server.Name.StripColors()</option>
}
else
{
<option value="@Url.Action("Advanced", "ClientStatistics", new {serverId = server.Endpoint})">@server.Name.StripColors()</option>
}
}
</select>
</div>
<div class="@headerClass p-4 mb-0 d-flex flex-wrap">
<div class="align-self-center d-flex flex-column flex-lg-row text-center text-lg-left mb-3 mb-md-0 p-2 ml-lg-0 mr-lg-0 ml-auto mr-auto">
<div class="mr-lg-3 m-auto">
<img class="img-fluid align-self-center" id="rank_icon" src="~/images/stats/ranks/rank_@(Model.ZScore.RankIconIndexForZScore()).png" alt="@performance"/>
</div>
<div class="d-flex flex-column align-self-center" id="client_stats_summary">
<div class="h1 mb-0 font-weight-bold">
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@Model.ClientId">@Model.ClientName.StripColors()</a>
</div>
@if (Model.Level == EFClient.Permission.Banned)
{
<div class="h5 mb-0">@ViewBag.Localization["GLOBAL_PERMISSION_BANNED"]</div>
}
else if (Model.ZScore != null)
{
if (Model.ServerId != null)
{
<div class="h5 mb-0">@((ViewBag.Localization["WEBFRONT_ADV_STATS_PERFORMANCE"] as string).FormatExt(performance.ToNumericalString()))</div>
}
else
{
<div class="h5 mb-0">@((ViewBag.Localization["WEBFRONT_ADV_STATS_RATING"] as string).FormatExt(Model.Rating.ToNumericalString()))</div>
}
if (Model.Ranking > 0)
{
<div class="h5 mb-0">@((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED"] as string).FormatExt(Model.Ranking.ToNumericalString()))</div>
}
else
{
<div class="h5 mb-0">@ViewBag.Localization["WEBFRONT_ADV_STATS_EXPIRED"]</div>
}
}
else
{
<div class="h5 mb-0">@ViewBag.Localization["WEBFRONT_STATS_INDEX_UNRANKED"]</div>
}
</div>
</div>
<div class="w-50 m-auto ml-md-auto mr-md-0" id="client_performance_history_container">
<canvas id="client_performance_history" data-history="@Html.Raw(Json.Serialize(performanceHistory))"></canvas>
</div>
</div>
<div class="mb-4 bg-dark @borderBottomClass d-flex flex-wrap">
@foreach (var card in statCards)
{
<div class="pl-3 pr-4 pb-3 pt-3 stat-card flex-fill w-50 w-md-auto">
@if (string.IsNullOrWhiteSpace(card.Value))
{
<h5 class="card-title @textClass">&mdash;</h5>
}
else
{
<h5 class="card-title @textClass">@card.Value</h5>
}
<h6 class="card-subtitle mb-0 text-muted">@card.Name</h6>
</div>
}
</div>
<div class="row">
<!-- WEAPONS USED -->
<div class="col-12 mb-4">
<div class="@headerClass h4 mb-1 p-2">
<div class="text-center">@ViewBag.Localization["WEBFRONT_ADV_STATS_WEAP_USAGE"]</div>
</div>
<table class="table mb-0">
<tr class="@headerClass">
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_WEAPON"]</th>
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_FAV_ATTACHMENTS"]</th>
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_KILLS"]</th>
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"]</th>
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"]</th>
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_USAGE"]</th>
</tr>
@foreach (var weaponHit in weapons.Take(maxItems))
{
<tr class="bg-dark">
<td class="@textClass text-force-break">@GetWeaponNameForHit(weaponHit)</td>
@{ var attachments = GetWeaponAttachmentName(weaponHit.WeaponAttachmentCombo); }
@if (string.IsNullOrWhiteSpace(attachments))
{
<td class="text-muted text-force-break">&mdash;</td>
}
else
{
<td class="text-muted text-force-break">@attachments</td>
}
<td class="text-success text-force-break">@weaponHit.KillCount.ToNumericalString()</td>
<td class="text-muted text-force-break">@weaponHit.HitCount.ToNumericalString()</td>
<td class="text-muted text-force-break">@weaponHit.DamageInflicted.ToNumericalString()</td>
<td class="text-muted text-force-break">@TimeSpan.FromSeconds(weaponHit.UsageSeconds ?? 0).HumanizeForCurrentCulture(minUnit: TimeUnit.Second)</td>
</tr>
}
<!-- OVERFLOW -->
@foreach (var weaponHit in weapons.Skip(maxItems))
{
<tr class="bg-dark hidden-row" style="display:none">
<td class="@textClass text-force-break">@GetWeaponNameForHit(weaponHit)</td>
@{ var attachments = GetWeaponAttachmentName(weaponHit.WeaponAttachmentCombo); }
@if (string.IsNullOrWhiteSpace(attachments))
{
<td class="text-muted text-force-break">&mdash;</td>
}
else
{
<td class="text-muted text-force-break">@attachments</td>
}
<td class="text-success text-force-break">@weaponHit.KillCount.ToNumericalString()</td>
<td class="text-muted text-force-break">@weaponHit.HitCount.ToNumericalString()</td>
<td class="text-muted text-force-break">@weaponHit.DamageInflicted.ToNumericalString()</td>
<td class="text-muted text-force-break">@TimeSpan.FromSeconds(weaponHit.UsageSeconds ?? 0).HumanizeForCurrentCulture()</td>
</tr>
}
<tr>
</table>
<button class="btn @buttonClass btn-block table-slide">
<span class="oi oi-chevron-bottom"></span>
</button>
</div>
</div>
<div class="row">
<!-- HIT LOCATIONS -->
<div class="col-lg-6 col-12 pr-3 pr-lg-0" id="hit_location_table">
<div class="@headerClass h4 mb-1 p-2">
<div class="text-center">@ViewBag.Localization["WEBFRONT_ADV_STATS_HIT_LOCATIONS"]</div>
</div>
<table class="table @borderBottomClass bg-dark mb-0 pb-0">
<tr class="@headerClass">
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_LOCATION"]</th>
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_HITS"]</th>
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_PERCENTAGE"]</th>
<th class="text-force-break">@ViewBag.Localization["WEBFRONT_ADV_STATS_DAMAGE"]</th>
</tr>
@{
var totalHits = filteredHitLocations.Sum(hit => hit.HitCount);
}
@foreach (var hitLocation in filteredHitLocations.Take(8))
{
<tr>
<td class="@textClass text-force-break">@config.GetStringForGame(hitLocation.HitLocation.Name)</td>
<td class="text-success text-force-break">@hitLocation.HitCount</td>
<td class="text-muted text-force-break">@Math.Round((hitLocation.HitCount / (float) totalHits) * 100.0).ToString(Utilities.CurrentLocalization.Culture)%</td>
<td class="text-muted text-force-break">@hitLocation.DamageInflicted.ToNumericalString()</td>
</tr>
}
@foreach (var hitLocation in filteredHitLocations.Skip(8))
{
<tr class="bg-dark hidden-row" style="display:none;">
<td class="@textClass text-force-break">@config.GetStringForGame(hitLocation.HitLocation.Name)</td>
<td class="text-success text-force-break">@hitLocation.HitCount</td>
<td class="text-muted text-force-break">@Math.Round((hitLocation.HitCount / (float) totalHits) * 100.0).ToString(Utilities.CurrentLocalization.Culture)%</td>
<td class="text-muted text-force-break">@hitLocation.DamageInflicted.ToNumericalString()</td>
</tr>
}
</table>
<button class="btn @buttonClass btn-block table-slide">
<span class="oi oi-chevron-bottom"></span>
</button>
</div>
<div class="col-lg-6 col-12 pl-3 pl-lg-0">
<div class="@borderBottomClass text-center h-100" id="hitlocation_container">
<canvas id="hitlocation_model">
</canvas>
</div>
</div>
</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>
}

View File

@ -0,0 +1,151 @@
@using IW4MAdmin.Plugins.Stats
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
@{
Layout = null;
var loc = Utilities.CurrentLocalization.LocalizationIndex.Set;
double getDeviation(double deviations) => Math.Pow(Math.E, 5.259 + (deviations * 0.812));
string rankIcon(double? elo)
{
if (elo >= getDeviation(-0.75) && elo < getDeviation(1.25))
{
return "0_no-place/menu_div_no_place.png";
}
if (elo >= getDeviation(0.125) && elo < getDeviation(0.625))
{
return "1_iron/menu_div_iron_sub03.png";
}
if (elo >= getDeviation(0.625) && elo < getDeviation(1.0))
{
return "2_bronze/menu_div_bronze_sub03.png";
}
if (elo >= getDeviation(1.0) && elo < getDeviation(1.25))
{
return "3_silver/menu_div_silver_sub03.png";
}
if (elo >= getDeviation(1.25) && elo < getDeviation(1.5))
{
return "4_gold/menu_div_gold_sub03.png";
}
if (elo >= getDeviation(1.5) && elo < getDeviation(1.75))
{
return "5_platinum/menu_div_platinum_sub03.png";
}
if (elo >= getDeviation(1.75) && elo < getDeviation(2.0))
{
return "6_semipro/menu_div_semipro_sub03.png";
}
if (elo >= getDeviation(2.0))
{
return "7_pro/menu_div_pro_sub03.png";
}
return "0_no-place/menu_div_no_place.png";
}
}
@if (Model.Count == 0)
{
<div class="p-2 text-center">@Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_TEXT_NOQUALIFY"]</div>
}
@foreach (var stat in Model)
{
<div class="row ml-0 mr-0 pt-2 pb-2">
@if (ViewBag.UseNewStats)
{
<img class="align-self-center d-block d-md-none m-auto pb-3 pt-3" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/>
}
<div class="col-md-4 text-md-left text-center">
<div class="h2 d-flex flex-row justify-content-center justify-content-md-start align-items-center">
<div class="text-muted">#@stat.Ranking</div>
@if (stat.RatingChange > 0)
{
<div class="d-flex flex-column text-center pl-1">
<div class="oi oi-caret-top text-success client-rating-change-up"></div>
<div class="client-rating-change-amount text-success">@stat.RatingChange</div>
</div>
}
@if (stat.RatingChange < 0)
{
<div class="d-flex flex-column text-center pl-1">
<div class="client-rating-change-amount client-rating-change-amount-down text-danger">@Math.Abs(stat.RatingChange)</div>
<div class="oi oi-caret-bottom text-danger client-rating-change-down"></div>
</div>
}
<span class="text-muted pr-1 pl-1">&ndash;</span>
@if (!ViewBag.UseNewStats)
{
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@stat.ClientId">
<color-code value="@stat.Name" allow="ViewBag.EnableColorCodes"></color-code>
</a>
}
else
{
<a asp-controller="ClientStatistics" asp-action="Advanced" asp-route-id="@stat.ClientId">
<color-code value="@stat.Name" allow="ViewBag.EnableColorCodes"></color-code>
</a>
}
</div>
@if (ViewBag.UseNewStats)
{
<div class="d-flex flex-column">
<div>
<span class="text-primary font-weight-bold h5">
@stat.Performance.ToNumericalString()
</span>
@if (stat.ServerId == null)
{
<span class="text-muted font-weight-bold h5">@loc["WEBFRONT_ADV_STATS_RATING"].FormatExt("").ToLower()</span>
}
else
{
<span class="text-muted font-weight-bold h5">@loc["WEBFRONT_ADV_STATS_PERFORMANCE"].FormatExt("").ToLower()</span>
}
</div>
<div>
<span class="text-primary">@stat.Kills.ToNumericalString()</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
</div>
<div>
<span class="text-primary">@stat.Deaths.ToNumericalString()</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br />
</div>
<div>
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
</div>
<div>
<span class="text-primary">@stat.TimePlayedValue.HumanizeForCurrentCulture() </span><span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span>
</div>
<div>
<span class="text-primary"> @stat.LastSeenValue.HumanizeForCurrentCulture() </span><span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span>
</div>
</div>
}
else
{
<span class="text-primary">@stat.Performance</span> <span class="text-muted"> @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]</span>
<br/>
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
<span class="text-primary">@stat.Kills</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
<span class="text-primary">@stat.Deaths</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br />
<span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span> <span class="text-primary"> @stat.TimePlayed </span><span class="text-muted">@loc["GLOBAL_TIME_HOURS"]</span><br />
<span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span><span class="text-primary"> @stat.LastSeen </span><span class="text-muted">@loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</span>
}
</div>
<div class="col-md-6 client-rating-graph" id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@Html.Raw(Json.Serialize(stat.PerformanceHistory))">
</div>
<div class="col-md-2 client-rating-icon text-md-right text-center align-items-center d-flex justify-content-center">
@if (ViewBag.UseNewStats)
{
<img class="align-self-center d-none d-md-block" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/>
}
else
{
<img src="/images/icons/@rankIcon(stat.Performance)"/>
}
</div>
</div>
}

View File

@ -0,0 +1,34 @@
<ul class="nav nav-tabs border-top border-bottom nav-fill row" role="tablist" id="stats_top_players">
<li class="nav-item">
<a class="nav-link active top-players-link" href="#server_0" role="tab" data-toggle="tab" aria-selected="true" data-serverid="0">@ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"]</a>
</li>
@foreach (var server in ViewBag.Servers)
{
<li class="nav-item ">
<a class="nav-link top-players-link" href="#server_@server.ID" role="tab" data-toggle="tab" aria-selected="false" data-serverid="@server.ID">
<color-code value="@server.Name" allow="@ViewBag.EnableColorCodes"></color-code>
</a>
</li>
}
</ul>
<div class="tab-content border-bottom row">
<div role="tabpanel" class="tab-pane active striped flex-fill" id="server_0">
@await Component.InvokeAsync("TopPlayers", new { count = 25, offset = 0 })
</div>
@foreach (var server in ViewBag.Servers)
{
<div role="tabpanel" class="tab-pane striped flex-fill" id="server_@server.ID">
</div>
}
</div>
@section scripts
{
<environment include="Development">
<script type="text/javascript" src="~/js/loader.js"></script>
<script type="text/javascript" src="~/js/stats.js"></script>
</environment>
<script>initLoader('/Stats/GetTopPlayersAsync', '#server_0', 25);</script>
}

View File

@ -0,0 +1,24 @@
@using SharedLibraryCore.Dtos.Meta.Responses
@model IList<MessageResponse>
@{
Layout = null;
}
<div class="client-message-context">
<h5 class="bg-primary pt-2 pb-2 pl-3 mb-0 mt-2 text-white">@Model.First().When.ToString()</h5>
<div class="bg-dark p-3 mb-2 border-bottom">
@foreach (var message in Model)
{
<span class="text-white">
<color-code value="@message.ClientName" allow="ViewBag.EnableColorCodes"></color-code>
</span>
<span>
&mdash;
<span class="@(message.IsQuickMessage ? "font-italic" : "")">
<color-code value="@(message.IsHidden ? message.HiddenMessage : message.Message)" allow="ViewBag.EnableColorCodes"></color-code>
</span>
</span>
<br />
}
</div>
</div>

View File

@ -0,0 +1,22 @@
@model IEnumerable<Data.Models.Client.Stats.EFACSnapshot>
@{
Layout = null;
}
<div class="penalty-info-context bg-dark p-2 mt-2 mb-2 border-top border-bottom">
@foreach (var snapshot in Model)
{
<!-- this is not ideal, but I didn't want to manually write out all the properties-->
var snapProperties = Model.First().GetType().GetProperties();
foreach (var prop in snapProperties)
{
@if ((prop.Name.EndsWith("Id") && prop.Name != "WeaponId") || new[] { "Active", "Client", "PredictedViewAngles" }.Contains(prop.Name))
{
continue;
}
<span class="text-white">@prop.Name </span> <span>&mdash; @prop.GetValue(snapshot).ToString()</span><br />
}
<div class="w-100 mt-1 mb-1 border-bottom"></div>
}
</div>

View File

@ -1,4 +1,4 @@
@model SharedLibraryCore.Database.Models.EFPenalty.PenaltyType
@model Data.Models.EFPenalty.PenaltyType
@{
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
}
@ -7,11 +7,11 @@
<div class="d-block d-md-flex w-100 pb-2">
<select class="form-control bg-dark text-muted" id="penalty_filter_selection">
@{
foreach (var penaltyType in Enum.GetValues(typeof(SharedLibraryCore.Database.Models.EFPenalty.PenaltyType)))
foreach (var penaltyType in Enum.GetValues(typeof(Data.Models.EFPenalty.PenaltyType)))
{
if ((SharedLibraryCore.Database.Models.EFPenalty.PenaltyType)penaltyType == SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Any)
if ((Data.Models.EFPenalty.PenaltyType)penaltyType == Data.Models.EFPenalty.PenaltyType.Any)
{
if (Model == SharedLibraryCore.Database.Models.EFPenalty.PenaltyType.Any)
if (Model == Data.Models.EFPenalty.PenaltyType.Any)
{
<option value="@Convert.ToInt32(penaltyType)" selected="selected" )>@loc["WEBFRONT_PENALTY_TEMPLATE_SHOW"] @penaltyType.ToString()</option>
}
@ -22,7 +22,7 @@
}
else
{
if ((SharedLibraryCore.Database.Models.EFPenalty.PenaltyType)penaltyType == Model)
if ((Data.Models.EFPenalty.PenaltyType)penaltyType == Model)
{
<option value="@Convert.ToInt32(penaltyType)" selected="selected">@loc["WEBFRONT_PENALTY_TEMPLATE_SHOWONLY"] @penaltyType.ToString()s</option>
}

View File

@ -2,7 +2,7 @@
var loc = SharedLibraryCore.Utilities.CurrentLocalization.LocalizationIndex;
}
<!DOCTYPE html>
<html>
<html xmlns="http://www.w3.org/1999/html">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@ -159,6 +159,7 @@
<script type="text/javascript" src="~/lib/moment-timezone/moment-timezone.js"></script>
<script type="text/javascript" src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script type="text/javascript" src="~/lib/canvas.js/canvasjs.js"></script>
<script type="text/javascript" src="~/lib/chart.js/dist/Chart.bundle.min.js"></script>
<script type="text/javascript" src="~/js/action.js"></script>
<script type="text/javascript" src="~/js/search.js"></script>
</environment>
@ -172,6 +173,6 @@
_localization[key] = value;
});
</script>
@RenderSection("scripts", required: false)
@await RenderSectionAsync("scripts", required: false)
</body>
</html>