huge commit for advanced stats feature.
broke data out into its own library. may be breaking changes with existing plugins
@ -103,7 +103,7 @@ a.link-inverse:hover {
|
||||
border-bottom-color: $orange;
|
||||
}
|
||||
|
||||
form *, select {
|
||||
form *, select, button.btn {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
@ -140,7 +140,6 @@ form *, select {
|
||||
z-index: 100;
|
||||
font-size: 4rem;
|
||||
-webkit-animation: rotation 1s infinite linear;
|
||||
background-color: $black;
|
||||
background-color: rgba(0, 0,0, 0.5);
|
||||
border-radius: 40px;
|
||||
padding: 5px;
|
||||
@ -209,7 +208,6 @@ form *, select {
|
||||
}
|
||||
|
||||
.nav-tabs, .nav-tabs .nav-link.active {
|
||||
color: $white;
|
||||
color: $white !important;
|
||||
border: none;
|
||||
}
|
||||
@ -400,3 +398,42 @@ input:checked + .toggle-switch-slider:before {
|
||||
color: #ff6060 !important;
|
||||
color: rgba(255, 69, 69, 0.85) !important;
|
||||
}
|
||||
|
||||
.text-force-break
|
||||
{
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
div.card {
|
||||
min-width: 15rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
#rank_icon {
|
||||
max-height: 6rem;
|
||||
}
|
||||
|
||||
.border-bottom-danger {
|
||||
border-bottom: $danger;
|
||||
}
|
||||
|
||||
.border-top-danger {
|
||||
border-top: $danger;
|
||||
}
|
||||
|
||||
.border-danger {
|
||||
border: 1px solid $danger;
|
||||
}
|
||||
|
||||
#client_stats_summary a:hover {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#hitlocation_container {
|
||||
background-color: #141414;
|
||||
}
|
||||
|
BIN
WebfrontCore/wwwroot/images/stats/hit_location_model.png
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_0.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_1.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_10.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_11.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_12.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_13.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_14.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_15.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_16.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_17.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_18.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_19.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_2.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_20.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_21.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_22.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_23.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_24.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_3.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_4.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_5.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_6.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_7.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_8.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
WebfrontCore/wwwroot/images/stats/ranks/rank_9.png
Normal file
After Width: | Height: | Size: 19 KiB |
406
WebfrontCore/wwwroot/js/advanced_stats.js
Normal file
@ -0,0 +1,406 @@
|
||||
window.onresize = function () {
|
||||
drawPlayerModel();
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.table-slide').click(function () {
|
||||
$(this).siblings().children().children('.hidden-row').slideToggle(0);
|
||||
$(this).children('span').toggleClass('oi-chevron-top oi-chevron-bottom');
|
||||
});
|
||||
setupPerformanceGraph();
|
||||
drawPlayerModel();
|
||||
})
|
||||
|
||||
function setupPerformanceGraph() {
|
||||
const summary = $('#client_stats_summary');
|
||||
if (summary === undefined) {
|
||||
return;
|
||||
}
|
||||
const chart = $('#client_performance_history');
|
||||
const container = $('#client_performance_history_container');
|
||||
chart.attr('height', summary.height());
|
||||
chart.attr('width', container.width());
|
||||
renderPerformanceChart();
|
||||
}
|
||||
|
||||
function drawPlayerModel() {
|
||||
const canvas = document.getElementById('hitlocation_model');
|
||||
if (canvas === null) {
|
||||
return;
|
||||
}
|
||||
const context = canvas.getContext('2d');
|
||||
const container = $('#hitlocation_container');
|
||||
const background = new Image();
|
||||
background.onload = () => {
|
||||
const backgroundRatioX = background.width / background.height;
|
||||
|
||||
canvas.height = container.height() - 28;
|
||||
canvas.width = (canvas.height * backgroundRatioX);
|
||||
|
||||
const scalar = canvas.height / background.height;
|
||||
|
||||
drawHitLocationChart(context, background, scalar, canvas.width, canvas.height);
|
||||
}
|
||||
background.src = '/images/stats/hit_location_model.png';
|
||||
}
|
||||
|
||||
function buildHitLocationPosition() {
|
||||
let hitLocations = {}
|
||||
hitLocations['head'] = {
|
||||
x: 454.5,
|
||||
y: 108.5,
|
||||
width: 157,
|
||||
height: 217
|
||||
}
|
||||
|
||||
hitLocations['torso_upper'] = {
|
||||
x: 457,
|
||||
y: 318,
|
||||
width: 254,
|
||||
height: 202
|
||||
}
|
||||
|
||||
hitLocations['torso_lower'] = {
|
||||
x: 456.50,
|
||||
y: 581,
|
||||
width: 315,
|
||||
height: 324
|
||||
}
|
||||
|
||||
hitLocations['right_leg_upper'] = {
|
||||
x: 527.5,
|
||||
y: 856.7,
|
||||
width: 149,
|
||||
height: 228
|
||||
}
|
||||
|
||||
hitLocations['right_leg_lower'] = {
|
||||
x: 542,
|
||||
y: 1077.6,
|
||||
width: 120,
|
||||
height: 214
|
||||
}
|
||||
|
||||
hitLocations['right_foot'] = {
|
||||
x: 558.5,
|
||||
y: 1253.5,
|
||||
width: 93,
|
||||
height: 138
|
||||
}
|
||||
|
||||
hitLocations['left_leg_upper'] = {
|
||||
x: 382.5,
|
||||
y: 857,
|
||||
width: 141,
|
||||
height: 228
|
||||
}
|
||||
|
||||
hitLocations['left_leg_lower'] = {
|
||||
x: 371.5,
|
||||
y: 1078,
|
||||
width: 119,
|
||||
height: 214
|
||||
}
|
||||
|
||||
hitLocations['left_foot'] = {
|
||||
x: 353,
|
||||
y: 1254,
|
||||
width: 90,
|
||||
height: 138
|
||||
}
|
||||
|
||||
hitLocations['left_arm_upper'] = {
|
||||
p1: {
|
||||
x: 330,
|
||||
y: 218
|
||||
},
|
||||
p2: {
|
||||
x: 330,
|
||||
y: 400
|
||||
},
|
||||
p3: {
|
||||
x: 255,
|
||||
y: 475
|
||||
},
|
||||
p4: {
|
||||
x: 165,
|
||||
y: 375
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['right_arm_upper'] = {
|
||||
p1: {
|
||||
x: 584,
|
||||
y: 218
|
||||
},
|
||||
p2: {
|
||||
x: 584,
|
||||
y: 400
|
||||
},
|
||||
p3: {
|
||||
x: 659,
|
||||
y: 475
|
||||
},
|
||||
p4: {
|
||||
x: 749,
|
||||
y: 375
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['left_arm_lower'] = {
|
||||
p1: {
|
||||
x: 165,
|
||||
y: 375
|
||||
},
|
||||
p2: {
|
||||
x: 255,
|
||||
y: 475
|
||||
},
|
||||
p3: {
|
||||
x: 121,
|
||||
y: 584
|
||||
},
|
||||
p4: {
|
||||
x: 30,
|
||||
y: 512
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['right_arm_lower'] = {
|
||||
p1: {
|
||||
x: 749,
|
||||
y: 375
|
||||
},
|
||||
p2: {
|
||||
x: 659,
|
||||
y: 475
|
||||
},
|
||||
p3: {
|
||||
x: 789,
|
||||
y: 587
|
||||
},
|
||||
p4: {
|
||||
x: 876,
|
||||
y: 497
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['left_hand'] = {
|
||||
p1: {
|
||||
x: 30,
|
||||
y: 512
|
||||
},
|
||||
p2: {
|
||||
x: 121,
|
||||
y: 584
|
||||
},
|
||||
p3: {
|
||||
x: 0,
|
||||
y: 669
|
||||
},
|
||||
p4: {
|
||||
x: 0,
|
||||
y: 582
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
|
||||
hitLocations['right_hand'] = {
|
||||
p1: {
|
||||
x: 789,
|
||||
y: 587
|
||||
},
|
||||
p2: {
|
||||
x: 876,
|
||||
y: 497
|
||||
},
|
||||
p3: {
|
||||
x: 905,
|
||||
y: 534
|
||||
},
|
||||
p4: {
|
||||
x: 905,
|
||||
y: 666
|
||||
},
|
||||
type: 'polygon'
|
||||
}
|
||||
return hitLocations;
|
||||
}
|
||||
|
||||
function drawHitLocationChart(context, background, scalar, width, height) {
|
||||
context.drawImage(background, 0, 0, background.width, background.height, 0, 0, width, height);
|
||||
|
||||
const hitLocations = buildHitLocationPosition();
|
||||
|
||||
$.each(hitLocationData, (index, hit) => {
|
||||
let scaledPercentage = hit.percentage / maxPercentage;
|
||||
let red;
|
||||
let green = 255;
|
||||
|
||||
if (scaledPercentage < 0.5) {
|
||||
red = Math.round(scaledPercentage * 255 * 2);
|
||||
} else {
|
||||
red = 255;
|
||||
green = Math.round((1 - scaledPercentage) * 255 * 2);
|
||||
}
|
||||
|
||||
red = red.toString(16).padStart(2, '0');
|
||||
green = green.toString(16).padStart(2, '0');
|
||||
|
||||
const color = '#' + red + green + '0077';
|
||||
const location = hitLocations[hit.name];
|
||||
|
||||
if (location.type === 'polygon') {
|
||||
drawPolygon(context, scalar, location.p1, location.p2, location.p3, location.p4, color);
|
||||
} else {
|
||||
drawRectangle(context, scalar, location.x, location.y, location.width, location.height, color);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawRectangle(context, scalar, x, y, width, height, color) {
|
||||
const scaledRectWidth = width * scalar;
|
||||
const scaledRectHeight = height * scalar;
|
||||
const rectX = x * scalar - (scaledRectWidth / 2);
|
||||
const rectY = y * scalar - (scaledRectHeight / 2);
|
||||
context.beginPath();
|
||||
context.fillStyle = color
|
||||
context.fillRect(rectX, rectY, scaledRectWidth, scaledRectHeight);
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
function drawPolygon(context, scalar, p1, p2, p3, p4, color) {
|
||||
|
||||
const points = [p1, p2, p3, p4];
|
||||
|
||||
$.each(points, (index, point) => {
|
||||
point.x = point.x * scalar;
|
||||
point.y = point.y * scalar;
|
||||
});
|
||||
|
||||
context.beginPath();
|
||||
context.fillStyle = color;
|
||||
context.moveTo(p1.x, p1.y);
|
||||
context.lineTo(p2.x, p2.y);
|
||||
context.lineTo(p3.x, p3.y);
|
||||
context.lineTo(p4.x, p4.y);
|
||||
context.fill();
|
||||
context.closePath();
|
||||
}
|
||||
|
||||
function getClosestMultiple(baseValue, value) {
|
||||
return Math.round(value / baseValue) * baseValue;
|
||||
}
|
||||
|
||||
function renderPerformanceChart() {
|
||||
const id = 'client_performance_history';
|
||||
const data = $('#' + id).data('history');
|
||||
|
||||
if (data === undefined) {
|
||||
return;
|
||||
}
|
||||
if (data.length <= 1) {
|
||||
// only 0 perf
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = [];
|
||||
data.forEach(function (item, i) {
|
||||
labels.push(i);
|
||||
});
|
||||
|
||||
const padding = 4;
|
||||
let dataMin = Math.min(...data);
|
||||
const dataMax = Math.max(...data);
|
||||
|
||||
if (dataMax - dataMin === 0) {
|
||||
dataMin = 0;
|
||||
}
|
||||
|
||||
dataMin = Math.max(0, dataMin);
|
||||
|
||||
const min = getClosestMultiple(padding, dataMin - padding);
|
||||
const max = getClosestMultiple(padding, dataMax + padding);
|
||||
|
||||
const chartData = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
pointBackgroundColor: 'rgba(255, 255, 255, 0)',
|
||||
pointBorderColor: 'rgba(255, 255, 255, 0)',
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: 'rgba(255, 255, 255, 1)',
|
||||
}]
|
||||
};
|
||||
|
||||
const options = {
|
||||
defaultFontFamily: '-apple-system, BlinkMacSystemFont, "Open Sans", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: false,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
label: (tooltipItem) => Math.round(tooltipItem.yLabel) + ' ' + _localization["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
|
||||
title: () => ''
|
||||
},
|
||||
mode: 'nearest',
|
||||
intersect: false,
|
||||
animationDuration: 0,
|
||||
cornerRadius: 0,
|
||||
displayColors: false
|
||||
},
|
||||
hover: {
|
||||
mode: 'nearest',
|
||||
intersect: false
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
fill: false,
|
||||
borderColor: 'rgba(255, 255, 255, 0.75)',
|
||||
borderWidth: 2
|
||||
},
|
||||
point: {
|
||||
radius: 5
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: false,
|
||||
}],
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
|
||||
position: 'right',
|
||||
ticks: {
|
||||
callback: function (value, index, values) {
|
||||
if (index === values.length - 1) {
|
||||
return min;
|
||||
} else if (index === 0) {
|
||||
return max;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
fontColor: 'rgba(255, 255, 255, 0.25)'
|
||||
}
|
||||
}]
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
left: 15
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
new Chart(id, {
|
||||
type: 'line',
|
||||
data: chartData,
|
||||
options: options
|
||||
});
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
function getStatsChart(id, width, height) {
|
||||
const data = $('#' + id).data('history');
|
||||
|
||||
if (data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fixedData = [];
|
||||
data.forEach(function (item, i) {
|
||||
fixedData[i] = { x: i, y: Math.floor(item) };
|
||||
@ -12,7 +17,7 @@
|
||||
dataMin = 0;
|
||||
}
|
||||
|
||||
const padding = (dataMax - dataMin) * 0.075;
|
||||
const padding = (dataMax - dataMin) * 0.5;
|
||||
const min = Math.max(0, dataMin - padding);
|
||||
const max = dataMax + padding;
|
||||
let interval = Math.floor((max - min) / 2);
|
||||
@ -27,12 +32,11 @@
|
||||
animationEnabled: false,
|
||||
toolTip: {
|
||||
contentFormatter: function (e) {
|
||||
return Math.round(e.entries[0].dataPoint.y, 1);
|
||||
return `${_localization['WEBFRONT_ADV_STATS_RANKING_METRIC']} ${Math.round(e.entries[0].dataPoint.y, 1)}`;
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: _localization['WEBFRONT_STATS_PERFORMANCE_HISTORY'],
|
||||
fontSize: 14
|
||||
fontSize: 0
|
||||
},
|
||||
axisX: {
|
||||
gridThickness: 0,
|
||||
|