2026-01-26 02:13:06 +08:00
|
|
|
|
{% extends "base.html" %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block title %}Strategy Board - Tactics{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block head %}
|
|
|
|
|
|
<!-- Leaflet CSS -->
|
|
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.player-token {
|
|
|
|
|
|
cursor: grab;
|
|
|
|
|
|
transition: transform 0.1s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.player-token:active {
|
|
|
|
|
|
cursor: grabbing;
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
#map-container {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
background-color: #1a1a1a;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.leaflet-container {
|
|
|
|
|
|
background: #1a1a1a;
|
|
|
|
|
|
}
|
|
|
|
|
|
.custom-scroll::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.custom-scroll::-webkit-scrollbar-track {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
.custom-scroll::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background-color: rgba(156, 163, 175, 0.5);
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
|
<div class="flex flex-col h-[calc(100vh-4rem)]">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Navigation (Compact) -->
|
|
|
|
|
|
<div class="bg-white dark:bg-slate-800 border-b border-gray-200 dark:border-slate-700 px-4 py-2 flex items-center justify-between shrink-0 z-30 shadow-sm">
|
|
|
|
|
|
<div class="flex space-x-6 text-sm font-medium">
|
|
|
|
|
|
<a href="{{ url_for('tactics.index') }}" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-white">← Dashboard</a>
|
|
|
|
|
|
<a href="{{ url_for('tactics.analysis') }}" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-white">Deep Analysis</a>
|
|
|
|
|
|
<a href="{{ url_for('tactics.data') }}" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-white">Data Center</a>
|
|
|
|
|
|
<span class="text-yrtv-600 dark:text-yrtv-400 border-b-2 border-yrtv-500">Strategy Board</span>
|
|
|
|
|
|
<a href="{{ url_for('tactics.economy') }}" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-white">Economy</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
Real-time Sync: <span class="text-green-500">● Active</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Main Board Area -->
|
|
|
|
|
|
<div class="flex flex-1 overflow-hidden" x-data="tacticsBoard()">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Left Sidebar: Controls & Roster -->
|
|
|
|
|
|
<div class="w-72 flex flex-col bg-white dark:bg-slate-800 border-r border-gray-200 dark:border-slate-700 shadow-xl z-20">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Map Select -->
|
|
|
|
|
|
<div class="p-4 border-b border-gray-200 dark:border-slate-700">
|
|
|
|
|
|
<div class="flex space-x-2 mb-2">
|
|
|
|
|
|
<select x-model="currentMap" @change="changeMap()" class="flex-1 rounded border-gray-300 dark:bg-slate-700 dark:border-slate-600 dark:text-white text-sm">
|
|
|
|
|
|
<option value="de_mirage">Mirage</option>
|
|
|
|
|
|
<option value="de_inferno">Inferno</option>
|
|
|
|
|
|
<option value="de_dust2">Dust 2</option>
|
|
|
|
|
|
<option value="de_nuke">Nuke</option>
|
|
|
|
|
|
<option value="de_ancient">Ancient</option>
|
|
|
|
|
|
<option value="de_anubis">Anubis</option>
|
|
|
|
|
|
<option value="de_vertigo">Vertigo</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex space-x-2">
|
|
|
|
|
|
<button @click="saveBoard()" class="flex-1 px-3 py-1.5 bg-yrtv-600 text-white rounded hover:bg-yrtv-700 text-xs font-medium">Save Snapshot</button>
|
|
|
|
|
|
<button @click="clearBoard()" class="px-3 py-1.5 bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 rounded hover:bg-red-200 dark:hover:bg-red-900/50 text-xs font-medium">Clear</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Scrollable Content -->
|
|
|
|
|
|
<div class="flex-1 overflow-y-auto custom-scroll p-4 space-y-6">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Roster (Draggable) -->
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Roster</h3>
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<template x-for="player in roster" :key="player.steam_id_64">
|
|
|
|
|
|
<div class="player-token group flex items-center p-2 rounded-lg border border-transparent hover:bg-gray-50 dark:hover:bg-slate-700 hover:border-gray-200 dark:hover:border-slate-600 transition select-none cursor-grab active:cursor-grabbing"
|
|
|
|
|
|
:data-id="player.steam_id_64"
|
|
|
|
|
|
draggable="true"
|
|
|
|
|
|
@dragstart="dragStart($event, player)">
|
|
|
|
|
|
|
|
|
|
|
|
<img :src="player.avatar_url || 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg'"
|
|
|
|
|
|
class="w-8 h-8 rounded-full border border-gray-200 dark:border-slate-600 object-cover pointer-events-none">
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ml-3 flex-1 min-w-0 pointer-events-none">
|
|
|
|
|
|
<div class="text-xs font-medium text-gray-900 dark:text-white truncate" x-text="player.username || player.name"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template x-if="roster.length === 0">
|
|
|
|
|
|
<div class="text-xs text-gray-500 text-center py-4 border-2 border-dashed border-gray-200 dark:border-slate-700 rounded-lg">
|
|
|
|
|
|
No players found.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Active Players List -->
|
|
|
|
|
|
<div x-show="activePlayers.length > 0">
|
|
|
|
|
|
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3 flex justify-between items-center">
|
|
|
|
|
|
<span>On Board</span>
|
|
|
|
|
|
<span class="text-xs bg-yrtv-100 text-yrtv-800 dark:bg-yrtv-900 dark:text-yrtv-300 px-2 py-0.5 rounded-full" x-text="activePlayers.length"></span>
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<ul class="space-y-1">
|
|
|
|
|
|
<template x-for="p in activePlayers" :key="p.id">
|
|
|
|
|
|
<li class="flex items-center justify-between p-2 rounded bg-gray-50 dark:bg-slate-700/50">
|
|
|
|
|
|
<span class="text-xs text-gray-700 dark:text-gray-300 truncate" x-text="p.username || p.name"></span>
|
|
|
|
|
|
<button @click="removeMarker(p.id)" class="text-gray-400 hover:text-red-500 transition">×</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Radar Chart -->
|
|
|
|
|
|
<div class="pt-4 border-t border-gray-200 dark:border-slate-700">
|
|
|
|
|
|
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Synergy</h3>
|
|
|
|
|
|
<div class="relative h-40 w-full">
|
|
|
|
|
|
<canvas id="tacticRadar"></canvas>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Main Map Area -->
|
|
|
|
|
|
<div class="flex-1 relative bg-gray-900" id="map-dropzone" @dragover.prevent @drop="dropOnMap($event)">
|
|
|
|
|
|
<div id="map-container"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="absolute bottom-4 right-4 z-[400] bg-black/50 backdrop-blur text-white text-[10px] px-2 py-1 rounded pointer-events-none">
|
|
|
|
|
|
Drag players to map • Scroll to zoom
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Scripts -->
|
|
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
function tacticsBoard() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
roster: [],
|
|
|
|
|
|
currentMap: 'de_mirage',
|
|
|
|
|
|
map: null,
|
|
|
|
|
|
markers: {}, // id -> marker
|
|
|
|
|
|
activePlayers: [], // list of {id, name, stats}
|
|
|
|
|
|
radarChart: null,
|
|
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
|
this.fetchRoster();
|
|
|
|
|
|
this.initMap();
|
|
|
|
|
|
this.initRadar();
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
|
if (this.map) this.map.invalidateSize();
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
fetchRoster() {
|
|
|
|
|
|
fetch('/teams/api/roster')
|
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
this.roster = data.roster || [];
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
initMap() {
|
|
|
|
|
|
this.map = L.map('map-container', {
|
|
|
|
|
|
crs: L.CRS.Simple,
|
|
|
|
|
|
minZoom: -2,
|
|
|
|
|
|
maxZoom: 2,
|
|
|
|
|
|
zoomControl: true,
|
|
|
|
|
|
attributionControl: false
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.loadMapImage();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
loadMapImage() {
|
|
|
|
|
|
const mapUrls = {
|
|
|
|
|
|
'de_mirage': 'https://static.wikia.nocookie.net/cswikia/images/e/e3/Mirage_CS2_Radar.png',
|
|
|
|
|
|
'de_inferno': 'https://static.wikia.nocookie.net/cswikia/images/7/77/Inferno_CS2_Radar.png',
|
|
|
|
|
|
'de_dust2': 'https://static.wikia.nocookie.net/cswikia/images/0/03/Dust2_CS2_Radar.png',
|
|
|
|
|
|
'de_nuke': 'https://static.wikia.nocookie.net/cswikia/images/1/14/Nuke_CS2_Radar.png',
|
|
|
|
|
|
'de_ancient': 'https://static.wikia.nocookie.net/cswikia/images/1/16/Ancient_CS2_Radar.png',
|
|
|
|
|
|
'de_anubis': 'https://static.wikia.nocookie.net/cswikia/images/2/22/Anubis_CS2_Radar.png',
|
|
|
|
|
|
'de_vertigo': 'https://static.wikia.nocookie.net/cswikia/images/2/23/Vertigo_CS2_Radar.png'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const url = mapUrls[this.currentMap] || mapUrls['de_mirage'];
|
|
|
|
|
|
const bounds = [[0,0], [1024,1024]];
|
|
|
|
|
|
|
|
|
|
|
|
this.map.eachLayer((layer) => {
|
|
|
|
|
|
this.map.removeLayer(layer);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
L.imageOverlay(url, bounds).addTo(this.map);
|
|
|
|
|
|
this.map.fitBounds(bounds);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
changeMap() {
|
|
|
|
|
|
this.loadMapImage();
|
|
|
|
|
|
this.clearBoard();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
dragStart(event, player) {
|
|
|
|
|
|
event.dataTransfer.setData('text/plain', JSON.stringify(player));
|
|
|
|
|
|
event.dataTransfer.effectAllowed = 'copy';
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
dropOnMap(event) {
|
|
|
|
|
|
const data = event.dataTransfer.getData('text/plain');
|
|
|
|
|
|
if (!data) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const player = JSON.parse(data);
|
|
|
|
|
|
const container = document.getElementById('map-container');
|
|
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
const x = event.clientX - rect.left;
|
|
|
|
|
|
const y = event.clientY - rect.top;
|
|
|
|
|
|
|
|
|
|
|
|
const point = this.map.containerPointToLatLng([x, y]);
|
|
|
|
|
|
|
|
|
|
|
|
this.addMarker(player, point);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error("Drop failed:", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
addMarker(player, latlng) {
|
|
|
|
|
|
if (this.markers[player.steam_id_64]) {
|
|
|
|
|
|
this.markers[player.steam_id_64].setLatLng(latlng);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const displayName = player.username || player.name || player.steam_id_64;
|
|
|
|
|
|
|
|
|
|
|
|
const iconHtml = `
|
|
|
|
|
|
<div class="flex flex-col items-center justify-center transform hover:scale-110 transition duration-200">
|
|
|
|
|
|
<img src="${player.avatar_url || 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg'}"
|
|
|
|
|
|
class="w-10 h-10 rounded-full border-2 border-white shadow-lg box-content">
|
|
|
|
|
|
<span class="mt-1 text-[10px] font-bold text-white bg-black/60 px-1.5 py-0.5 rounded backdrop-blur-sm whitespace-nowrap overflow-hidden max-w-[80px] text-ellipsis">
|
|
|
|
|
|
${displayName}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const icon = L.divIcon({
|
|
|
|
|
|
className: 'bg-transparent',
|
|
|
|
|
|
html: iconHtml,
|
|
|
|
|
|
iconSize: [60, 60],
|
|
|
|
|
|
iconAnchor: [30, 30]
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const marker = L.marker(latlng, { icon: icon, draggable: true }).addTo(this.map);
|
|
|
|
|
|
this.markers[player.steam_id_64] = marker;
|
|
|
|
|
|
|
|
|
|
|
|
this.activePlayers.push({
|
|
|
|
|
|
id: player.steam_id_64,
|
|
|
|
|
|
username: player.username,
|
|
|
|
|
|
name: player.name,
|
|
|
|
|
|
stats: player.stats
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.updateRadar();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
removeMarker(id) {
|
|
|
|
|
|
if (this.markers[id]) {
|
|
|
|
|
|
this.map.removeLayer(this.markers[id]);
|
|
|
|
|
|
delete this.markers[id];
|
|
|
|
|
|
this.activePlayers = this.activePlayers.filter(p => p.id !== id);
|
|
|
|
|
|
this.updateRadar();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
clearBoard() {
|
|
|
|
|
|
for (let id in this.markers) {
|
|
|
|
|
|
this.map.removeLayer(this.markers[id]);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.markers = {};
|
|
|
|
|
|
this.activePlayers = [];
|
|
|
|
|
|
this.updateRadar();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
saveBoard() {
|
|
|
|
|
|
const title = prompt("Enter a title for this strategy:", "New Strat " + new Date().toLocaleTimeString());
|
|
|
|
|
|
if (!title) return;
|
|
|
|
|
|
|
|
|
|
|
|
const markerData = [];
|
|
|
|
|
|
for (let id in this.markers) {
|
|
|
|
|
|
const m = this.markers[id];
|
|
|
|
|
|
markerData.push({
|
|
|
|
|
|
id: id,
|
|
|
|
|
|
lat: m.getLatLng().lat,
|
|
|
|
|
|
lng: m.getLatLng().lng
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fetch("{{ url_for('tactics.save_board') }}", {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
title: title,
|
|
|
|
|
|
map_name: this.currentMap,
|
|
|
|
|
|
markers: markerData
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(r => r.json())
|
|
|
|
|
|
.then(data => {
|
|
|
|
|
|
if(data.success) alert("Saved!");
|
|
|
|
|
|
else alert("Error: " + data.message);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
initRadar() {
|
|
|
|
|
|
const ctx = document.getElementById('tacticRadar').getContext('2d');
|
|
|
|
|
|
Chart.defaults.color = '#9ca3af';
|
|
|
|
|
|
Chart.defaults.borderColor = '#374151';
|
|
|
|
|
|
|
|
|
|
|
|
this.radarChart = new Chart(ctx, {
|
|
|
|
|
|
type: 'radar',
|
|
|
|
|
|
data: {
|
2026-01-29 03:17:24 +08:00
|
|
|
|
labels: ['枪法', '生存', '道具', '残局', '经济', '节奏', '手枪', '稳定'],
|
2026-01-26 02:13:06 +08:00
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: 'Avg',
|
2026-01-29 03:17:24 +08:00
|
|
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0],
|
2026-01-26 02:13:06 +08:00
|
|
|
|
backgroundColor: 'rgba(139, 92, 246, 0.2)',
|
|
|
|
|
|
borderColor: 'rgba(139, 92, 246, 1)',
|
|
|
|
|
|
pointBackgroundColor: 'rgba(139, 92, 246, 1)',
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
r: {
|
|
|
|
|
|
beginAtZero: true,
|
2026-01-29 03:17:24 +08:00
|
|
|
|
max: 100,
|
2026-01-26 02:13:06 +08:00
|
|
|
|
grid: { color: 'rgba(156, 163, 175, 0.1)' },
|
|
|
|
|
|
angleLines: { color: 'rgba(156, 163, 175, 0.1)' },
|
|
|
|
|
|
pointLabels: { font: { size: 9 } },
|
|
|
|
|
|
ticks: { display: false }
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
plugins: { legend: { display: false } }
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
updateRadar() {
|
|
|
|
|
|
if (this.activePlayers.length === 0) {
|
2026-01-29 03:17:24 +08:00
|
|
|
|
this.radarChart.data.datasets[0].data = [0, 0, 0, 0, 0, 0, 0, 0];
|
2026-01-26 02:13:06 +08:00
|
|
|
|
this.radarChart.update();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 03:17:24 +08:00
|
|
|
|
let totals = [0, 0, 0, 0, 0, 0, 0, 0];
|
2026-01-26 02:13:06 +08:00
|
|
|
|
this.activePlayers.forEach(p => {
|
|
|
|
|
|
const s = p.stats || {};
|
2026-01-29 03:17:24 +08:00
|
|
|
|
totals[0] += s.score_aim || 0;
|
|
|
|
|
|
totals[1] += s.score_defense || 0;
|
|
|
|
|
|
totals[2] += s.score_utility || 0;
|
|
|
|
|
|
totals[3] += s.score_clutch || 0;
|
|
|
|
|
|
totals[4] += s.score_economy || 0;
|
|
|
|
|
|
totals[5] += s.score_pace || 0;
|
|
|
|
|
|
totals[6] += s.score_pistol || 0;
|
|
|
|
|
|
totals[7] += s.score_stability || 0;
|
2026-01-26 02:13:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const count = this.activePlayers.length;
|
|
|
|
|
|
const avgs = totals.map(t => t / count);
|
|
|
|
|
|
|
|
|
|
|
|
this.radarChart.data.datasets[0].data = avgs;
|
|
|
|
|
|
this.radarChart.update();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
2026-01-29 03:17:24 +08:00
|
|
|
|
{% endblock %}
|