Files
HeXi/hexi/plugins/nonebot_plugin_bf_bot/services/processing.py
T
sansenhoshiandClaude Code a31aab00e8 refactor(bf-bot): 插件重构为 handlers/services/utils 分层,资源归入 res/
- 旧平铺模块(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>
2026-10-13 17:53:06 +08:00

302 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""战绩加工:最佳武器/载具、载具摧毁数、六维雷达数据与称号评级。"""
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),
}