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>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-11-09 13:03:50 +01:00
1 parent e206664b3d
commit f7ea371862
92 files changed
+4081 -21

No files matched your search

+11 -3
View File
@@ -8,6 +8,8 @@ this file inside the companion mod: mod/src/main/resources/assets/cobblesurvival
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
@@ -104,15 +106,21 @@ def main():
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)):
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 not entry["zones"]:
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)
@@ -159,7 +167,7 @@ def main():
"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,
"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,
+18 -4
View File
@@ -230,6 +230,15 @@ def chapter_quests(chapter, book, zones):
for t in q.get("tasks", []):
tasks += [{**t, "item": o, "each": None} for o in fields["ores"]] if t.get("each") == "ores" else [t]
q["tasks"] = [{k: v for k, v in t.items() if k != "each"} for t in tasks]
# One quest per Sigil: craft it, use it, catch what comes (world/summons.json; names from the JEI data).
if chapter.get("summons"):
sigils = json.loads((ROOT / "mod/src/main/resources/assets/cobblesurvival/jei/drop_sources.json").read_text())["summons"]
for s in sigils:
quests.append({
"id": s["species"].split(":")[1], "title": s["name"], "icon": "cobblesurvival:summon_sigil", "after": ["about"],
"text": f"Craft the {s['name']} Sigil and use it in {zones[s['zone']]['name']}. Catch the Lv {s['level']} {s['name']} that answers.",
"tasks": [{"cobblemon": "catch", "pokemon": [s["species"]]}],
})
# "Catch Pokémon of a zone": its spawn pool's species (Cobblemon Quests' own biome filter knows no zones).
for q in quests:
for t in q.get("tasks", []):
@@ -264,8 +273,9 @@ def task_nbt(task, name, events):
raise ValueError(f"{name}: unknown task {task}")
def auto_layout(quests, chapter_id):
"""Left to right by dependency depth inside the chapter, stacked within a column."""
def auto_layout(quests, chapter_id, grid=None):
"""Left to right by dependency depth inside the chapter, stacked within a column; with `grid` columns, what depends
on something goes into a grid to the right instead of one tall column."""
local = {q["id"] for q in quests}
depth = {}
@@ -279,6 +289,10 @@ def auto_layout(quests, chapter_id):
for q in quests:
columns.setdefault(d(q), []).append(q["id"])
pos = {}
if grid:
for i, qid in enumerate(q["id"] for q in quests if depth[q["id"]] > 0):
pos[f"{chapter_id}/{qid}"] = [2.0 + (i % grid) * 1.5, (i // grid) * 1.5]
columns = {c: ids for c, ids in columns.items() if c == 0}
for col, ids in columns.items():
for row, qid in enumerate(ids):
pos[f"{chapter_id}/{qid}"] = [col * 2.0, (row - (len(ids) - 1) / 2) * 1.5]
@@ -296,7 +310,7 @@ def build(book):
for index, chapter in enumerate(book["chapters"]):
cid = chapter["id"]
quests = expanded[cid]
auto = auto_layout(quests, cid)
auto = auto_layout(quests, cid, chapter.get("columns"))
chapter_id = object_id(f"chapter:{cid}")
lang[f"chapter.{chapter_id}.title"] = chapter["title"]
if chapter.get("subtitle"):
@@ -369,7 +383,7 @@ def pull(book, folder):
auto, by_id = {}, {}
for c in book["chapters"]:
quests = chapter_quests(c, book, zones)
auto |= auto_layout(quests, c["id"])
auto |= auto_layout(quests, c["id"], c.get("columns"))
by_id |= {object_id(f"quest:{c['id']}/{q['id']}"): f"{c['id']}/{q['id']}" for q in quests}
layout = dict(book.get("layout") or {})
for path in sorted((folder / "chapters").glob("*.snbt")):
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Write the crafted summons (Phase 9; Adrian, 2026-11-09) from world/summons.json.
Legendaries, mythicals, Ultra Beasts and Paradox Pokémon have no spawn data, so each gets a Sigil (one item,
cobblesurvival:summon_sigil, told apart by its custom data), used in a fitting zone to summon a catchable wild one. This
writes, under datapacks/core/data/cobblesurvival/:
summons/<species>.json species, level, zone, read by the companion mod (summon/Summons.kt)
recipe/summon_<species>.json material in the corners, ring on the sides, core in the middle; the result carries
the species, its name and where to use it, so the client needs no server data
and the sigil's texture and item model in the companion mod. The fights are written by tools/zone_bosses.py, so run that
afterwards, and tools/jei_sources.py and tools/quests.py after it.
Needs the Cobblemon jar in build/server/mods (any test-server run), for the names.
Usage: tools/summons.py
"""
import json
import re
import zipfile
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parent.parent
DATA = ROOT / "datapacks/core/data/cobblesurvival"
ASSETS = ROOT / "mod/src/main/resources/assets/cobblesurvival"
SIGIL = "cobblesurvival:summon_sigil"
# The sigil: a dark disc with a gold ring and a violet eye.
TEXTURE = [
"................",
".....######.....",
"...##gggggg##...",
"..#gg......gg#..",
"..#g..vvvv..g#..",
".#g..vv++vv..g#.",
".#g.vv++++vv.g#.",
".#g.vv+WW+vv.g#.",
".#g.vv+WW+vv.g#.",
".#g.vv++++vv.g#.",
".#g..vv++vv..g#.",
"..#g..vvvv..g#..",
"..#gg......gg#..",
"...##gggggg##...",
".....######.....",
"................",
]
COLORS = {"#": (40, 30, 60, 255), "g": (230, 185, 70, 255), ".": (0, 0, 0, 0), "v": (130, 60, 200, 255),
"+": (190, 120, 245, 255), "W": (250, 235, 255, 255)}
def names(jar):
found = {}
with zipfile.ZipFile(jar) as z:
for name in z.namelist():
m = re.fullmatch(r"data/cobblemon/species/[^/]+/([^/]+)\.json", name)
if m:
found[m[1]] = json.loads(z.read(name))["name"]
return found
def main():
table = json.loads((ROOT / "world/summons.json").read_text())
plan = json.loads((ROOT / "world/zones.json").read_text())
zones = {z["id"]: z["name"] for z in plan["zones"] + plan["caves"] + plan.get("dimensions", [])}
species_names = names(next((ROOT / "build/server/mods").glob("Cobblemon-*.jar")))
summons, recipes = DATA / "summons", DATA / "recipe"
summons.mkdir(parents=True, exist_ok=True)
for old in list(summons.glob("*.json")) + list(recipes.glob("summon_*.json")):
old.unlink()
comment = "Generated by tools/summons.py from world/summons.json; do not edit by hand."
for species, s in table["summons"].items():
tier = table["tiers"][str(s["tier"])]
name = species_names[species]
(summons / f"{species}.json").write_text(json.dumps(
{"_comment": comment, "species": species, "level": tier["level"], "zone": s["zone"]}, indent=1) + "\n")
lore = f"Use in {zones[s['zone']]} to summon a Lv {tier['level']} {name}"
(recipes / f"summon_{species}.json").write_text(json.dumps({
"_comment": comment, "type": "minecraft:crafting_shaped", "category": "misc",
"pattern": ["MRM", "RCR", "MRM"],
"key": {"M": {"item": s["material"]}, "R": {"item": tier["ring"]}, "C": {"item": tier["core"]}},
"result": {"id": SIGIL, "count": 1, "components": {
"minecraft:custom_data": {"cobblesurvival_summon": species},
"minecraft:item_name": json.dumps(f"{name} Sigil"),
"minecraft:lore": [json.dumps({"text": lore, "color": "gray", "italic": False})],
}},
}, indent=1) + "\n")
image = Image.new("RGBA", (16, 16))
for y, row in enumerate(TEXTURE):
for x, c in enumerate(row):
image.putpixel((x, y), COLORS[c])
(ASSETS / "textures/item").mkdir(parents=True, exist_ok=True)
image.save(ASSETS / "textures/item/summon_sigil.png")
(ASSETS / "models/item/summon_sigil.json").write_text(json.dumps(
{"parent": "minecraft:item/generated", "textures": {"layer0": "cobblesurvival:item/summon_sigil"}}, indent=2) + "\n")
print(f"wrote {len(table['summons'])} summons")
if __name__ == "__main__":
main()
+8
View File
@@ -26,6 +26,8 @@ END = {"species": "eternatus", "level": 80, "respawn_hours": 5, "xp_first_win":
# Adrian, 2026-11-02: Eternatus was too small (0.2x, about a block). Scaled to stand this many blocks tall.
END_HEIGHT = 15
END_ARENA = 40
# A summoned legendary's fight (world/summons.json; Adrian, 2026-11-09): catchable, same rules as a boss.
SUMMON_ARENA = 32
# Up to this level: these patterns.
TIERS = [
@@ -71,6 +73,7 @@ def species_data(jar):
if name.startswith("data/cobblemon/species/") and name.endswith(".json"):
d = json.loads(z.read(name))
types[d["name"].lower()] = {t for t in (d.get("primaryType"), d.get("secondaryType")) if t}
types[Path(name).stem] = types[d["name"].lower()]
heights[d["name"].lower()] = (d.get("hitbox") or {}).get("height", 1) * d.get("baseScale", 1)
return types, heights
@@ -94,6 +97,11 @@ def main():
end = boss(END["level"], types.get(END["species"], set()), END_ARENA, end=end_def)
(OUT / "end.json").write_text(json.dumps({"_comment": comment, **end}, indent=1) + "\n")
print(f"end: {END['species']} Lv {END['level']}, {len(end['patterns'])} patterns")
table = json.loads((ROOT / "world/summons.json").read_text())
for species, s in table["summons"].items():
fight = boss(table["tiers"][str(s["tier"])]["level"], types.get(species, set()), SUMMON_ARENA, summon=species)
(OUT / f"summon_{species}.json").write_text(json.dumps({"_comment": comment, **fight}, indent=1) + "\n")
print(f"{len(table['summons'])} summoned legendaries")
if __name__ == "__main__":