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>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-29 19:01:21 +01:00
commit 398301f0be
232 files changed
+72665

No files matched your search

+299
View File
@@ -0,0 +1,299 @@
"""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))