"""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 mana. Deals' ' then' ' physical damage,' ' shields for , slowing by' ' %.'), "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) '}, {"minUnits": 4, "style": 3, "desc": '(4) '}, ], "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": '', "curveValues": {"AS": [[1, 1.1]]}}, {"apiName": "DA_Component_NeedlesslyLargeRod", "name": "Rod", "composition": [], "icon": "T_Rod", "statLine": '', "curveValues": {"AP": [[1, 0.1]]}}, {"apiName": "DA_Component_GiantsBelt", "name": "Belt", "composition": [], "icon": "T_Belt", "statLine": '', "curveValues": {"Health": [[1, 150]]}}, ], "augments": [ {"apiName": "DA_AdvancedLoan", "name": "Advanced Loan", "rarity": "Gold", "icon": "T_AugmentIcon_AdvancedLoan", "desc": 'Gain 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 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 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"]