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>
183 lines
9.1 KiB
Python
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()
|