2026-04-08 08:13:02 +00:00
|
|
|
import os
|
|
|
|
|
import json
|
|
|
|
|
from collections import defaultdict
|
2026-04-09 10:11:39 +00:00
|
|
|
import data.db_access as db_access
|
2026-04-08 08:13:02 +00:00
|
|
|
|
|
|
|
|
from tags.slayer import compute_slayer_for_career_player
|
2026-04-10 08:08:47 +00:00
|
|
|
from tags.objective_payload import compute_payload_tag
|
|
|
|
|
from tags.objective_domination import compute_dom_objective_for_career_player
|
2026-04-08 08:13:02 +00:00
|
|
|
from tags.sharpshooter import compute_sharpshooter_for_career_player
|
2026-04-14 07:32:50 +00:00
|
|
|
from tags.consistency import build_consistency_tag
|
2026-04-10 08:08:47 +00:00
|
|
|
from tags.clutch import compute_clutch, compute_clutch_raw
|
2026-04-21 08:55:26 +00:00
|
|
|
from tags.anchor import compute_anchor_for_career_player
|
|
|
|
|
from tags.breaker import compute_breaker_raw
|
2026-04-10 08:08:47 +00:00
|
|
|
|
|
|
|
|
from data.league_metrics import compute_league_metrics, compute_league_clutch_metrics
|
2026-04-21 08:55:26 +00:00
|
|
|
from data.tag_framework import percentile_rank, tier_from_percentile
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-15 06:15:07 +00:00
|
|
|
|
2026-04-08 08:13:02 +00:00
|
|
|
def build_career_database(output_path):
|
2026-04-10 08:08:47 +00:00
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
#print("USING DB:", db_access.DB_PATH)
|
2026-04-10 08:08:47 +00:00
|
|
|
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-09 10:11:39 +00:00
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# 1. Load all players from the local SQLite DB
|
|
|
|
|
# ---------------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
player_rows = db_access.query("""
|
|
|
|
|
SELECT DISTINCT s.PlayerUUID, p.PlayerGameName
|
|
|
|
|
FROM stats s
|
|
|
|
|
LEFT JOIN players p ON p.PlayerUUID = s.PlayerUUID;
|
|
|
|
|
""")
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# Load Player Identity Registry (PIR)
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
from analysis.player_identity_registry import PlayerIdentityRegistry
|
|
|
|
|
pir = PlayerIdentityRegistry()
|
|
|
|
|
|
|
|
|
|
|
2026-04-09 10:11:39 +00:00
|
|
|
if not player_rows:
|
|
|
|
|
print("No players found in local DB.")
|
|
|
|
|
return False
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-09 10:11:39 +00:00
|
|
|
final_db = {"players": {}, "league_averages": {}}
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-09 10:11:39 +00:00
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# 2. Build per-player career stats from local DB
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
for row in player_rows:
|
2026-04-21 08:55:26 +00:00
|
|
|
raw_uuid = str(row["PlayerUUID"])
|
2026-04-10 08:08:47 +00:00
|
|
|
name = row["PlayerGameName"] or "Unknown"
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-23 09:24:48 +00:00
|
|
|
# Get this player's full match history
|
2026-04-21 08:55:26 +00:00
|
|
|
matches = db_access.get_player_match_history(raw_uuid)
|
2026-04-09 10:11:39 +00:00
|
|
|
if not matches:
|
2026-04-08 08:13:02 +00:00
|
|
|
continue
|
2026-04-21 08:55:26 +00:00
|
|
|
|
2026-04-23 09:24:48 +00:00
|
|
|
# Infer "season" from earliest CycleID (or whatever you treat as season)
|
|
|
|
|
first_cycle = min(m.get("CycleID", 0) for m in matches) or 0
|
|
|
|
|
|
|
|
|
|
# Resolve canonical identity AND stamp first_season
|
|
|
|
|
canonical_id = pir.resolve(raw_uuid, season=first_cycle)
|
|
|
|
|
pir.add_name(canonical_id, name)
|
|
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
# Use CANONICAL ID as the key in the career DB
|
|
|
|
|
pid = canonical_id
|
2026-04-10 08:08:47 +00:00
|
|
|
|
|
|
|
|
#print("DEBUG MATCH ROW FOR", pid, ":", matches[0])
|
|
|
|
|
#break
|
|
|
|
|
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-09 10:11:39 +00:00
|
|
|
career_raw = defaultdict(float)
|
|
|
|
|
|
|
|
|
|
for m in matches:
|
|
|
|
|
career_raw["kills"] += m["Kills"]
|
|
|
|
|
career_raw["deaths"] += m["Deaths"]
|
|
|
|
|
career_raw["damage"] += m["Damage"]
|
|
|
|
|
career_raw["shots"] += m["Shots"]
|
|
|
|
|
career_raw["shots_hit"] += m["ShotsHit"]
|
|
|
|
|
career_raw["headshots"] += m["Headshots"]
|
|
|
|
|
career_raw["PAY_PushTime"] += m["PAY_PushTime"]
|
|
|
|
|
career_raw["DOM_captures"] += m["DOM_Captures"]
|
|
|
|
|
career_raw["DOM_counters"] += m["DOM_Counters"]
|
2026-04-10 08:08:47 +00:00
|
|
|
career_raw["maps"] += 1
|
2026-04-09 10:11:39 +00:00
|
|
|
|
|
|
|
|
maps = max(1, career_raw["maps"])
|
|
|
|
|
KD = career_raw["kills"] / career_raw["deaths"] if career_raw["deaths"] > 0 else career_raw["kills"]
|
|
|
|
|
accuracy = (career_raw["shots_hit"] / career_raw["shots"]) if career_raw["shots"] > 0 else 0.0
|
2026-04-08 08:13:02 +00:00
|
|
|
|
|
|
|
|
derived = {
|
2026-04-09 10:11:39 +00:00
|
|
|
"kills_per_map": career_raw["kills"] / maps,
|
|
|
|
|
"deaths_per_map": career_raw["deaths"] / maps,
|
2026-04-10 08:08:47 +00:00
|
|
|
"push_time_per_season": career_raw["PAY_PushTime"],
|
2026-04-08 08:13:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final_db["players"][pid] = {
|
2026-04-09 10:11:39 +00:00
|
|
|
"name": name,
|
2026-04-08 08:13:02 +00:00
|
|
|
"career": {
|
2026-04-09 10:11:39 +00:00
|
|
|
"kills": career_raw["kills"],
|
|
|
|
|
"deaths": career_raw["deaths"],
|
2026-04-08 08:13:02 +00:00
|
|
|
"KD": KD,
|
|
|
|
|
"accuracy": accuracy,
|
2026-04-09 10:11:39 +00:00
|
|
|
"push_time": career_raw["PAY_PushTime"],
|
2026-04-13 07:56:38 +00:00
|
|
|
"DOM_Captures": int(career_raw["DOM_captures"]),
|
|
|
|
|
"DOM_Counters": int(career_raw["DOM_counters"]),
|
2026-04-09 10:11:39 +00:00
|
|
|
"maps": career_raw["maps"],
|
|
|
|
|
"damage": career_raw["damage"],
|
|
|
|
|
"shots": career_raw["shots"],
|
|
|
|
|
"shots_hit": career_raw["shots_hit"],
|
|
|
|
|
"headshots": career_raw["headshots"],
|
2026-04-08 08:13:02 +00:00
|
|
|
},
|
2026-04-13 07:56:38 +00:00
|
|
|
"DOM_Captures": int(career_raw["DOM_captures"]), # 👈 add this
|
|
|
|
|
"DOM_Counters": int(career_raw["DOM_counters"]), # 👈 and this
|
2026-04-08 08:13:02 +00:00
|
|
|
"derived": derived,
|
2026-04-09 10:11:39 +00:00
|
|
|
"matches": matches,
|
2026-04-08 08:13:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 10:11:39 +00:00
|
|
|
# ---------------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
# 3. PRECOMPUTE RAW CLUTCH VALUES (required for distribution)
|
2026-04-09 10:11:39 +00:00
|
|
|
# ---------------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
all_players = list(final_db["players"].values())
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-10 08:08:47 +00:00
|
|
|
for pdata in all_players:
|
|
|
|
|
pdata["clutch_raw"] = compute_clutch_raw(pdata)
|
2026-04-15 06:15:07 +00:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# NORMALIZE CAREER STAT KEYS BEFORE LEAGUE METRICS
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
for pid, pdata in final_db["players"].items():
|
|
|
|
|
career = pdata.get("career", {})
|
|
|
|
|
|
|
|
|
|
career["push_time"] = (
|
|
|
|
|
career.get("push_time")
|
|
|
|
|
or career.get("PAY_PushTime")
|
|
|
|
|
or pdata.get("PAY_PushTime")
|
|
|
|
|
or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
career["captures"] = (
|
|
|
|
|
career.get("captures")
|
|
|
|
|
or career.get("DOM_Captures")
|
|
|
|
|
or pdata.get("DOM_Captures")
|
|
|
|
|
or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
career["counters"] = (
|
|
|
|
|
career.get("counters")
|
|
|
|
|
or career.get("DOM_Counters")
|
|
|
|
|
or pdata.get("DOM_Counters")
|
|
|
|
|
or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
career["damage"] = (
|
|
|
|
|
career.get("damage")
|
|
|
|
|
or career.get("Damage")
|
|
|
|
|
or pdata.get("Damage")
|
|
|
|
|
or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
career["kills"] = (
|
|
|
|
|
career.get("kills")
|
|
|
|
|
or career.get("Kills")
|
|
|
|
|
or pdata.get("Kills")
|
|
|
|
|
or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
career["deaths"] = (
|
|
|
|
|
career.get("deaths")
|
|
|
|
|
or career.get("Deaths")
|
|
|
|
|
or pdata.get("Deaths")
|
|
|
|
|
or 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
career["maps"] = (
|
|
|
|
|
career.get("maps")
|
|
|
|
|
or pdata.get("maps")
|
|
|
|
|
or pdata.get("career", {}).get("maps")
|
|
|
|
|
or 1
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
pdata["career"] = career
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-10 08:08:47 +00:00
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# 4. Compute league metrics for ALL TAGS
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
league_averages, distributions = compute_league_metrics(all_players)
|
|
|
|
|
clutch_averages, clutch_distribution = compute_league_clutch_metrics(all_players)
|
2026-04-09 10:11:39 +00:00
|
|
|
|
2026-04-10 08:08:47 +00:00
|
|
|
# Merge clutch averages into league averages
|
|
|
|
|
league_averages.update(clutch_averages)
|
2026-04-09 10:11:39 +00:00
|
|
|
|
|
|
|
|
final_db["league_averages"] = league_averages
|
2026-04-15 06:15:07 +00:00
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
|
|
|
|
|
|
2026-04-15 06:15:07 +00:00
|
|
|
# ---------------------------------------------------------
|
2026-04-21 08:55:26 +00:00
|
|
|
# 4.6 Build Breaker distribution
|
2026-04-15 06:15:07 +00:00
|
|
|
# ---------------------------------------------------------
|
2026-04-21 08:55:26 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
breaker_distribution = []
|
2026-04-15 06:15:07 +00:00
|
|
|
for pid, pdata in final_db["players"].items():
|
2026-04-21 08:55:26 +00:00
|
|
|
career = pdata["career"]
|
|
|
|
|
damage_per_map = career["damage"] / max(1, career["maps"])
|
|
|
|
|
kills_per_map = career["kills"] / max(1, career["maps"])
|
2026-04-15 06:15:07 +00:00
|
|
|
|
2026-04-21 08:55:26 +00:00
|
|
|
raw_breaker = compute_breaker_raw(damage_per_map, kills_per_map)
|
|
|
|
|
breaker_distribution.append(raw_breaker)
|
2026-04-09 10:11:39 +00:00
|
|
|
|
2026-04-15 06:15:07 +00:00
|
|
|
|
2026-04-09 10:11:39 +00:00
|
|
|
# ---------------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
# 5. Compute all tags using distributions
|
2026-04-09 10:11:39 +00:00
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
for pid, pdata in final_db["players"].items():
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-23 09:24:48 +00:00
|
|
|
# -----------------------------------------------------
|
2026-04-08 08:13:02 +00:00
|
|
|
# Slayer
|
2026-04-23 09:24:48 +00:00
|
|
|
# -----------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
pdata["slayer"] = compute_slayer_for_career_player(
|
|
|
|
|
pdata,
|
|
|
|
|
league_averages,
|
|
|
|
|
distributions["slayer"]
|
|
|
|
|
)
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-23 09:24:48 +00:00
|
|
|
# -----------------------------------------------------
|
2026-04-08 08:13:02 +00:00
|
|
|
# Sharpshooter
|
2026-04-23 09:24:48 +00:00
|
|
|
# -----------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
pdata["sharpshooter"] = compute_sharpshooter_for_career_player(
|
|
|
|
|
pdata["career"],
|
|
|
|
|
league_averages,
|
|
|
|
|
distributions["sharpshooter"]
|
|
|
|
|
)
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-23 09:24:48 +00:00
|
|
|
# -----------------------------------------------------
|
2026-04-08 08:13:02 +00:00
|
|
|
# Payload Objective Specialist
|
2026-04-23 09:24:48 +00:00
|
|
|
# -----------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
pdata["objective_payload"] = compute_payload_tag(
|
|
|
|
|
pdata,
|
|
|
|
|
league_averages,
|
|
|
|
|
distributions["payload"]
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-23 09:24:48 +00:00
|
|
|
# -----------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
# Domination Objective Specialist
|
2026-04-23 09:24:48 +00:00
|
|
|
# -----------------------------------------------------
|
2026-04-15 06:15:07 +00:00
|
|
|
pdata["objective_domination"] = compute_dom_objective_for_career_player(
|
2026-04-10 08:08:47 +00:00
|
|
|
pdata,
|
|
|
|
|
league_averages,
|
|
|
|
|
distributions["domination"]
|
2026-04-09 10:11:39 +00:00
|
|
|
)
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-14 07:32:50 +00:00
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# Consistency (Hybrid Career Tag - New System)
|
|
|
|
|
# ---------------------------------------------------------
|
2026-04-09 10:11:39 +00:00
|
|
|
matches = pdata["matches"]
|
2026-04-14 07:32:50 +00:00
|
|
|
maps_played = pdata["career"].get("maps", 0)
|
|
|
|
|
|
|
|
|
|
per_match_scores = []
|
|
|
|
|
for m in matches:
|
|
|
|
|
dmg = m.get("Damage", 0)
|
|
|
|
|
kills = m.get("Kills", 0)
|
|
|
|
|
deaths = m.get("Deaths", 0)
|
|
|
|
|
kd = kills / deaths if deaths > 0 else kills
|
|
|
|
|
|
|
|
|
|
slayer_component = (
|
|
|
|
|
(dmg / 10000) * 0.40 +
|
|
|
|
|
(kills / 30) * 0.40 +
|
|
|
|
|
(kd / 5) * 0.20
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
push = m.get("PAY_PushTime", 0)
|
|
|
|
|
caps = m.get("DOM_Captures", 0)
|
|
|
|
|
counters = m.get("DOM_Counters", 0)
|
|
|
|
|
|
|
|
|
|
objective_component = (
|
|
|
|
|
(push / 300) * 0.40 +
|
|
|
|
|
(caps / 20) * 0.30 +
|
|
|
|
|
(counters / 20) * 0.30
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
shots = m.get("Shots", 0)
|
|
|
|
|
shots_hit = m.get("ShotsHit", 0)
|
|
|
|
|
accuracy = (shots_hit / shots) if shots > 0 else 0.0
|
|
|
|
|
accuracy_component = accuracy * 0.20
|
|
|
|
|
|
|
|
|
|
perf = slayer_component + objective_component + accuracy_component
|
|
|
|
|
per_match_scores.append(perf)
|
|
|
|
|
|
|
|
|
|
if per_match_scores:
|
|
|
|
|
floor_value = min(per_match_scores)
|
|
|
|
|
avg_value = sum(per_match_scores) / len(per_match_scores)
|
|
|
|
|
|
|
|
|
|
mean = avg_value
|
|
|
|
|
variance = sum((x - mean) ** 2 for x in per_match_scores) / len(per_match_scores)
|
|
|
|
|
adjusted_variance = variance * (1 + 1 / max(1, len(per_match_scores)))
|
|
|
|
|
stability_value = 1 / (1 + adjusted_variance)
|
|
|
|
|
else:
|
|
|
|
|
floor_value = 0.0
|
|
|
|
|
avg_value = 0.0
|
|
|
|
|
stability_value = 0.0
|
|
|
|
|
|
|
|
|
|
consistency_result = build_consistency_tag(
|
|
|
|
|
floor_current=floor_value,
|
2026-04-23 09:24:48 +00:00
|
|
|
floor_career=floor_value,
|
2026-04-14 07:32:50 +00:00
|
|
|
floor_league=league_averages.get("consistency_floor", 0.0),
|
|
|
|
|
|
|
|
|
|
stability_current=stability_value,
|
|
|
|
|
stability_career=stability_value,
|
|
|
|
|
stability_league=league_averages.get("consistency_stability", 0.0),
|
|
|
|
|
|
|
|
|
|
average_current=avg_value,
|
|
|
|
|
average_career=avg_value,
|
|
|
|
|
average_league=league_averages.get("consistency_average", 0.0),
|
|
|
|
|
|
|
|
|
|
matches_played_current=maps_played,
|
|
|
|
|
percentile=None
|
2026-04-10 08:08:47 +00:00
|
|
|
)
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-14 07:32:50 +00:00
|
|
|
pdata["consistency"] = {
|
|
|
|
|
"raw": consistency_result.normalized_score,
|
2026-04-23 09:24:48 +00:00
|
|
|
"pct": None,
|
2026-04-14 07:32:50 +00:00
|
|
|
"tier": consistency_result.tier,
|
|
|
|
|
"summary": consistency_result.summary_short,
|
|
|
|
|
"components": {
|
|
|
|
|
"floor": consistency_result.components.floor,
|
|
|
|
|
"stability": consistency_result.components.stability,
|
|
|
|
|
"average": consistency_result.components.average_performance,
|
|
|
|
|
},
|
|
|
|
|
"extras": consistency_result.extras,
|
|
|
|
|
}
|
2026-04-23 09:24:48 +00:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# Career Consistency Percentile + Tier (NEW)
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
raw_cons = pdata["consistency"]["raw"]
|
|
|
|
|
cons_dist = distributions.get("consistency", [])
|
|
|
|
|
|
|
|
|
|
pct = percentile_rank(raw_cons, cons_dist)
|
|
|
|
|
tier = tier_from_percentile(pct)
|
|
|
|
|
|
|
|
|
|
pdata["consistency"]["pct"] = pct
|
|
|
|
|
pdata["consistency"]["tier"] = tier
|
|
|
|
|
pdata["consistency"]["summary"] = (
|
|
|
|
|
f"{tier} Tier Consistency ({pct:.1f} percentile)"
|
|
|
|
|
)
|
2026-04-14 07:32:50 +00:00
|
|
|
|
2026-04-23 09:24:48 +00:00
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# Anchor (requires consistency)
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
consistency_norm = pdata["consistency"]["raw"]
|
|
|
|
|
pdata["anchor"] = compute_anchor_for_career_player(
|
|
|
|
|
pdata,
|
|
|
|
|
league_averages,
|
|
|
|
|
distributions["anchor"],
|
|
|
|
|
consistency_norm
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# Clutch
|
|
|
|
|
# ---------------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
pdata["clutch"] = compute_clutch(
|
|
|
|
|
pdata,
|
|
|
|
|
league_averages,
|
|
|
|
|
clutch_distribution
|
|
|
|
|
)
|
2026-04-08 08:13:02 +00:00
|
|
|
|
2026-04-23 09:24:48 +00:00
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
# Breaker
|
|
|
|
|
# ---------------------------------------------------------
|
|
|
|
|
career = pdata["career"]
|
|
|
|
|
damage_per_map = career["damage"] / max(1, career["maps"])
|
|
|
|
|
kills_per_map = career["kills"] / max(1, career["maps"])
|
|
|
|
|
|
|
|
|
|
raw_breaker = compute_breaker_raw(damage_per_map, kills_per_map)
|
|
|
|
|
pct = percentile_rank(raw_breaker, breaker_distribution)
|
|
|
|
|
tier = tier_from_percentile(pct)
|
|
|
|
|
|
|
|
|
|
pdata["breaker"] = {
|
|
|
|
|
"raw": raw_breaker,
|
|
|
|
|
"pct": pct,
|
|
|
|
|
"tier": tier,
|
|
|
|
|
"summary": f"Offensive disruptor ({tier} Tier, {pct:.1f} percentile)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Save PIR with first_season stamped for all canonical IDs
|
|
|
|
|
pir.save()
|
|
|
|
|
|
2026-04-09 10:11:39 +00:00
|
|
|
# ---------------------------------------------------------
|
2026-04-10 08:08:47 +00:00
|
|
|
# 6. Save DB
|
2026-04-09 10:11:39 +00:00
|
|
|
# ---------------------------------------------------------
|
2026-04-08 08:13:02 +00:00
|
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
|
|
|
json.dump(final_db, f, indent=2)
|
|
|
|
|
|
|
|
|
|
return True
|