A/B auf 38 Holdout-Matches: mechanical 0.724 vs legacy 0.695. Promotion durchgeführt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
150 lines
4.6 KiB
Python
150 lines
4.6 KiB
Python
"""Normalize the raw Community Dragon TFT JSON into flat unit/trait/item tables.
|
|
|
|
Fails loudly on missing keys — never guess through structure drift.
|
|
"""
|
|
|
|
import re
|
|
|
|
CDRAGON_GAME = "https://raw.communitydragon.org/latest/game"
|
|
|
|
DAMAGE_TAG_RE = re.compile(
|
|
r"<(magicDamage|physicalDamage|trueDamage)>(.*?)</\1>", re.S
|
|
)
|
|
VAR_RE = re.compile(r"@([A-Za-z0-9_]+?)(?:\*[\d.]+)?@")
|
|
# Reihenfolge = Priorität; ADDamage/APDamage werden gesondert addiert.
|
|
DAMAGE_FALLBACKS = (
|
|
"Damage", "DamageAP", "DamageAD", "SpellDamage",
|
|
"DamagePerSecond", "TrueDamagePerSecond", "BonusDamageOnAttack",
|
|
)
|
|
TAG_TO_TYPE = {"magicDamage": "magic", "physicalDamage": "physical", "trueDamage": "true"}
|
|
|
|
|
|
def resolve_spell(ability: dict) -> dict:
|
|
"""Spell-Schaden pro Stern aus Desc-Markup + Variablen auflösen.
|
|
|
|
Stern-Konvention der Arrays: value[1..3] = 1-3 Sterne (verifiziert).
|
|
"""
|
|
desc = ability.get("desc") or ""
|
|
variables = {v["name"]: v["value"] for v in ability.get("variables", [])
|
|
if v.get("value")}
|
|
|
|
def get(name: str):
|
|
val = variables.get(name)
|
|
return val if val and any(val) else None
|
|
|
|
dmg_type = None
|
|
array = None
|
|
for m in DAMAGE_TAG_RE.finditer(desc):
|
|
for vm in VAR_RE.finditer(m.group(2)):
|
|
name = vm.group(1)
|
|
array = get(name) or get(name.removeprefix("Modified"))
|
|
if array:
|
|
dmg_type = TAG_TO_TYPE[m.group(1)]
|
|
break
|
|
if array:
|
|
break
|
|
|
|
if array is None:
|
|
ad, ap = get("ADDamage"), get("APDamage")
|
|
if ad and ap:
|
|
array = [a + b for a, b in zip(ad, ap)]
|
|
else:
|
|
for name in DAMAGE_FALLBACKS:
|
|
array = get(name)
|
|
if array:
|
|
break
|
|
|
|
has_ap = "%i:scaleAP%" in desc
|
|
has_ad = "%i:scaleAD%" in desc
|
|
scaling = "both" if has_ap and has_ad else "ap" if has_ap else "ad" if has_ad else None
|
|
|
|
return {
|
|
"spell_damage": array,
|
|
"spell_damage_type": dmg_type or ("magic" if array else None),
|
|
"spell_scaling": scaling,
|
|
}
|
|
|
|
|
|
def icon_url(asset_path: str) -> str:
|
|
p = asset_path.lower()
|
|
for ext in (".tex", ".dds"):
|
|
if p.endswith(ext):
|
|
p = p[: -len(ext)] + ".png"
|
|
return f"{CDRAGON_GAME}/{p}"
|
|
|
|
|
|
def detect_set(raw: dict) -> int:
|
|
candidates = [
|
|
int(k) for k, v in raw["sets"].items() if v.get("champions")
|
|
]
|
|
if not candidates:
|
|
raise ValueError("no set with champions found in cdragon data")
|
|
return max(candidates)
|
|
|
|
|
|
def parse(raw: dict, set_override: int | None = None) -> dict:
|
|
set_number = set_override if set_override is not None else detect_set(raw)
|
|
set_data = raw["sets"][str(set_number)]
|
|
|
|
units = {}
|
|
for c in set_data["champions"]:
|
|
if not c["traits"] or "Minion" in c["apiName"]:
|
|
continue # PvE monsters, summons (z.B. TFT17_IvernMinion)
|
|
units[c["apiName"]] = {
|
|
"api_name": c["apiName"],
|
|
"name": c["name"],
|
|
"cost": c["cost"],
|
|
"traits": c["traits"],
|
|
"role": c.get("role"),
|
|
"stats": c["stats"],
|
|
"ability_name": c["ability"]["name"],
|
|
"icon": icon_url(c["squareIcon"]),
|
|
**resolve_spell(c["ability"]),
|
|
}
|
|
|
|
traits = {}
|
|
trait_name_to_api = {}
|
|
for t in set_data["traits"]:
|
|
traits[t["apiName"]] = {
|
|
"api_name": t["apiName"],
|
|
"name": t["name"],
|
|
"breakpoints": [
|
|
{"min_units": e["minUnits"], "style": e["style"],
|
|
"variables": e.get("variables") or {}}
|
|
for e in t["effects"]
|
|
],
|
|
"icon": icon_url(t["icon"]),
|
|
}
|
|
trait_name_to_api[t["name"]] = t["apiName"]
|
|
|
|
# Champion traits are display names; map them to api names.
|
|
for u in units.values():
|
|
u["traits"] = [trait_name_to_api[name] for name in u["traits"]]
|
|
|
|
set_prefix = f"TFT{set_number}_"
|
|
items = {}
|
|
augments = {}
|
|
for i in raw["items"]:
|
|
api = i["apiName"]
|
|
if not (api.startswith(set_prefix) or api.startswith("TFT_Item_")):
|
|
continue
|
|
entry = {
|
|
"api_name": api,
|
|
"name": i["name"],
|
|
"composition": i["composition"],
|
|
"effects": i.get("effects") or {},
|
|
"icon": icon_url(i["icon"]) if i["icon"] else None,
|
|
}
|
|
if "Augment" in api:
|
|
augments[api] = entry
|
|
else:
|
|
items[api] = entry
|
|
|
|
return {
|
|
"meta": {"set": set_number, "set_name": set_data["name"]},
|
|
"units": units,
|
|
"traits": traits,
|
|
"items": items,
|
|
"augments": augments,
|
|
}
|