2.1 : New

This commit is contained in:
2026-01-28 01:20:26 +08:00
parent b3941cad3b
commit 4afb728bfa
12 changed files with 1084 additions and 16 deletions

View File

@@ -217,20 +217,21 @@
{{ detail_item('Assists (场均助攻)', features['basic_avg_assisted_kill'], 'basic_avg_assisted_kill') }}
{{ detail_item('AWP Kills (狙击击杀)', features['basic_avg_awp_kill'], 'basic_avg_awp_kill') }}
{{ detail_item('Jumps (场均跳跃)', features['basic_avg_jump_count'], 'basic_avg_jump_count', '{:.1f}') }}
{{ detail_item('Knife Kills (场均刀杀)', features['basic_avg_knife_kill'], 'basic_avg_knife_kill') }}
{{ detail_item('Zeus Kills (电击枪杀)', features['basic_avg_zeus_kill'], 'basic_avg_zeus_kill') }}
{{ detail_item('Zeus Buy% (起电击枪)', features['basic_zeus_pick_rate'], 'basic_zeus_pick_rate', '{:.1%}') }}
<!-- Row 3: Objective -->
{{ detail_item('MVP (最有价值)', features['basic_avg_mvps'], 'basic_avg_mvps') }}
{{ detail_item('Plants (下包)', features['basic_avg_plants'], 'basic_avg_plants') }}
{{ detail_item('Defuses (拆包)', features['basic_avg_defuses'], 'basic_avg_defuses') }}
{{ detail_item('Flash Assist (闪光助攻)', features['basic_avg_flash_assists'], 'basic_avg_flash_assists') }}
<div class="hidden lg:block"></div> <!-- Spacer -->
<!-- Row 4: Opening -->
{{ detail_item('First Kill (场均首杀)', features['basic_avg_first_kill'], 'basic_avg_first_kill') }}
{{ detail_item('First Death (场均首死)', features['basic_avg_first_death'], 'basic_avg_first_death') }}
{{ detail_item('FK Rate (首杀率)', features['basic_first_kill_rate'], 'basic_first_kill_rate', '{:.1%}') }}
{{ detail_item('FD Rate (首死率)', features['basic_first_death_rate'], 'basic_first_death_rate', '{:.1%}') }}
<div class="hidden lg:block"></div> <!-- Spacer -->
<!-- Row 5: Multi-Kills -->
{{ detail_item('2K Rounds (双杀)', features['basic_avg_kill_2'], 'basic_avg_kill_2') }}
@@ -321,6 +322,51 @@
</div>
</div>
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
ROUND (Round Dynamics)
</h4>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{{ detail_item('Kill Early (前30秒击杀)', features['rd_phase_kill_early_share'], 'rd_phase_kill_early_share', '{:.1%}') }}
{{ detail_item('Kill Mid (30-60秒击杀)', features['rd_phase_kill_mid_share'], 'rd_phase_kill_mid_share', '{:.1%}') }}
{{ detail_item('Kill Late (60秒后击杀)', features['rd_phase_kill_late_share'], 'rd_phase_kill_late_share', '{:.1%}') }}
{{ detail_item('Death Early (前30秒死亡)', features['rd_phase_death_early_share'], 'rd_phase_death_early_share', '{:.1%}') }}
{{ detail_item('Death Mid (30-60秒死亡)', features['rd_phase_death_mid_share'], 'rd_phase_death_mid_share', '{:.1%}') }}
{{ detail_item('Death Late (60秒后死亡)', features['rd_phase_death_late_share'], 'rd_phase_death_late_share', '{:.1%}') }}
{{ detail_item('FirstDeath Win% (首死后胜率)', features['rd_firstdeath_team_first_death_win_rate'], 'rd_firstdeath_team_first_death_win_rate', '{:.1%}', count_label=features['rd_firstdeath_team_first_death_rounds']) }}
{{ detail_item('Invalid Death% (无效死亡)', features['rd_invalid_death_rate'], 'rd_invalid_death_rate', '{:.1%}', count_label=features['rd_invalid_death_rounds']) }}
{{ detail_item('Pressure KPR (落后≥3)', features['rd_pressure_kpr_ratio'], 'rd_pressure_kpr_ratio', '{:.2f}x') }}
{{ detail_item('MatchPt KPR (赛点放大)', features['rd_matchpoint_kpr_ratio'], 'rd_matchpoint_kpr_ratio', '{:.2f}x', count_label=features['rd_matchpoint_rounds']) }}
{{ detail_item('Trade Resp (10s响应)', features['rd_trade_response_10s_rate'], 'rd_trade_response_10s_rate', '{:.1%}') }}
{{ detail_item('Pressure Perf (Leetify)', features['rd_pressure_perf_ratio'], 'rd_pressure_perf_ratio', '{:.2f}x') }}
{{ detail_item('MatchPt Perf (Leetify)', features['rd_matchpoint_perf_ratio'], 'rd_matchpoint_perf_ratio', '{:.2f}x') }}
{{ detail_item('Comeback KillShare (追分)', features['rd_comeback_kill_share'], 'rd_comeback_kill_share', '{:.1%}', count_label=features['rd_comeback_rounds']) }}
{{ detail_item('Map Stability (地图稳定)', features['map_stability_coef'], 'map_stability_coef', '{:.3f}') }}
</div>
<div class="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="bg-gray-50 dark:bg-slate-700/30 rounded-xl p-4 border border-gray-100 dark:border-slate-600">
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Phase Split</div>
<div class="h-40">
<canvas id="phaseChart"></canvas>
</div>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 rounded-xl p-4 border border-gray-100 dark:border-slate-600">
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Top Weapons</div>
<div id="weaponTopTable" class="text-sm"></div>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 rounded-xl p-4 border border-gray-100 dark:border-slate-600">
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Round Type Split</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400 mb-2">
KPR=Kills per Round每回合击杀 · Perf=Leetify Round Performance Score回合表现分
</div>
<div id="roundTypeTable" class="text-sm"></div>
</div>
</div>
</div>
<!-- Group 5: SPECIAL (Clutch & Multi) -->
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
@@ -951,7 +997,176 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
const phaseCanvas = document.getElementById('phaseChart');
if (phaseCanvas) {
const ctxPhase = phaseCanvas.getContext('2d');
new Chart(ctxPhase, {
type: 'bar',
data: {
labels: ['Early', 'Mid', 'Late'],
datasets: [
{
label: 'Kills',
data: [
{{ features.get('rd_phase_kill_early_share', 0) }},
{{ features.get('rd_phase_kill_mid_share', 0) }},
{{ features.get('rd_phase_kill_late_share', 0) }}
],
backgroundColor: 'rgba(124, 58, 237, 0.55)'
},
{
label: 'Deaths',
data: [
{{ features.get('rd_phase_death_early_share', 0) }},
{{ features.get('rd_phase_death_mid_share', 0) }},
{{ features.get('rd_phase_death_late_share', 0) }}
],
backgroundColor: 'rgba(148, 163, 184, 0.55)'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
suggestedMax: 1,
ticks: {
callback: (v) => `${Math.round(v * 100)}%`
}
}
},
plugins: {
legend: { display: true, position: 'bottom' },
tooltip: {
callbacks: {
label: (ctx) => `${ctx.dataset.label}: ${(ctx.parsed.y * 100).toFixed(1)}%`
}
}
}
}
});
}
const weaponTop = JSON.parse({{ (features.get('rd_weapon_top_json', '[]') or '[]') | tojson }});
const weaponTopEl = document.getElementById('weaponTopTable');
if (weaponTopEl) {
if (!Array.isArray(weaponTop) || weaponTop.length === 0) {
weaponTopEl.innerHTML = '<div class="text-gray-500 dark:text-gray-400">No data</div>';
} else {
const matchesPlayed = Number({{ features.get('total_matches', 0) or 0 }}) || 0;
const weaponRankMap = {{ (distribution.get('top_weapon_rank_map', {}) or {}) | tojson }};
const rows = weaponTop.map(w => {
const kills = Number(w.kills || 0);
const hsRate = Number(w.hs_rate || 0);
const kpm = matchesPlayed > 0 ? (kills / matchesPlayed) : kills;
return { ...w, kills, hsRate, kpm };
});
rows.sort((a, b) => b.kpm - a.kpm);
const catMap = { pistol: '副武器', smg: '冲锋枪', shotgun: '霰弹枪', rifle: '步枪', sniper: '狙击枪', lmg: '重机枪' };
const fmtPct = (v) => `${(v * 100).toFixed(1)}%`;
weaponTopEl.innerHTML = `
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead class="text-gray-500 dark:text-gray-400">
<tr>
<th class="text-left font-bold py-1 pr-2">武器</th>
<th class="text-right font-bold py-1 px-2">击杀</th>
<th class="text-right font-bold py-1 px-2">爆头率</th>
<th class="text-left font-bold py-1 pl-2">价格/类型</th>
</tr>
</thead>
<tbody class="text-gray-700 dark:text-gray-200">
${rows.map((w) => {
const category = catMap[w.category] || (w.category || '');
const price = (w.price != null) ? `$${w.price}` : '—';
const info = weaponRankMap[w.weapon] || {};
const kpmRank = (info.kpm_rank != null && info.kpm_total != null) ? `#${info.kpm_rank}/${info.kpm_total}` : '—';
const hsRank = (info.hs_rank != null && info.hs_total != null) ? `#${info.hs_rank}/${info.hs_total}` : '—';
const killCell = `${w.kills} (场均 ${w.kpm.toFixed(2)} · ${kpmRank})`;
const hsCell = `${fmtPct(w.hsRate)} (${hsRank})`;
const priceType = `${price}${category ? '-' + category : ''}`;
return `
<tr class="border-t border-gray-100 dark:border-slate-600/40">
<td class="py-1 pr-2 font-mono">${w.weapon}</td>
<td class="py-1 px-2 text-right font-mono">${killCell}</td>
<td class="py-1 px-2 text-right font-mono">${hsCell}</td>
<td class="py-1 pl-2 font-mono">${priceType}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
}
}
const roundSplit = JSON.parse({{ (features.get('rd_roundtype_split_json', '{}') or '{}') | tojson }});
const roundSplitEl = document.getElementById('roundTypeTable');
if (roundSplitEl) {
const keys = Object.keys(roundSplit || {});
if (keys.length === 0) {
roundSplitEl.innerHTML = '<div class="text-gray-500 dark:text-gray-400">No data</div>';
} else {
const order = ['pistol', 'reg', 'eco', 'rifle', 'fullbuy', 'overtime'];
keys.sort((a, b) => order.indexOf(a) - order.indexOf(b));
const rtRank = {
pistol: { kpr: { rank: {{ (distribution.get('rd_rt_kpr_pistol') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_kpr_pistol') or {}).get('total', 'null') }} } },
reg: { kpr: { rank: {{ (distribution.get('rd_rt_kpr_reg') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_kpr_reg') or {}).get('total', 'null') }} } },
overtime: { kpr: { rank: {{ (distribution.get('rd_rt_kpr_overtime') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_kpr_overtime') or {}).get('total', 'null') }} },
perf: { rank: {{ (distribution.get('rd_rt_perf_overtime') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_perf_overtime') or {}).get('total', 'null') }} } },
eco: { perf: { rank: {{ (distribution.get('rd_rt_perf_eco') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_perf_eco') or {}).get('total', 'null') }} } },
rifle: { perf: { rank: {{ (distribution.get('rd_rt_perf_rifle') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_perf_rifle') or {}).get('total', 'null') }} } },
fullbuy: { perf: { rank: {{ (distribution.get('rd_rt_perf_fullbuy') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_perf_fullbuy') or {}).get('total', 'null') }} } },
};
const fmtRank = (r) => (r && r.rank != null && r.total != null) ? `#${r.rank}/${r.total}` : '—';
roundSplitEl.innerHTML = `
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead class="text-gray-500 dark:text-gray-400">
<tr>
<th class="text-left font-bold py-1 pr-2">类型</th>
<th class="text-right font-bold py-1 px-2">KPR</th>
<th class="text-right font-bold py-1 px-2">队内</th>
<th class="text-right font-bold py-1 px-2">Perf</th>
<th class="text-right font-bold py-1 px-2">队内</th>
<th class="text-right font-bold py-1 pl-2">样本</th>
</tr>
</thead>
<tbody class="text-gray-700 dark:text-gray-200">
${keys.map(k => {
const v = roundSplit[k] || {};
const kpr = (v.kpr != null) ? Number(v.kpr).toFixed(2) : '—';
const perf = (v.perf != null) ? Number(v.perf).toFixed(2) : '—';
const rounds = v.rounds != null ? v.rounds : 0;
const rk = rtRank[k] || {};
const kprRank = fmtRank(rk.kpr);
const perfRank = fmtRank(rk.perf);
return `
<tr class="border-t border-gray-100 dark:border-slate-600/40">
<td class="py-1 pr-2 font-mono">${k}</td>
<td class="py-1 px-2 text-right font-mono">${kpr}</td>
<td class="py-1 px-2 text-right font-mono">${kprRank}</td>
<td class="py-1 px-2 text-right font-mono">${perf}</td>
<td class="py-1 px-2 text-right font-mono">${perfRank}</td>
<td class="py-1 pl-2 text-right font-mono">n=${rounds}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
}
}
});
});
</script>
{% endblock %}
{% endblock %}