- 旧平铺模块(data/data_utils/database_op/image_builder*/get_bf6_data/ gametools_bf6/bf6_data/img_utils)拆分为 handlers(绑定/查询)、 services(fetchers/processing/query)、utils(渲染/参数)分层; 新增 repository.py / config.py / paths.py - 字体/图片/模板资源迁入 res/,新增 BF2042 卡面素材(bg/object_icon 等) - browser_ext/:BF2042 扩展捕获桥(WS 直连兜底通道)源码入库 - 新增 hexi/web/src/plugins/bf_bot 前端管理页 - 新增 tests:test_bf_bot_helpers / repository / tracker_api Co-Authored-By: Claude Code <noreply@anthropic.com>
302 lines
11 KiB
Python
302 lines
11 KiB
Python
"""战绩加工:最佳武器/载具、载具摧毁数、六维雷达数据与称号评级。"""
|
||
|
||
from functools import reduce
|
||
|
||
from nonebot import logger
|
||
|
||
from ..utils.params import SPECIALIST_NAMES, interval_table, round_data
|
||
|
||
|
||
async def get_best_weapon_and_best_vehicle(player_stat):
|
||
weapon = sorted(player_stat["weapons"], key=lambda k: k["kills"], reverse=True)[0]
|
||
vehicle = sorted(player_stat["vehicles"], key=lambda k: k["kills"], reverse=True)[0]
|
||
return weapon, vehicle
|
||
|
||
|
||
async def get_best_classes(player_stat):
|
||
classes_data = player_stat["classes"]
|
||
data = [obj for obj in classes_data if obj.get("className") != "All"]
|
||
best_classes = max(data, key=lambda x: x["kills"])
|
||
best_classes_name = best_classes["className"]
|
||
return best_classes_name
|
||
|
||
|
||
async def get_vehicle_destroyed(player_stat):
|
||
vehicle = reduce(lambda acc, x: acc + x["destroyed"], player_stat, 0)
|
||
return vehicle
|
||
|
||
|
||
async def stats_calculator(player_stat, game):
|
||
per500_data = round_data[game]
|
||
# 场次(无直接数值反馈,采用胜利数+失败数)
|
||
match_played = player_stat["wins"] + player_stat["loses"]
|
||
|
||
kd = player_stat["killDeath"]
|
||
kpm = player_stat["killsPerMinute"]
|
||
kill_assists = player_stat["killAssists"]
|
||
if game == "bf3":
|
||
headshots = float(0)
|
||
else:
|
||
headshots = float(player_stat["headshots"].replace("%", ""))
|
||
spm = player_stat["scorePerMinute"]
|
||
revives = player_stat["revives"]
|
||
|
||
apr = kill_assists / match_played
|
||
rpr = revives / match_played
|
||
|
||
# 计算比值
|
||
# kd
|
||
ratio_kd = round(kd / per500_data["kd"], 2)
|
||
# kpm
|
||
ratio_kpm = round(kpm / per500_data["kpm"], 2)
|
||
# 爆头率
|
||
ratio_hs = round(headshots / 100 / per500_data["hs"], 2)
|
||
# 场均救援
|
||
ratio_rpr = round(rpr / per500_data["rpr"], 2)
|
||
# 场均助攻
|
||
ratio_apr = round(apr / per500_data["apr"], 2)
|
||
# 分钟得分
|
||
ratio_spm = round(spm / per500_data["spm"], 2)
|
||
logger.info(
|
||
"雷达图数据"
|
||
f"\n生涯KD比值:{ratio_kd}\n生涯Kpm比值:{ratio_kpm}\n生涯准度比值:{ratio_hs}"
|
||
f"\n场均救援比值:{ratio_rpr}\n场均助攻比值:{ratio_apr}\n分钟得分比值:{ratio_spm}"
|
||
)
|
||
|
||
stat_data = [ratio_kd, ratio_apr, ratio_hs, ratio_spm, ratio_kpm, ratio_rpr]
|
||
|
||
# 血腥度 kd*0.5 + kpm *0.4 +hs*0.1
|
||
player_blood = kd * 0.5 + kpm * 0.4 + headshots * 0.1
|
||
ratio_blood = player_blood / per500_data["blood"]
|
||
|
||
# 贡献度 场均助攻*0.4 + 场均救援*0.4 + spm*0.2
|
||
player_dedication = rpr * 0.4 + apr * 0.4 + spm * 0.2
|
||
ratio_dedication = player_dedication / per500_data["dedication"]
|
||
|
||
# 全能度 血腥度*0.5 + 贡献度*0.5
|
||
player_all_round = player_blood * 0.5 + player_dedication * 0.5
|
||
ratio_all_round = player_all_round / per500_data["all_round"]
|
||
|
||
logger.info(
|
||
"称号评级数据"
|
||
f"\n血腥度:{player_blood} 比值:{ratio_blood}"
|
||
f"\n贡献度:{player_dedication} 比值:{ratio_dedication}"
|
||
f"\n全能度:{player_all_round} 比值:{ratio_all_round}"
|
||
)
|
||
level_designation = get_level_designation(
|
||
ratio_blood, ratio_dedication, ratio_all_round
|
||
)
|
||
return stat_data, level_designation
|
||
|
||
|
||
def get_level_designation(blood, dedication, all_round):
|
||
feature_type = ""
|
||
final_max = max([blood, dedication, all_round])
|
||
if final_max == blood:
|
||
feature_type = "blood"
|
||
elif final_max == dedication:
|
||
feature_type = "dedication"
|
||
elif final_max == all_round:
|
||
feature_type = "all_round"
|
||
logger.info(f"最高能力:{feature_type}")
|
||
logger.info(f"最高能力值:{final_max}")
|
||
final_info = get_feature(final_max, feature_type)
|
||
return final_info
|
||
|
||
|
||
def get_feature(value, feature_type):
|
||
"""根据输入值和数值类别,返回对应特征值"""
|
||
for (low, high), features in interval_table.items():
|
||
if low <= value < high:
|
||
return features[feature_type]
|
||
raise ValueError(f"数值 {value} 不在任何区间内")
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# BF2042 生涯卡数据(旧 project 卡片槽位 → tracker.gg 数据映射)
|
||
# --------------------------------------------------------------------------- #
|
||
_PLACEHOLDER = "–"
|
||
|
||
|
||
def _seg_stat(seg: dict, *keys):
|
||
"""从段 stats 取第一个命中的显示值(displayValue 优先)。"""
|
||
stats = (seg or {}).get("stats") or {}
|
||
for key in keys:
|
||
entry = stats.get(key)
|
||
if isinstance(entry, dict):
|
||
dv = entry.get("displayValue")
|
||
if dv is not None:
|
||
return dv
|
||
v = entry.get("value")
|
||
if v is not None:
|
||
return v
|
||
elif entry is not None:
|
||
return entry
|
||
return _PLACEHOLDER
|
||
|
||
|
||
def _seg_value(seg: dict, *keys) -> float:
|
||
stats = (seg or {}).get("stats") or {}
|
||
for key in keys:
|
||
entry = stats.get(key)
|
||
if isinstance(entry, dict) and entry.get("value") is not None:
|
||
return float(entry["value"])
|
||
return 0.0
|
||
|
||
|
||
def _fmt_minutes_hours(minutes: float) -> str:
|
||
"""tracker timePlayed(分钟) → "X.XX" 小时字符串(旧卡习惯)。"""
|
||
try:
|
||
return f"{float(minutes) / 60:.2f}"
|
||
except (TypeError, ValueError):
|
||
return _PLACEHOLDER
|
||
|
||
|
||
def _s_level(level) -> str:
|
||
"""等级 S 前缀:tracker 的 298 → 'S199'(与旧 gametools 一致)。"""
|
||
try:
|
||
lv = int(level)
|
||
except (TypeError, ValueError):
|
||
return str(level)
|
||
return f"S{lv - 99}" if lv - 99 > 0 else str(lv)
|
||
|
||
|
||
def _top_rows(segments: list[dict], top: int) -> list[dict]:
|
||
"""武器/载具段 → 卡片 TOP 行(按击杀降序)。"""
|
||
ranked = sorted(segments or [], key=lambda s: _seg_value(s, "kills"), reverse=True)
|
||
rows = []
|
||
for seg in ranked[:top]:
|
||
meta = seg.get("metadata") or {}
|
||
rows.append(
|
||
{
|
||
"名称": (meta.get("name") or "").strip() or _PLACEHOLDER,
|
||
"类别": meta.get("category") or "",
|
||
"击杀": _seg_stat(seg, "kills"),
|
||
"爆头率": _seg_stat(seg, "headshotPercentage"),
|
||
"命中率": _seg_stat(seg, "shotsAccuracy"),
|
||
"KPM": _seg_stat(seg, "killsPerMinute"),
|
||
"时长": _fmt_minutes_hours(_seg_value(seg, "timePlayed")),
|
||
"摧毁": _seg_stat(seg, "destroyedWith"),
|
||
# 原始值给排序/调试用
|
||
"_kills": _seg_value(seg, "kills"),
|
||
}
|
||
)
|
||
return rows
|
||
|
||
|
||
def build_bf2042_career_card(
|
||
profile: dict,
|
||
weapon_segs: list[dict] | None,
|
||
vehicle_segs: list[dict] | None,
|
||
soldier_segs: list[dict] | None = None,
|
||
) -> dict:
|
||
"""tracker.gg 数据 → 旧卡片字段契约(dev/bf2042 的 data 结构)。
|
||
|
||
字段名沿用旧版(userName/killDeath/infantryKillDeath/secondsPlayed/
|
||
dividedKills/humanPrecentage/weapons[].headshots 等),渲染层照旧坐标绘制;
|
||
tracker 没有的数据(命中率/mvp/标记敌人数/处决等)填占位符 "–"。
|
||
"""
|
||
data = (profile or {}).get("data") or {}
|
||
segments = data.get("segments") or []
|
||
overview = next((s for s in segments if s.get("type") == "overview"), {})
|
||
info = data.get("platformInfo") or {}
|
||
|
||
kills = _seg_value(overview, "kills")
|
||
kd = _seg_value(overview, "kdRatio")
|
||
kpm = _seg_value(overview, "killsPerMinute")
|
||
human_kills = _seg_value(overview, "humanKills")
|
||
human_per = round(100.0 * human_kills / kills, 1) if kills else 0.0
|
||
ratio = (human_kills / kills) if kills else 0.0
|
||
|
||
level = _seg_stat(overview, "level")
|
||
try:
|
||
level_s = int(level) - 99
|
||
level = f"S {level_s:03d}" if level_s > 0 else f"{int(level):03d}"
|
||
except (TypeError, ValueError):
|
||
level = str(level)
|
||
|
||
# 真实 K/D、真实 KPM(旧卡算法:按"对人击杀占比"折算)
|
||
real_kd = round(kd * ratio, 2)
|
||
real_kpm = round(kpm * ratio, 2)
|
||
|
||
# 最佳专家(soldier 段→旧 classes/characterName 结构)
|
||
best = None
|
||
ranked = sorted(
|
||
soldier_segs or [], key=lambda s: _seg_value(s, "kills"), reverse=True
|
||
)
|
||
if ranked:
|
||
seg = ranked[0]
|
||
raw_name = ((seg.get("metadata") or {}).get("name") or "").strip()
|
||
best = {
|
||
"characterName": raw_name,
|
||
"displayName": SPECIALIST_NAMES.get(raw_name, raw_name),
|
||
"className": (seg.get("metadata") or {}).get("category") or "",
|
||
"kills": _seg_stat(seg, "kills"),
|
||
"kpm": _seg_stat(seg, "killsPerMinute"),
|
||
"killDeath": _seg_stat(seg, "kdRatio"),
|
||
"secondsPlayed": round(_seg_value(seg, "timePlayed")),
|
||
}
|
||
|
||
# 鉴定文案(旧阈值)
|
||
if kpm > 1.00 and kd > 2 and real_kd > 1.5:
|
||
verdict, verdict_color = "Pro哥\n你带我走吧T_T", "gold"
|
||
else:
|
||
verdict, verdict_color = "薯薯\n别拷打我了哥>_<", "skyblue"
|
||
|
||
def _objects(segs: list[dict], name_key: str, is_vehicle: bool) -> list[dict]:
|
||
rows = []
|
||
for seg in sorted(
|
||
segs or [], key=lambda x: _seg_value(x, "kills"), reverse=True
|
||
)[:5]:
|
||
meta = seg.get("metadata") or {}
|
||
rows.append(
|
||
{
|
||
name_key: (meta.get("name") or "").strip(),
|
||
"type": meta.get("category") or "",
|
||
"kills": _seg_stat(seg, "kills"),
|
||
"headshots": _seg_stat(seg, "headshotPercentage"),
|
||
"accuracy": _seg_stat(seg, "shotsAccuracy"),
|
||
"killsPerMinute": _seg_stat(seg, "killsPerMinute"),
|
||
"timeEquipped": round(_seg_value(seg, "timePlayed")),
|
||
"destroyed": _seg_stat(seg, "destroyedWith") if is_vehicle else "–",
|
||
}
|
||
)
|
||
return rows
|
||
|
||
return {
|
||
"userName": info.get("platformUserHandle") or _PLACEHOLDER,
|
||
"platform": info.get("platformSlug") or "origin",
|
||
"level": level,
|
||
"secondsPlayed": int(_seg_value(overview, "timePlayed") // 3600),
|
||
"matchesPlayed": _seg_stat(overview, "matches"),
|
||
"mvp": "–", # tracker 无
|
||
"bestSquad": "–", # tracker 无
|
||
"playerTakeDowns": _seg_stat(overview, "meleeKills"),
|
||
"kills": _seg_stat(overview, "kills"),
|
||
"deaths": _seg_stat(overview, "deaths"),
|
||
"killDeath": _seg_stat(overview, "kdRatio"),
|
||
"infantryKillDeath": round(kd * ratio, 2),
|
||
"killsPerMinute": _seg_stat(overview, "killsPerMinute"),
|
||
"killsPerMatch": _seg_stat(overview, "killsPerMatch"),
|
||
"headShots": _seg_stat(overview, "headshotPercentage"),
|
||
"accuracy": "–", # tracker overview 无整体命中率
|
||
"winPercent": _seg_stat(overview, "wlPercentage"),
|
||
"humanPrecentage": f"{human_per}%",
|
||
"dividedKills": {
|
||
"ai": _seg_stat(overview, "aiKills"),
|
||
"vehicle": _seg_stat(overview, "vehicleKills"),
|
||
"longDistance": "–", # tracker 无
|
||
"inRound": {"longDistance": "–"},
|
||
},
|
||
"revives": _seg_stat(overview, "revives"),
|
||
"enemiesSpotted": "–", # tracker 无
|
||
"vehiclesDestroyed": _seg_stat(overview, "vehiclesDestroyed"),
|
||
"bestClass": best,
|
||
"realKd": real_kd,
|
||
"realKpm": real_kpm,
|
||
"verdict": verdict,
|
||
"verdictColor": verdict_color,
|
||
"weapons": _objects(weapon_segs or [], "weaponName", False),
|
||
"vehicles": _objects(vehicle_segs or [], "vehicleName", True),
|
||
}
|