File size: 6,143 Bytes
3023403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""Evolutions and pre-evolutions of existing dex species.

The dex owns the chain rules (at most 3 stages, no cycles, legendaries
neither evolve nor are evolved from - tools/dexlib.py there re-checks every
submission at PR time), but a rejection that only arrives after the artwork
is paid for is a bad experience. This module answers the two questions the
Create tab must answer before spending anything: is this anchor allowed, and
what family context does the new stage inherit?

It reads the dex snapshot's species.json/spec.json directly, not the derived
entries list: those are the files the dex validates, so the guard and the
gatekeeper read the same fields.
"""

import json
import os

from fakemon_forge import species as species_mod

MAX_CHAIN_LENGTH = 3  # mirrors tools/dexlib.py in the dex repo

METHODS = {
    "Level up": "EVO_LEVEL",
    "Trade": "EVO_TRADE",
    "Friendship": "EVO_FRIENDSHIP",
}


def _load(path):
    try:
        with open(path) as f:
            return json.load(f)
    except (OSError, ValueError):
        return {}


def chains(dex_root):
    """Per-species evolution context, keyed by directory slug.

    Each entry carries what the guard needs (links, legendary flags) and
    what the new stage inherits (family, family seed, stage position, the
    anchor's committed BST, its raw artwork for visual continuity).
    """
    base = os.path.join(dex_root, "dex")
    out = {}
    if not os.path.isdir(base):
        return out
    for slug in sorted(os.listdir(base)):
        sdir = os.path.join(base, slug)
        if not os.path.isdir(sdir):
            continue
        spec = _load(os.path.join(sdir, "spec.json"))
        species = _load(os.path.join(sdir, "species.json"))
        if not spec and not species:
            continue
        targets = [l.get("target") for l in (species.get("evolvesTo") or [])
                   if isinstance(l, dict) and l.get("target")]
        stats = [species.get(k) or 0 for k in
                 ("baseHP", "baseAttack", "baseDefense",
                  "baseSpAttack", "baseSpDefense", "baseSpeed")]
        out[slug] = {
            "name": spec.get("name") or species.get("speciesName") or slug,
            "family": spec.get("family") or slug,
            "family_seed": spec.get("family_seed", spec.get("seed", 0)),
            "stage": int(spec.get("stage") or 1),
            "of_stages": int(spec.get("of_stages") or 1),
            "from": species.get("evolvesFrom"),
            "to": targets,
            "legendary": bool(species.get("isSubLegendary") or
                              species.get("isRestrictedLegendary")),
            "bst": sum(s for s in stats if isinstance(s, int)),
            "raw": os.path.join(sdir, "raw_front.png"),
        }
    return out


def _back_len(ch, slug):
    """Stages before `slug` on its chain. Cycle-safe: corrupt data ends the walk."""
    seen, n, total = set(), slug, 0
    while True:
        prev = ch.get(n, {}).get("from")
        if not prev or prev in seen or prev not in ch:
            return total
        seen.add(prev)
        n, total = prev, total + 1


def _fwd_len(ch, slug):
    """Longest forward path from `slug`. Cycle-safe."""
    best, stack = 0, [(slug, 0, {slug})]
    while stack:
        n, d, seen = stack.pop()
        best = max(best, d)
        for t in ch.get(n, {}).get("to", []):
            if t not in seen and t in ch:
                stack.append((t, d + 1, seen | {t}))
    return best


def check_anchor(ch, anchor, direction):
    """None when the anchor may take this addition, else the reason it may not.

    direction is "evolution" (the new stage evolves from the anchor) or "pre"
    (the anchor evolves from the new stage). The dex re-checks all of this at
    PR time; this is the friendly front stop before paid generation.
    """
    info = ch.get(anchor)
    if info is None:
        return f"Unknown anchor {anchor!r} - reload the dex and try again."
    name = info["name"]
    if info["legendary"]:
        return (f"{name} is legendary - legendaries cannot evolve or have "
                "pre-evolutions.")
    if direction == "pre":
        if info["from"]:
            return f"{name} already has a pre-evolution."
        if _fwd_len(ch, anchor) + 2 > MAX_CHAIN_LENGTH:
            return (f"A pre-evolution for {name} would make a chain longer "
                    f"than {MAX_CHAIN_LENGTH}.")
    elif _back_len(ch, anchor) + 2 > MAX_CHAIN_LENGTH:
        return (f"{name} is already at the end of a {MAX_CHAIN_LENGTH}-stage "
                "chain.")
    return None


def new_stage_context(ch, anchor, direction):
    """Family context and BST for the new stage. check_anchor() must pass first.

    The new stage joins the anchor's family: same family seed (so the battle
    style and stat-shape draws match the line), a stage position past the
    anchor - or before it, for a pre-evolution - and a BST drawn from the
    family's ladder as the rung it would occupy, clamped so an evolution is
    always an upgrade over the anchor's committed stats and a pre-evolution
    always a downgrade. The anchor's BST is real data; the re-drawn ladder is
    hypothetical, so the clamp wins ties.
    """
    info = ch[anchor]
    seed_spec = {"family_seed": info["family_seed"]}
    if direction == "pre":
        stage = 1
        of_stages = _fwd_len(ch, anchor) + 2
        rung = species_mod.family_bst_ladder(seed_spec, of_stages)[0]
        total = max(species_mod.BST_STAGE_FLOOR,
                    min(rung, info["bst"] - species_mod.BST_STAGE_GAP))
    else:
        stage = info["stage"] + 1
        of_stages = _back_len(ch, anchor) + 2
        rung = species_mod.family_bst_ladder(seed_spec, stage)[-1]
        total = max(rung, info["bst"] + species_mod.BST_STAGE_GAP)
    # The legendary band is a ROM-level invariant; it outranks the upgrade gap
    # on the rare anchor whose committed BST sits right below it.
    total = min(total, species_mod.BST_LEGENDARY_MIN - 1)
    return {"family": info["family"], "family_seed": info["family_seed"],
            "stage": stage, "of_stages": of_stages, "total": total}