Phase 8: the pack's own JEI pages for every drop source
Adrian, 2026-11-02/03: every item that can drop gets a JEI page naming its sources, alongside the Cobblemon Info add-on (which shows drops and biomes, not zones). The companion mod's JEI plugin (jei/DropSourcesJei.kt) adds: - Pokemon drops on the island: 794 species of lines that live on the island, with chances, amounts and the zones they spawn in (or where their line lives); spawns with their own drops (Beheeyem in the End). The Drop Synthesizer core is the catalyst. - Guardians and the End boss; Sacrifice Altar tiers and specials; Rebirth Chamber; zone ore veins with regrow times; Cobblebase pasture finds per skill and zone. JEI runs on the client, so the pages read assets/cobblesurvival/jei/ drop_sources.json inside the mod, written by tools/jei_sources.py from the pack's files. Self-tests drop_sources_match_the_server (file vs live server: drops, zones, own-drop spawns, guardians, End boss, altar) and jei_pages_build (every page builds, every item exists). The ore a vein gives moved into world/zones.json (ores -> item), shared with the quest book. JEI's API (19.64.0.455) is a compile-only dependency from JEI's own Maven. Self-tests and the island check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c5d1cf93f4
commit
72965caedf
13 files changed
+19637
-49
No files matched your search
@@ -0,0 +1,174 @@
|
||||
#!/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
|
||||
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)
|
||||
|
||||
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)):
|
||||
continue
|
||||
entry = {"species": sid, "drops": drops,
|
||||
"zones": [{"zone": z, "levels": f"{lo}-{hi}"} for z, (lo, hi) in sorted(where.get(sid, {}).items())]}
|
||||
if 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, "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()
|
||||
+3
-2
@@ -161,7 +161,8 @@ def zone_data():
|
||||
pool = DATA / f"spawn_pool_world/{zid}.json"
|
||||
spawns = json.loads(pool.read_text())["spawns"] if pool.exists() else []
|
||||
z["species"] = sorted({f"cobblemon:{s['pokemon'].split()[0].lower()}" for s in spawns})
|
||||
z["veins"] = sorted({n["vein"] for n in nodes if n["zone"] == zid})
|
||||
# What its ore veins give when mined (world/zones.json → ores → item; veins are named <ore>_node).
|
||||
z["ore_items"] = [plan["ores"][v.removesuffix("_node")]["item"] for v in sorted({n["vein"] for n in nodes if n["zone"] == zid})]
|
||||
guardian = DATA / f"guardians/{zid}.json"
|
||||
z["guardian_data"] = json.loads(guardian.read_text()) if guardian.exists() else None
|
||||
return zones
|
||||
@@ -194,7 +195,7 @@ def title_case(item):
|
||||
|
||||
def zone_fields(zone, chapter, book):
|
||||
g = zone["guardian_data"]
|
||||
ores = [book["ore_items"][v] for v in zone["veins"] if v in book["ore_items"]]
|
||||
ores = zone["ore_items"]
|
||||
return {
|
||||
"zone": zone["name"], "zone_id": zone["id"], "low": zone["levels"][0], "high": zone["levels"][1],
|
||||
"hazard_text": chapter.get("hazard_text", ""),
|
||||
|
||||
Reference in new issue
Block a user