204 lines
9.4 KiB
Python
204 lines
9.4 KiB
Python
"""Der MetaTFT-Parser muss dieselben Tabellen liefern wie der cdragon-Parser.
|
|
|
|
Die Fixtures sind gekürzte, aber echte Ausschnitte aus
|
|
data.metatft.com/lookups/TFTSet18_latest_en_us.json.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from tft.staticdata import metatft
|
|
|
|
|
|
@pytest.fixture
|
|
def raw():
|
|
return {
|
|
"_metadata": {"set": "TFTSet18", "setName": "Enchanted Wilds", "patch": "pbe"},
|
|
"roles": {"DA_Role_AttackCaster": "Attack Caster"},
|
|
"units": [
|
|
{
|
|
"apiName": "TFT18_Ashe", "characterName": "TFT18_Ashe", "name": "Ashe",
|
|
"cost": 5, "shopUnit": True, "role": "DA_Role_AttackCaster",
|
|
"traits": ["Blossom", "Hunter"],
|
|
"traitApiNames": ["DA_18_Blossom", "DA_18_Hunter"],
|
|
"stats": {"hp": 900, "damage": 75, "armor": 45, "magicResist": 45,
|
|
"attackSpeed": 0.8, "mana": 80, "initialMana": 20,
|
|
"critChance": 0.25, "critMultiplier": 1.4, "range": 5},
|
|
"curveValues": {"AbilityShield": [[1, 60], [2, 90], [3, 200], [4, 310]],
|
|
"ChillPercent": [[1, 20], [4, 20]]},
|
|
"ability": {
|
|
"name": "Spirit Rift",
|
|
# Wie im Original: erst ein Mana-Tag, dann zwei Schadenszahlen.
|
|
"desc": ('Costs <TFTAttribute attributeID="TFTCalculationAttributes.ManaCalc1"'
|
|
' style="colorStat" icon="icon.AP"/> mana. Deals'
|
|
' <TFTAttribute attributeID="TFTCalculationAttributes.PhysicalDamageCalc2"'
|
|
' style="colorPhysical" icon="icon.AD"/> then'
|
|
' <TFTAttribute attributeID="TFTCalculationAttributes.PhysicalDamageCalc1"'
|
|
' style="colorPhysical" icon="icon.AD"/> physical damage,'
|
|
' shields for <TFTCurveTable row="AbilityShield"/>, slowing by'
|
|
' <TFTCurveTable row="ChillPercent"/>%.'),
|
|
"variables": [],
|
|
"attributeValues": {
|
|
"TFTCalculationAttributes.ManaCalc1": [3, 3, 100, 100],
|
|
"TFTCalculationAttributes.PhysicalDamageCalc1": [440, 660, 1000, 1000],
|
|
"TFTCalculationAttributes.PhysicalDamageCalc2": [7, 11, 220, 220],
|
|
},
|
|
},
|
|
},
|
|
{ # Verwandlungsform: kein shopUnit, gehört nicht in den Pool.
|
|
"apiName": "TFT18_EliseSpider", "name": "Elise", "cost": 2,
|
|
"shopUnit": False, "traits": ["Coven"], "traitApiNames": ["DA_18_Coven"],
|
|
"stats": {"hp": 700}, "ability": {},
|
|
},
|
|
{ # Lücke im PBE-Dump: ohne HP wäre die eHP 0.
|
|
"apiName": "TFT18_Ivern", "name": "Ivern", "cost": 5, "shopUnit": True,
|
|
"traits": ["Greenfather"], "traitApiNames": ["DA_18_Greenfather"],
|
|
"stats": {"damage": 60, "armor": 55}, "ability": {},
|
|
},
|
|
],
|
|
"traits": [
|
|
{"apiName": "DA_18_Brawler", "name": "Brawler", "type": "class",
|
|
"icon": "T_TraitIcon_18_Brawler",
|
|
"effects": [
|
|
{"minUnits": 2, "style": 1,
|
|
"desc": '(2) <TFTCurveTable row="BrawlerHealthPercent" column="1"'
|
|
' format="percent"/>'},
|
|
{"minUnits": 4, "style": 3,
|
|
"desc": '(4) <TFTCurveTable row="BrawlerHealthPercent" column="2"'
|
|
' format="percent"/>'},
|
|
],
|
|
"curveValues": {"BrawlerHealthPercent": [[0, 0], [1, 0.25], [2, 0.4]]}},
|
|
{"apiName": "DA_18_Blossom", "name": "Blossom", "type": "origin",
|
|
"icon": "T_TraitIcon_18_Blossom", "effects": [], "curveValues": {}},
|
|
{"apiName": "DA_18_Hunter", "name": "Hunter", "type": "class",
|
|
"icon": "T_TraitIcon_18_Hunter", "effects": [], "curveValues": {}},
|
|
],
|
|
"items": [
|
|
{"apiName": "DA_Component_RecurveBow", "name": "Recurve Bow", "composition": [],
|
|
"icon": "T_RecurveBow",
|
|
"statLine": '<TFTCurveTable row="AS" icon="icon.AS" format="percentMinusOne"/>',
|
|
"curveValues": {"AS": [[1, 1.1]]}},
|
|
{"apiName": "DA_Component_NeedlesslyLargeRod", "name": "Rod", "composition": [],
|
|
"icon": "T_Rod",
|
|
"statLine": '<TFTCurveTable row="AP" icon="icon.AP" format="percent"/>',
|
|
"curveValues": {"AP": [[1, 0.1]]}},
|
|
{"apiName": "DA_Component_GiantsBelt", "name": "Belt", "composition": [],
|
|
"icon": "T_Belt",
|
|
"statLine": '<TFTCurveTable row="Health" icon="icon.Health"/>',
|
|
"curveValues": {"Health": [[1, 150]]}},
|
|
],
|
|
"augments": [
|
|
{"apiName": "DA_AdvancedLoan", "name": "Advanced Loan", "rarity": "Gold",
|
|
"icon": "T_AugmentIcon_AdvancedLoan",
|
|
"desc": 'Gain <TFTCurveTable row="GoldInstantBasic"/> Gold.',
|
|
"curveValues": {"GoldInstantBasic": [[1, 20]]}},
|
|
],
|
|
}
|
|
|
|
|
|
def test_meta_and_unit_filter(raw):
|
|
tables = metatft.parse(raw)
|
|
assert tables["meta"]["set"] == 18
|
|
assert tables["meta"]["set_name"] == "Enchanted Wilds"
|
|
# EliseSpider ist kein Shop-Unit, Ivern hat keine HP.
|
|
assert list(tables["units"]) == ["TFT18_Ashe"]
|
|
assert tables["meta"]["incomplete_units"] == ["Ivern (hp)"]
|
|
|
|
|
|
def test_missing_attack_damage_is_reported_but_kept(raw):
|
|
"""Ohne AD ist der Auto-DPS 0 — die Unit wird trotzdem gespielt.
|
|
|
|
Anders als bei fehlender HP fliegt sie deshalb nicht aus dem Pool.
|
|
"""
|
|
del raw["units"][0]["stats"]["damage"]
|
|
tables = metatft.parse(raw)
|
|
assert "TFT18_Ashe" in tables["units"]
|
|
assert "Ashe (damage)" in tables["meta"]["incomplete_units"]
|
|
|
|
|
|
def test_unit_matches_cdragon_shape(raw):
|
|
unit = metatft.parse(raw)["units"]["TFT18_Ashe"]
|
|
assert unit["traits"] == ["DA_18_Blossom", "DA_18_Hunter"]
|
|
assert unit["role"] == "AttackCaster"
|
|
assert unit["stats"]["hp"] == 900
|
|
# Der erste Tag im Text ist die Manazahl 3 — der Schaden ist 440.
|
|
# cdragon zählt Sterne ab Index 1; MetaTFT ab 0.
|
|
assert unit["spell_damage"] == [0, 440, 660, 1000]
|
|
assert unit["spell_damage_type"] == "physical"
|
|
assert unit["spell_scaling"] == "ad"
|
|
# Kurven sind Treppen: Stufe 4 fehlt, Stufe 1 gilt weiter.
|
|
assert unit["ability_vars"]["AbilityShield"] == [0, 60, 90, 200]
|
|
assert unit["ability_vars"]["ChillPercent"] == [0, 20, 20, 20]
|
|
|
|
|
|
def test_trait_breakpoints_carry_their_curve_values(raw):
|
|
brawler = metatft.parse(raw)["traits"]["DA_18_Brawler"]
|
|
assert [b["min_units"] for b in brawler["breakpoints"]] == [2, 4]
|
|
assert brawler["breakpoints"][0]["variables"] == {"BrawlerHealthPercent": 0.25}
|
|
assert brawler["breakpoints"][1]["variables"] == {"BrawlerHealthPercent": 0.4}
|
|
|
|
|
|
def test_unknown_traits_are_dropped(raw):
|
|
# Greenfather steht in keiner Trait-Tabelle; ein Verweis darauf würde
|
|
# active_trait_tiers durchlaufen und im UI als Geist-Trait auftauchen.
|
|
raw["units"][0]["traitApiNames"].append("DA_18_DoesNotExist")
|
|
assert metatft.parse(raw)["units"]["TFT18_Ashe"]["traits"] == [
|
|
"DA_18_Blossom", "DA_18_Hunter"]
|
|
|
|
|
|
def test_item_effects_follow_statsheet_conventions(raw):
|
|
items = metatft.parse(raw)["items"]
|
|
# AS und AP sind Prozentzahlen, Health ist flach — wie in Set 17.
|
|
assert items["DA_Component_RecurveBow"]["effects"] == {"AS": pytest.approx(10.0)}
|
|
assert items["DA_Component_NeedlesslyLargeRod"]["effects"] == {"AP": pytest.approx(10.0)}
|
|
assert items["DA_Component_GiantsBelt"]["effects"] == {"Health": 150}
|
|
|
|
|
|
def test_augment_tier_and_rendered_desc(raw):
|
|
aug = metatft.parse(raw)["augments"]["DA_AdvancedLoan"]
|
|
assert aug["tier"] == 2 # Gold
|
|
assert aug["desc"] == "Gain 20 Gold."
|
|
assert aug["effects"] == {"GoldInstantBasic": 20}
|
|
|
|
|
|
def test_curve_steps_hold_the_last_value():
|
|
curve = [[1, 5], [3, 9]]
|
|
assert [metatft.curve_at(curve, i) for i in (1, 2, 3, 4)] == [5, 5, 9, 9]
|
|
assert metatft.curve_at([], 1) is None
|
|
|
|
|
|
@pytest.mark.parametrize("value,fmt,want", [
|
|
(0.25, "percent", 0.25),
|
|
(1.10, "percentMinusOne", pytest.approx(0.10)),
|
|
# Durability wird als Resthärte angegeben: 0.96 = 4% weniger Schaden.
|
|
(0.96, "invertedPercent", pytest.approx(0.04)),
|
|
(175, None, 175),
|
|
])
|
|
def test_format_drives_the_conversion(value, fmt, want):
|
|
assert metatft.as_fraction(value, fmt) == want
|
|
|
|
|
|
def test_spell_picks_the_biggest_damage_not_the_first_tag(raw):
|
|
"""Lux nennt im Tooltip zuerst 3 Mana, dann 330 Schaden.
|
|
|
|
Wer den ersten <TFTAttribute> nimmt, modelliert die Manazahl als
|
|
Schaden — das betraf 33 von 63 Units.
|
|
"""
|
|
unit = metatft.parse(raw)["units"]["TFT18_Ashe"]
|
|
assert unit["spell_damage"][1] == 440
|
|
assert unit["spell_scaling"] == "ad"
|
|
assert unit["spell_damage_type"] == "physical"
|
|
|
|
|
|
def test_on_attack_damage_is_flagged(raw):
|
|
ability = raw["units"][0]["ability"]
|
|
ability["desc"] = ('Attacks deal <TFTAttribute'
|
|
' attributeID="TFTCalculationAttributes.PhysicalDamageCalc1"'
|
|
' style="colorPhysical" icon="icon.AD"/> bonus damage.')
|
|
assert metatft.parse(raw)["units"]["TFT18_Ashe"]["spell_on_attack"] is True
|
|
|
|
|
|
def test_asset_names_are_kept_for_the_match_api(raw):
|
|
# Riot nennt Units beim Asset-Namen; ohne die Liste findet learn.py nichts.
|
|
raw["units"][0]["assetNames"] = ["DA_18_Ashe"]
|
|
assert metatft.parse(raw)["units"]["TFT18_Ashe"]["asset_names"] == ["DA_18_Ashe"]
|