Files
cobblemon-survival-pack/tools/jei_sources.py
T
claudeandClaude Opus 5.5 f7ea371862 Phase 9: crafted summons for the legendaries
Adrian, 2026-11-09: legendaries, mythicals, Ultra Beasts and Paradox Pokemon
come from crafted summons. 22 Sigils (Naganadel evolves from Poipole), one
item told apart by custom data, from world/summons.json through
tools/summons.py: late-game recipes in three tiers (Lv 70, 80, 90), each used
in a fitting zone. A summoned legend fights as a boss (area attacks by level
and type, arena 32, the Guardian's Ward) but can be caught; on capture it
loses every boss mark. One at a time within 96 blocks.

Quest book: a Legends chapter, one quest per Sigil (97 quests). JEI lists each
Sigil as its own item, and Island Drops says where a legend is summoned; drops
Cobblemon names but doesn't have (Ho-Oh's Sacred Ash) get no slot.

Self-tests every_implemented_pokemon_is_obtainable (spawn, fossil or Sigil for
every implemented species) and sigil_summons_a_catchable_legend. Self-tests
and the island check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:03:50 +01:00

183 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""Write the data behind the pack's own JEI pages (roadmap Phase 8; Adrian, 2026-11-02: every item that can drop gets
a JEI page naming its sources; 2026-11-03: alongside the Cobblemon Info add-on, which shows spawns and drops but not
the island's zones).
JEI runs on the client, which on a dedicated server doesn't have the pack's server data, so the pages are built from
this file inside the companion mod: mod/src/main/resources/assets/cobblesurvival/jei/drop_sources.json. It holds:
pokemon every species of a line that lives on the island: its drops (as the Drop Synthesizer rolls them)
and the zones it spawns in, or the zones where its line lives
(legendaries: where a Sigil summons them, world/summons.json; that line's evolutions too)
summons each Sigil: species, name, zone, level, so JEI can list it as an item of its own
spawn_drops zone spawns that drop something else than their species (e.g. Beheeyem in the End)
guardians each zone guardian: species, level, drops, return time
end_boss the End boss and what beating it gives
altar the Sacrifice Altar's level tiers and type specials; rebirth: the Rebirth Chamber's extra
ore_veins per zone, what its regenerating veins give and how fast they regrow
pasture_finds per Cobblebase skill and zone, what a pasture's Pokémon can find (the pack's zone-bound tables)
The self-test drop_sources_match_the_server compares it with the live server, so run this after changing species
drops, spawns, guardians, the End boss, the altar, ore veins or Cobblebase's tables, then rebuild the mod.
Needs the Cobblemon jar in build/server/mods (any test-server run).
Usage: tools/jei_sources.py
"""
import json
import re
import zipfile
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DATA = ROOT / "datapacks/core/data"
OUT = ROOT / "mod/src/main/resources/assets/cobblesurvival/jei/drop_sources.json"
SKILLS = {"mining": "mining", "finder_ore": "ore_finder", "finder_evo": "evolution_item_finder"}
def drop_entries(drops):
"""Cobblemon drop table → [{item, chance (percent, absent = always), count ("a-b")}]."""
out = []
for e in (drops or {}).get("entries", []):
if "item" not in e:
continue
entry = {"item": e["item"], "count": str(e.get("quantityRange", e.get("quantity", 1)))}
if "percentage" in e and float(e["percentage"]) < 100:
entry["chance"] = float(e["percentage"])
out.append(entry)
return out
def species_data(jar):
species = {}
with zipfile.ZipFile(jar) as z:
for name in z.namelist():
m = re.fullmatch(r"data/cobblemon/species/[^/]+/([^/]+)\.json", name)
if not m:
continue
d = json.loads(z.read(name))
pre = d.get("preEvolution")
species[f"cobblemon:{m[1]}"] = {
"name": d["name"], "implemented": d.get("implemented", False), "drops": d.get("drops"),
"pre": f"cobblemon:{pre.split()[0].lower()}" if pre else None,
}
# The pack's species additions replace a species' drops (datapacks/core/data/cobblesurvival/species_additions).
for path in sorted((DATA / "cobblesurvival/species_additions").glob("*.json")):
d = json.loads(path.read_text())
if "drops" in d and d["target"] in species:
species[d["target"]]["drops"] = d["drops"]
return species
def root_of(sid, species):
seen = set()
while species.get(sid, {}).get("pre") and sid not in seen:
seen.add(sid)
sid = species[sid]["pre"]
return sid
def main():
jar = next((ROOT / "build/server/mods").glob("Cobblemon-*.jar"))
species = species_data(jar)
plan = json.loads((ROOT / "world/zones.json").read_text())
zones = {z["id"]: z for z in plan["zones"] + plan["caves"] + plan.get("dimensions", [])}
# Where each species spawns, and spawns with drops of their own.
where = defaultdict(dict)
spawn_drops = []
for zid in zones:
pool = DATA / f"cobblesurvival/spawn_pool_world/{zid}.json"
if not pool.exists():
continue
for spawn in json.loads(pool.read_text())["spawns"]:
sid = f"cobblemon:{spawn['pokemon'].split()[0].lower()}"
a, _, b = str(spawn["level"]).partition("-")
low, high = where[sid].get(zid, (999, 0))
where[sid][zid] = (min(low, int(a)), max(high, int(b or a)))
if "drops" in spawn:
entry = {"species": sid, "zone": zid, "drops": drop_entries(spawn["drops"])}
if entry not in spawn_drops:
spawn_drops.append(entry)
line_zones = defaultdict(dict)
for sid, zs in where.items():
for zid, levels in zs.items():
line_zones[root_of(sid, species)].setdefault(zid, sid)
table = json.loads((ROOT / "world/summons.json").read_text())
summoned = {f"cobblemon:{k}": {"zone": v["zone"], "level": table["tiers"][str(v["tier"])]["level"]} for k, v in table["summons"].items()}
summons = [{"species": sid, "name": species[sid]["name"], **s} for sid, s in summoned.items()]
pokemon = []
for sid, s in sorted(species.items()):
drops = drop_entries(s["drops"])
root = root_of(sid, species)
if not drops or not s["implemented"] or not (where.get(sid) or line_zones.get(root) or root in summoned):
continue
entry = {"species": sid, "drops": drops,
"zones": [{"zone": z, "levels": f"{lo}-{hi}"} for z, (lo, hi) in sorted(where.get(sid, {}).items())]}
if root in summoned:
entry["summon"] = summoned[root]
elif not entry["zones"]:
entry["line"] = [{"zone": z, "species": member} for z, member in sorted(line_zones[root].items())]
pokemon.append(entry)
guardians = []
for path in sorted((DATA / "cobblesurvival/guardians").glob("*.json")):
g = json.loads(path.read_text())
name, *props = g["pokemon"].split()
guardians.append({"zone": g["zone"], "species": f"cobblemon:{name}",
"level": int(next(p.split("=")[1] for p in props if p.startswith("level="))),
"drops": g["drops"], "respawn_hours": g["respawn_hours"]})
end = json.loads((DATA / "cobblesurvival/bosses/end.json").read_text())["end"]
end_boss = {"species": f"cobblemon:{end['species']}", "level": end["level"], "xp_first_win": end["xp_first_win"],
"xp": end["xp"], "respawn_hours": end["respawn_hours"],
"first_win": [{"item": "minecraft:dragon_egg", "count": 1}]}
machines = DATA / "cobblesurvival/machines"
altar = json.loads((machines / "altar_core.json").read_text())["altar"]
rebirth = json.loads((machines / "rebirth_core.json").read_text())["rebirth"]
ore_veins = defaultdict(list)
for ore, spec in plan["ores"].items():
ore_veins[spec["zone"]].append({"item": spec["item"], "regen_minutes": spec["regen_minutes"]})
finds = defaultdict(lambda: defaultdict(set))
for path in sorted((DATA / "cobblebase/loot_table").glob("*.json")):
prefix, _, tier = path.stem.rpartition("_")
skill = SKILLS.get(prefix)
if not skill:
continue
for pool in json.loads(path.read_text()).get("pools", []):
for e in pool.get("entries", []):
if e.get("type") != "minecraft:item":
continue
zone_conditions = [c for c in e.get("conditions", []) if c.get("condition") == "cobblesurvival:in_zone"]
for zid in (zone_conditions[0]["zones"] if zone_conditions else ["*"]):
finds[(skill, zid)][e["name"]].add(tier)
pasture_finds = [{"skill": skill, "zone": None if zid == "*" else zid,
"items": [{"item": item, "tiers": sorted(tiers)} for item, tiers in sorted(items.items())]}
for (skill, zid), items in sorted(finds.items(), key=lambda kv: (kv[0][0], kv[0][1] != "*", kv[0][1]))]
data = {
"_comment": "Generated by tools/jei_sources.py from the pack's data; do not edit by hand. Read by the companion "
"mod's JEI plugin (jei/DropSourcesJei.kt); checked against the live server by the self-test "
"drop_sources_match_the_server.",
"zones": {zid: {"name": z["name"], "levels": z["levels"]} for zid, z in zones.items()},
"pokemon": pokemon, "summons": summons, "spawn_drops": spawn_drops, "guardians": guardians, "end_boss": end_boss,
"altar": {"tiers": altar["tiers"], "specials": altar.get("specials", [])}, "rebirth": rebirth.get("extra", []),
"ore_veins": [{"zone": z, "items": items} for z, items in sorted(ore_veins.items())],
"pasture_finds": pasture_finds,
}
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(data, indent=1, ensure_ascii=False) + "\n")
print(f"{len(pokemon)} Pokémon, {len(spawn_drops)} spawns with own drops, {len(guardians)} guardians, "
f"{len(ore_veins)} zones with veins, {len(pasture_finds)} pasture find lists; wrote {OUT.relative_to(ROOT)}")
if __name__ == "__main__":
main()