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:
1 parent
e206664b3d
commit
f7ea371862
92 files changed
+4081
-21
No files matched your search
+11
-3
@@ -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
@@ -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")):
|
||||
|
||||
@@ -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()
|
||||
@@ -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__":
|
||||
|
||||
Reference in new issue
Block a user