Files
claudeandClaude Opus 5.5 398301f0be Cobblemon Survival Pack: phases 0-2 and Phase 3 zones and hazards
Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus
the Kotlin companion mod (cobblesurvival) and its built-in data pack.

- Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and
  export tooling, Prism instance sync, test server with smoke checks.
- Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26),
  Adrian's biome list, no natural ore, structures without ore or vanilla
  mobs, no Nether portals, world template builder and world report.
- Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap
  fixes), Revival Charm, expensive Healing Machine, Lootr refreshing
  loot, in-game self-tests on Minecraft's GameTest framework.
- Phase 3 so far: 14 zones with their own spawn pools and level bands
  (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition),
  Cold Sweat heat and cold as gates with boss-drop insulation recipes,
  spawn survey and hazard self-tests in tools/quick_check.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:01:21 +01:00

300 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Turn the model into fieldguide.json, the single file every renderer reads.
Schema (version 1), all ids are namespaced ("cobblemon:eevee"):
meta generator info, layers read, status legend, rarity bucket chances, kubejs_export
(when the KubeJS recipe export was made, whether it is stale, what it removed/added)
island biomes (kind, status), dimensions, structures (status, reason)
locations [{id, name, kind, status, spawns: [spawn index]}] in display order
spawns [{id, species members, level, bucket, weight, position, conditions, status, biomes, zones}];
zones: the companion mod's zone condition (ids from world/zones.json); species locations
carry the zone names too
hidden_spawns same shape, for spawn pools a pack.mcmeta filter hides (Cobblemon's own pools, replaced
by the zone pools): never in the game, but resolved against the island the same way
(status = what it would be if active); tools/zone_spawns.py plans the zones from them
species {id: {name, dex, types, status, wild, locations, sources, evolutions, drops, pokedex_keys}}
items {id: {name, mod, category, tooltip, status, sources, uses, icon, technical}} (mod items)
vanilla {id: {name, status, icon}} for vanilla items the guide mentions
recipes {id: {type, label, result, count, grid, slots, script?}}; script ("server_scripts:x.js:12")
marks a recipe a pack KubeJS script added or changed
problems data the generator could not resolve
A runtime exporter or KubeJS dump can replace or extend any section; renderers
only read this file.
"""
import datetime
import json
from .datasource import split_id
from .model import CATEGORIES, TECHNICAL
from .world import RANK
KIND_ORDER = {"surface": 0, "cave": 1, "water": 2, "end": 3, "nether": 4}
def build(model, icons=None):
m, isl = model, model.island
st = model.get_status
icons = icons or {}
locations = []
loc_spawns = {b: [] for b in isl.biomes}
real_biomes = set(m.stack.ids("worldgen/biome")) # tags also name biomes of mods that are not installed
spawns = [_spawn(m, i, e, "spawn", real_biomes) for i, e in enumerate(m.spawns.entries)]
for i, e in enumerate(m.spawns.entries):
for b in e["biomes"]:
loc_spawns.setdefault(b, []).append(i)
for b, info in sorted(isl.biomes.items(), key=lambda kv: (KIND_ORDER.get(kv[1]["kind"], 9), kv[0])):
if info["kind"] == "nether":
continue
idx = [i for i in loc_spawns.get(b, []) if spawns[i]["status"] != "unavailable"]
locations.append({"id": b, "name": m.biome_name(b), "kind": info["kind"], "status": info["status"],
"reason": info.get("reason", ""), "spawns": idx})
species = {}
for sid, sp in sorted(m.species.items(), key=lambda kv: (kv[1]["dex"], kv[0])):
srcs = m.sources.get(f"species:{sid}", [])
wild = [s for s in srcs if s["kind"] == "wild"]
wild_status = max((s["status"] for s in wild), key=RANK.get, default="unavailable")
locs = {}
for s in wild:
if RANK[s["status"]]:
e = spawns[s["spawn"]]
for b in e["biomes"]:
lv = next((mm["level"] for mm in e["members"] if mm["species"] == sid), e["level"])
cur = locs.setdefault(b, {"level": None, "alpha": None, "buckets": set(), "special": True,
"zones": set()})
cur["zones"] |= set(e["zones"])
k = "alpha" if e["bucket"] == "boss" else "level"
cur[k] = list(lv) if cur[k] is None else [min(cur[k][0], lv[0]), max(cur[k][1], lv[1])]
cur["buckets"].add(e["bucket"])
cur["special"] = cur["special"] and e["needs_blocks"]
other = [_source(m, s) for s in srcs if s["kind"] != "wild"]
species[sid] = {
"name": sp["name"], "dex": sp["dex"], "types": sp["types"], "implemented": sp["implemented"],
"labels": sp["labels"], "status": st(f"species:{sid}"), "wild": wild_status,
"locations": [{"biome": b, "name": m.biome_name(b), "level": v["level"] or v["alpha"], "alpha_level": v["alpha"],
"buckets": sorted(v["buckets"]),
"zones": [m.spawns.zones.get(z, {}).get("name", z) for z in
sorted(v["zones"], key=lambda z: m.spawns.zones.get(z, {}).get("levels") or [0])],
"special": v["special"]}
for b, v in sorted(locs.items(), key=lambda kv: (kv[1]["special"], m.biome_name(kv[0])))],
"spawns": [s["spawn"] for s in wild],
"sources": sorted(other, key=lambda s: -RANK[s["status"]]),
"evolutions": [{"to": ev["to"], "name": m.species_name(ev["to"]), "how": ev["how"], "form": ev["form"]}
for ev in m.evo_from.get(sid, [])],
"pre": sp["pre"], "drops": _species_drops(sp.get("drops")),
"pokedex_keys": sp["pokedex"] + [k for f in sp["forms"] for k in f["pokedex"] if not f["battle_only"]],
"spawn_biomes_elsewhere": sorted({n for i in m.spawn_by_species.get(sid, [])
for n in spawns[i]["all_biomes"]})[:20],
"why_not_wild": sorted({w for i in m.spawn_by_species.get(sid, []) for w in spawns[i]["why_not"]})[:6]
if wild_status == "unavailable" else [],
}
items, vanilla = {}, {}
mentioned = set()
for iid in m.mod_items:
srcs = [_source(m, s) for s in _useful_sources(m, iid)]
uses = _dedupe(m.uses.get(iid, []))
for s in srcs:
mentioned.update(s.get("mention", []))
items[iid] = {
"name": m.item_name(iid), "mod": split_id(iid)[0], "category": _category(m, iid),
"tooltip": m.tooltip(iid), "status": st(f"item:{iid}"), "technical": iid in TECHNICAL,
"sources": sorted(srcs, key=lambda s: (-RANK[s["status"]], s["kind"])), "uses": uses,
"icon": icons.get(iid), "tags": sorted(t for t in m.itags.all_tags() if iid in m.itags.resolve(t))[:12],
"reason": _reason(m, iid, srcs),
}
recipes = {}
for rid, r in m.recipes.items():
if r["result"] in items or any(i in items for s in r["slots"] for i in s["items"]):
recipes[rid] = {k: r[k] for k in ("type", "label", "result", "count", "grid", "slots", "script") if k in r}
recipes[rid]["status"] = _recipe_status(m, rid)
mentioned.add(r["result"])
for s in r["slots"]:
mentioned.update(s["items"][:8])
for iid in sorted(mentioned):
if iid not in items and iid in m.items:
vsrcs = [_source(m, s) for s in _useful_sources(m, iid)]
vanilla[iid] = {"name": m.item_name(iid), "status": st(f"item:{iid}"), "icon": icons.get(iid),
"sources": sorted(vsrcs, key=lambda s: -RANK[s["status"]])[:12], "reason": _reason(m, iid, vsrcs)}
missing_vanilla = sorted(i for i in m.vanilla_items if st(f"item:{i}") == "unavailable")
spawner = m.stack.get("spawning", "cobblemon:best-spawner-config") or {}
return {
"meta": {
"schema": 1, "generated": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
"layers": [layer.name for layer in m.stack.layers], "world_type": isl.world_type,
"statuses": {"available": "obtainable on the island", "conditional": "needs something that may not "
"exist (the End, a stronghold inside the border)", "unavailable": "no source on the island"},
"buckets": spawner.get("worldBuckets", {}),
"kubejs_export": m.kubejs and {k: m.kubejs[k] for k in ("exported", "kubejs", "stale", "removed")}
| {"added_or_changed": sorted(m.kubejs["recipes"])},
},
"island": {
"dimensions": isl.dimensions,
"biomes": [{"id": b, "name": m.biome_name(b), **{k: v for k, v in info.items()}} for b, info in isl.biomes.items()],
"structures": [{"id": s, "name": m.structure_name(s), "status": v["status"], "reason": v["reason"],
"biomes": v["biomes"]} for s, v in sorted(isl.structures.items())],
"notes": isl.notes,
},
"locations": locations, "spawns": spawns, "species": species, "items": items, "vanilla": vanilla,
"hidden_spawns": [_spawn(m, i, e, "hidden_spawn", real_biomes) for i, e in enumerate(m.hidden_spawns.entries)],
"recipes": recipes,
"problems": {
**{k: sorted(v) for k, v in m.unresolved.items()},
"vanilla items with no source on the island": missing_vanilla,
"unknown tags": sorted(set().union(*(t.missing for t in m.tags.values()))),
"unreadable files": m.stack.errors,
"spawn pools skipped": [f"{a} ({b})" for a, b in m.spawns.skipped],
"zones in spawn conditions but not in world/zones.json": sorted(m.spawns.unknown_zones | m.hidden_spawns.unknown_zones),
},
}
def _spawn(m, i, e, prefix, real_biomes):
isl = m.island
status = m.get_status(f"{prefix}:{i}")
srcs = m.sources.get(f"{prefix}:{i}", [])
conds = []
for c, src in zip(e["conditions"], srcs):
conds.append({"text": c["text"], "biomes": c["biomes"], "status": src.get("status"),
"structures": c["structures"]["present"], "blocked": _names(m, src.get("blocked_by"))})
return {
"i": i, "id": e["id"], "pool": e["pool"], "type": e["type"], "bucket": e["bucket"], "weight": e["weight"],
"level": e["level"], "position": e["position"], "max_herd": e["max_herd"],
"members": [{"species": mm["species"], "name": m.species_name(mm["species"]), "props": mm["props"],
"level": mm["level"], "role": mm["role"], "weight": mm.get("weight")} for mm in e["members"]],
"conditions": conds, "anticonditions": [a["text"] for a in e["anticonditions"] if a["text"]],
"multipliers": e["multipliers"], "status": status, "biomes": e["biomes"], "zones": e["zones"],
# every alternative needs particular blocks nearby (often player-built: concrete, pyramid blocks)
"needs_blocks": all(any(t.startswith("near ") and "leaves" not in t for t in c["text"])
for c in e["conditions"]),
"all_biomes": sorted({m.biome_name(b) for c in e["conditions"] for b in c.get("all_ids", [])
if b in real_biomes and b not in isl.biomes}),
"why_not": _why_not(e, srcs, isl, m, real_biomes) if status == "unavailable" else [],
"drops": e.get("drops"),
}
def _why_not(e, srcs, isl, m, real_biomes):
"""Short reasons a spawn entry cannot happen on the island."""
out = []
for c, src in zip(e["conditions"], srcs):
if c["structures"]["refs"] and not c["structures"]["present"]:
out.append("needs a structure that does not generate here: "
+ ", ".join(r.lstrip("#").split(":")[-1].replace("_", " ") for r in c["structures"]["refs"]))
elif not c["biomes"]:
names = sorted({m.biome_name(b) for b in c.get("all_ids", []) if b in real_biomes})
out.append("biomes not on the island: " + ", ".join(names[:5]) + (" …" if len(names) > 5 else "")
if names else "no biome on the island")
elif src.get("blocked_by"):
out.append("needs " + "; ".join(_names(m, src["blocked_by"])))
return out
def _names(m, groups):
out = []
for g in groups or []:
if not g:
out.append("an ingredient no item matches (empty tag)")
continue
names = []
for k in g[:4]:
kind, rid = k.split(":", 1)
names.append({"item": m.item_name, "block": m.item_name, "mob": m.entity_name,
"species": m.species_name}.get(kind, lambda x: x)(rid) if kind != "spawn" else "a wild spawn")
out.append(" or ".join(dict.fromkeys(names)) + (" …" if len(g) > 4 else ""))
return out
def _source(m, s):
out = {"kind": s["kind"], "text": s["text"], "status": s.get("status", "unavailable")}
for k in ("chance", "recipe", "places", "biomes", "species", "block", "mob", "table", "fossils", "parents"):
if s.get(k) is not None:
out[k] = s[k]
if s.get("blocked_by"):
out["blocked"] = _names(m, s["blocked_by"])
mention = [k.split(":", 1)[1] for g in s.get("needs", []) for k in g[:3] if k.startswith("item:")]
if mention:
out["mention"] = mention
if out.get("biomes"):
out["biome_names"] = [m.biome_name(b) for b in out["biomes"]]
return out
def _reason(m, iid, srcs):
"""One line on why an item has no source on the island (empty if it has one)."""
if m.get_status(f"item:{iid}") != "unavailable":
return ""
if iid in m.island.removed_blocks:
return "ore: its world generation is switched off on this island"
blocked = [s for s in srcs if s.get("blocked")]
if blocked:
return "needs " + blocked[0]["blocked"][0]
if srcs:
text = srcs[0]["text"]
return text if text.endswith(")") else text + " (not on the island)"
raw = m.sources.get(f"item:{iid}", [])
if raw:
return "only from its own block, which never generates here"
return "no recipe, loot table, drop or worldgen feature in the data"
def _useful_sources(m, iid):
"""Drop circular 'break the placed block' sources and duplicates."""
out, seen = [], set()
for s in m.sources.get(f"item:{iid}", []):
if s["kind"] == "block" and s.get("block") == iid and m.island.natural_status(iid)[0] == "unavailable":
continue
key = (s["kind"], s["text"], s.get("recipe"), s.get("chance"))
if key in seen:
continue
seen.add(key)
out.append(s)
return out
def _dedupe(uses):
out, seen = [], set()
for u in uses:
key = (u["kind"], u["text"])
if key not in seen:
seen.add(key)
out.append(u)
return out
def _category(m, iid):
for name, tags in CATEGORIES:
if any(iid in m.itags.resolve(t) for t in tags):
return name
if iid in m.blocks:
return "Blocks"
return "Other"
def _recipe_status(m, rid):
for s in m.sources.get(f"item:{m.recipes[rid]['result']}", []):
if s.get("recipe") == rid:
return s["status"]
return "unavailable"
def _species_drops(drops):
out = []
for e in (drops or {}).get("entries", []):
chance = f"{e['percentage']:g}%" if "percentage" in e else f"×{e.get('quantityRange', e.get('quantity', 1))}"
out.append({"item": e["item"] if ":" in e["item"] else "minecraft:" + e["item"], "chance": chance})
return out
def write(data, path):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=1, default=_default) + "\n")
def _default(o):
if isinstance(o, set):
return sorted(o)
raise TypeError(type(o))