Augments: Tier aus Icon-Suffix, eine Stufe pro Runde, Evergreen-Pool, Beschreibungen gerendert, Karten-UI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -208,3 +208,16 @@ def test_natural_level_progression(art, cfg):
|
|||||||
assert checks["2-1"] == (3, 2)
|
assert checks["2-1"] == (3, 2)
|
||||||
assert checks["2-5"] == (4, 2)
|
assert checks["2-5"] == (4, 2)
|
||||||
assert checks["3-2"] == (5, 0)
|
assert checks["3-2"] == (5, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_augment_offer_single_tier(art, cfg):
|
||||||
|
game = Game(art, cfg, seed=9)
|
||||||
|
mixed = {f"AUG_T{t}_{i}": {"tier": t} for t in (1, 2, 3) for i in range(4)}
|
||||||
|
game.artifact = {**art, "static": {**art["static"], "augments": mixed},
|
||||||
|
"augment_pool": []}
|
||||||
|
game.idx = next(i for i, r in enumerate(game.rounds) if r["augment"])
|
||||||
|
game.player.augment_offer = []
|
||||||
|
game._maybe_offer_augment()
|
||||||
|
tiers = {mixed[a]["tier"] for a in game.player.augment_offer}
|
||||||
|
assert len(game.player.augment_offer) == 3
|
||||||
|
assert len(tiers) == 1 # alle Angebote aus derselben Stufe
|
||||||
|
|||||||
@@ -75,3 +75,18 @@ def test_resolve_spell_adaptive_and_fallback():
|
|||||||
utility = {"desc": "Grant a shield.", "variables": [
|
utility = {"desc": "Grant a shield.", "variables": [
|
||||||
{"name": "Shield", "value": [0, 300, 400, 500, 0, 0, 0]}]}
|
{"name": "Shield", "value": [0, 300, 400, 500, 0, 0, 0]}]}
|
||||||
assert resolve_spell(utility)["spell_damage"] is None
|
assert resolve_spell(utility)["spell_damage"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_augment_tier_from_icon():
|
||||||
|
from tft.staticdata.parse import augment_tier
|
||||||
|
assert augment_tier("ASSETS/x/GodAugmentEkko_II.TFT_Set17.tex") == 2
|
||||||
|
assert augment_tier("ASSETS/x/LeonaHero_I.TFT_Set17.tex") == 1
|
||||||
|
assert augment_tier("ASSETS/x/Big_III.tex") == 3
|
||||||
|
assert augment_tier("ASSETS/x/NoSuffix.tex") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_desc():
|
||||||
|
from tft.staticdata.parse import render_desc
|
||||||
|
desc = "Gain <TFTBonus>@Gold@</TFTBonus> gold and @ADAP*100@% AD.<br>Rest."
|
||||||
|
assert render_desc(desc, {"Gold": 4, "ADAP": 0.15}) == "Gain 4 gold and 15% AD. Rest."
|
||||||
|
assert render_desc("@Unknown@ gold", {}) == "? gold"
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ def serialize(game: Game) -> dict:
|
|||||||
"traits": traits_panel,
|
"traits": traits_panel,
|
||||||
"augment_offer": [
|
"augment_offer": [
|
||||||
{"api_name": a, "name": static["augments"].get(a, {}).get("name", a),
|
{"api_name": a, "name": static["augments"].get(a, {}).get("name", a),
|
||||||
|
"desc": static["augments"].get(a, {}).get("desc", ""),
|
||||||
|
"tier": static["augments"].get(a, {}).get("tier"),
|
||||||
"icon": static["augments"].get(a, {}).get("icon")}
|
"icon": static["augments"].get(a, {}).get("icon")}
|
||||||
for a in game.augment_offer
|
for a in game.augment_offer
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ stage1 = ["carousel", "pve", "pve", "pve"]
|
|||||||
stage_n = ["pvp", "pvp", "pvp", "carousel", "pvp", "pvp", "pve"]
|
stage_n = ["pvp", "pvp", "pvp", "carousel", "pvp", "pvp", "pve"]
|
||||||
augment_rounds = ["2-1", "3-2", "4-2"]
|
augment_rounds = ["2-1", "3-2", "4-2"]
|
||||||
|
|
||||||
|
[augments]
|
||||||
|
# Tier-Gewichte [Silber, Gold, Prismatisch] pro Augment-Runde.
|
||||||
|
# Alle drei Angebote einer Runde haben dieselbe Stufe (wie im Spiel).
|
||||||
|
tier_weights = { "2-1" = [50, 45, 5], "3-2" = [35, 50, 15], "4-2" = [20, 55, 25] }
|
||||||
|
|
||||||
[loot]
|
[loot]
|
||||||
# Vereinfachtes Loot: PvE-Drops als [components, gold].
|
# Vereinfachtes Loot: PvE-Drops als [components, gold].
|
||||||
stage1_pve = [1, 1] # pro Stage-1-PvE-Runde
|
stage1_pve = [1, 1] # pro Stage-1-PvE-Runde
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ def build(static: dict, learned: dict | None = None, extra_meta: dict | None = N
|
|||||||
"stat_mults": baseline.compute_stat_mults(static["units"]),
|
"stat_mults": baseline.compute_stat_mults(static["units"]),
|
||||||
"tier_profiles": tier_profiles(static, roles),
|
"tier_profiles": tier_profiles(static, roles),
|
||||||
"item_pool": craftable_items(static["items"]),
|
"item_pool": craftable_items(static["items"]),
|
||||||
|
# Riot-Match-API liefert keine Augments (Feld entfernt) — leer heißt:
|
||||||
|
# der Sim nutzt alle geparsten Augments als Pool.
|
||||||
|
"augment_pool": [],
|
||||||
"learned": learned or {},
|
"learned": learned or {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,15 +83,23 @@ class Game:
|
|||||||
def _maybe_offer_augment(self) -> None:
|
def _maybe_offer_augment(self) -> None:
|
||||||
if not self.round.get("augment"):
|
if not self.round.get("augment"):
|
||||||
return
|
return
|
||||||
available = [a for a in self.artifact["static"]["augments"]
|
augments = self.artifact["static"]["augments"]
|
||||||
if a not in self.player.augments]
|
pool = [a for a in (self.artifact.get("augment_pool") or list(augments))
|
||||||
|
if a in augments]
|
||||||
|
# Pro Runde eine Stufe rollen; alle Angebote (Spieler + Bots) kommen daraus.
|
||||||
|
weights = self.cfg.get("augments", {}).get("tier_weights", {}).get(
|
||||||
|
self.round["label"], [35, 50, 15]
|
||||||
|
)
|
||||||
|
tier = self.rng.choices((1, 2, 3), weights=weights)[0]
|
||||||
|
tier_pool = [a for a in pool if (augments[a].get("tier") or 2) == tier] or pool
|
||||||
|
|
||||||
|
available = [a for a in tier_pool if a not in self.player.augments]
|
||||||
if available and not self.player.augment_offer:
|
if available and not self.player.augment_offer:
|
||||||
self.player.augment_offer = self.rng.sample(available, min(3, len(available)))
|
self.player.augment_offer = self.rng.sample(available, min(3, len(available)))
|
||||||
for b in self.bots:
|
for b in self.bots:
|
||||||
if not b.alive:
|
if not b.alive:
|
||||||
continue
|
continue
|
||||||
choices = [a for a in self.artifact["static"]["augments"]
|
choices = [a for a in tier_pool if a not in b.augments]
|
||||||
if a not in b.augments]
|
|
||||||
if choices:
|
if choices:
|
||||||
b.augments.append(self.rng.choice(choices))
|
b.augments.append(self.rng.choice(choices))
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,34 @@ DAMAGE_FALLBACKS = (
|
|||||||
)
|
)
|
||||||
TAG_TO_TYPE = {"magicDamage": "magic", "physicalDamage": "physical", "trueDamage": "true"}
|
TAG_TO_TYPE = {"magicDamage": "magic", "physicalDamage": "physical", "trueDamage": "true"}
|
||||||
|
|
||||||
|
# Augment-Stufe steckt im Icon-Dateinamen: _I / _II / _III.
|
||||||
|
AUGMENT_TIER_RE = re.compile(r"[-_](I{1,3})\.", re.I)
|
||||||
|
PLACEHOLDER_RE = re.compile(r"@([A-Za-z0-9_]+)(?:\*([\d.]+))?@")
|
||||||
|
|
||||||
|
|
||||||
|
def augment_tier(icon_path: str | None) -> int | None:
|
||||||
|
m = AUGMENT_TIER_RE.search(icon_path or "")
|
||||||
|
return len(m.group(1)) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def render_desc(desc: str | None, effects: dict) -> str:
|
||||||
|
"""Tooltip-Platzhalter mit Effekt-Werten füllen, Markup entfernen."""
|
||||||
|
lowered = {k.lower(): v for k, v in (effects or {}).items()
|
||||||
|
if isinstance(v, (int, float))}
|
||||||
|
|
||||||
|
def repl(m: re.Match) -> str:
|
||||||
|
value = lowered.get(m.group(1).lower())
|
||||||
|
if value is None:
|
||||||
|
return "?"
|
||||||
|
if m.group(2):
|
||||||
|
value *= float(m.group(2))
|
||||||
|
return str(int(value)) if float(value).is_integer() else str(round(value, 1))
|
||||||
|
|
||||||
|
text = PLACEHOLDER_RE.sub(repl, desc or "")
|
||||||
|
text = re.sub(r"<[^>]+>", " ", text)
|
||||||
|
text = re.sub(r"%i:[^%]+%", "", text)
|
||||||
|
return re.sub(r"\s+", " ", text).strip()
|
||||||
|
|
||||||
|
|
||||||
def resolve_spell(ability: dict) -> dict:
|
def resolve_spell(ability: dict) -> dict:
|
||||||
"""Spell-Schaden pro Stern aus Desc-Markup + Variablen auflösen.
|
"""Spell-Schaden pro Stern aus Desc-Markup + Variablen auflösen.
|
||||||
@@ -126,7 +154,11 @@ def parse(raw: dict, set_override: int | None = None) -> dict:
|
|||||||
augments = {}
|
augments = {}
|
||||||
for i in raw["items"]:
|
for i in raw["items"]:
|
||||||
api = i["apiName"]
|
api = i["apiName"]
|
||||||
if not (api.startswith(set_prefix) or api.startswith("TFT_Item_")):
|
is_augment = "Augment" in api
|
||||||
|
# Augments: Set-eigene + generischer Evergreen-Pool (TFT_Augment_*).
|
||||||
|
prefixes = (set_prefix, "TFT_Item_", "TFT_Augment_") if is_augment \
|
||||||
|
else (set_prefix, "TFT_Item_")
|
||||||
|
if not api.startswith(prefixes):
|
||||||
continue
|
continue
|
||||||
entry = {
|
entry = {
|
||||||
"api_name": api,
|
"api_name": api,
|
||||||
@@ -135,7 +167,12 @@ def parse(raw: dict, set_override: int | None = None) -> dict:
|
|||||||
"effects": i.get("effects") or {},
|
"effects": i.get("effects") or {},
|
||||||
"icon": icon_url(i["icon"]) if i["icon"] else None,
|
"icon": icon_url(i["icon"]) if i["icon"] else None,
|
||||||
}
|
}
|
||||||
if "Augment" in api:
|
if is_augment:
|
||||||
|
# Müll raus: Shop-Mechanik-Angebote und unaufgelöste Tooltip-Namen.
|
||||||
|
if "MarketOffering" in api or "@" in (i["name"] or "") or "{" in (i["name"] or ""):
|
||||||
|
continue
|
||||||
|
entry["tier"] = augment_tier(i.get("icon"))
|
||||||
|
entry["desc"] = render_desc(i.get("desc"), i.get("effects") or {})
|
||||||
augments[api] = entry
|
augments[api] = entry
|
||||||
else:
|
else:
|
||||||
items[api] = entry
|
items[api] = entry
|
||||||
|
|||||||
@@ -59,9 +59,16 @@ const standings = computed(() => {
|
|||||||
<div v-if="s.augment_offer.length" class="augments">
|
<div v-if="s.augment_offer.length" class="augments">
|
||||||
<h3>Augment wählen</h3>
|
<h3>Augment wählen</h3>
|
||||||
<div class="offer">
|
<div class="offer">
|
||||||
<button v-for="(a, i) in s.augment_offer" :key="a.api_name" @click="store.pickAugment(i)">
|
<button
|
||||||
|
v-for="(a, i) in s.augment_offer"
|
||||||
|
:key="a.api_name"
|
||||||
|
class="augcard"
|
||||||
|
:class="'tier' + (a.tier || 2)"
|
||||||
|
@click="store.pickAugment(i)"
|
||||||
|
>
|
||||||
<img v-if="a.icon" :src="a.icon" :alt="a.name" />
|
<img v-if="a.icon" :src="a.icon" :alt="a.name" />
|
||||||
{{ a.name }}
|
<span class="augname">{{ a.name }}</span>
|
||||||
|
<span class="augdesc">{{ a.desc }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,9 +178,20 @@ header {
|
|||||||
}
|
}
|
||||||
.augments { background: #1d2438; border-radius: 8px; padding: 10px 14px; }
|
.augments { background: #1d2438; border-radius: 8px; padding: 10px 14px; }
|
||||||
.augments h3 { font-size: 12px; text-transform: uppercase; color: #8a92a8; margin-bottom: 6px; }
|
.augments h3 { font-size: 12px; text-transform: uppercase; color: #8a92a8; margin-bottom: 6px; }
|
||||||
.offer { display: flex; gap: 8px; }
|
.offer { display: flex; gap: 10px; }
|
||||||
.offer button { display: flex; align-items: center; gap: 8px; flex: 1; }
|
.augcard {
|
||||||
.offer img { width: 28px; height: 28px; }
|
flex: 1;
|
||||||
|
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||||
|
padding: 14px 12px;
|
||||||
|
text-align: center;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
.augcard img { width: 44px; height: 44px; }
|
||||||
|
.augname { font-weight: 700; font-size: 14px; }
|
||||||
|
.augdesc { font-size: 12px; color: #aab2c8; line-height: 1.35; }
|
||||||
|
.augcard.tier1 { border-color: #b9bec9; }
|
||||||
|
.augcard.tier2 { border-color: #e8c35a; }
|
||||||
|
.augcard.tier3 { border-color: #b153d8; box-shadow: 0 0 10px #b153d855; }
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1; min-height: 0;
|
flex: 1; min-height: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user