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:
commit
398301f0be
232 files changed
+72665
No files matched your search
@@ -0,0 +1,260 @@
|
||||
"""Cobblemon world spawns (data/*/spawn_pool_world), resolved against the island.
|
||||
|
||||
Mirrors Cobblemon 1.8.4: a spawn's conditions are OR'd and its anticonditions
|
||||
veto; presets (spawn_detail_presets) are merged into every condition with
|
||||
MergeMode.INSERT (lists appended, single values kept if already set) and their
|
||||
anticondition is appended (SpawnDetailPreset.apply). A biome named in any
|
||||
anticondition is never considered (BiomePrecalculation).
|
||||
"""
|
||||
import json
|
||||
|
||||
from .datasource import ROOT, norm_id, split_id
|
||||
|
||||
MERGED_FIELDS_SKIP = {"isRaining", "isThundering", "isSlimeChunk", "markers", "isPokeSnack"} # not in copyFrom()
|
||||
TIME_RANGES = {"day", "night", "morning", "noon", "afternoon", "evening", "midnight", "predawn", "dawn", "dusk",
|
||||
"twilight", "any"}
|
||||
MOON = ["full", "waning gibbous", "last quarter", "waning crescent", "new", "waxing crescent", "first quarter",
|
||||
"waxing gibbous"]
|
||||
|
||||
|
||||
def parse_range(text, default=(1, 100)):
|
||||
if text is None:
|
||||
return list(default)
|
||||
if isinstance(text, (int, float)):
|
||||
return [int(text), int(text)]
|
||||
parts = str(text).split("-")
|
||||
try:
|
||||
return [int(parts[0]), int(parts[-1])] if parts[0] else list(default)
|
||||
except ValueError:
|
||||
return list(default)
|
||||
|
||||
|
||||
def parse_props(text):
|
||||
"""'venusaur held_item=cobblemon:grass_gem alpha=true' -> species id and extra properties."""
|
||||
tokens = str(text).split()
|
||||
species = norm_id(tokens[0] if ":" in tokens[0] else "cobblemon:" + tokens[0]) if tokens else ""
|
||||
return species, tokens[1:]
|
||||
|
||||
|
||||
def as_list(v):
|
||||
return [] if v is None else v if isinstance(v, list) else [v]
|
||||
|
||||
|
||||
class Spawns:
|
||||
def __init__(self, stack, tags, island, pools=None):
|
||||
"""pools: {pool id: (file, json)} to read instead of the stack's visible spawn_pool_world files."""
|
||||
self.stack, self.tags, self.island = stack, tags, island
|
||||
self.btags, self.blocktags = tags["worldgen/biome"], tags["block"]
|
||||
self.zones = load_zones()
|
||||
self.unknown_zones = set()
|
||||
self.presets = {}
|
||||
for pid, rel in sorted(stack.ids("spawn_detail_presets").items()):
|
||||
self.presets[split_id(pid)[1]] = stack.json(rel) or {}
|
||||
self.entries = []
|
||||
self.skipped = []
|
||||
modids = stack.modids
|
||||
if pools is None:
|
||||
pools = {fid: (rel, stack.json(rel)) for fid, rel in sorted(stack.ids("spawn_pool_world").items())}
|
||||
for fid, (rel, d) in pools.items():
|
||||
d = d or {}
|
||||
if d.get("enabled") is False:
|
||||
self.skipped.append((fid, "disabled"))
|
||||
continue
|
||||
if not self._mods_ok(d, modids):
|
||||
self.skipped.append((fid, "needs other mods"))
|
||||
continue
|
||||
for s in d.get("spawns", []):
|
||||
if s.get("type") not in ("pokemon", "pokemon-herd") or not self._mods_ok(s, modids):
|
||||
continue
|
||||
self.entries.append(self._entry(fid, rel, s))
|
||||
|
||||
@staticmethod
|
||||
def _mods_ok(d, modids):
|
||||
return (all(m in modids for m in d.get("neededInstalledMods", []))
|
||||
and not any(m in modids for m in d.get("neededUninstalledMods", [])))
|
||||
|
||||
def _entry(self, fid, rel, s):
|
||||
conds = [dict(c) for c in as_list(s.get("condition")) + as_list(s.get("conditions"))]
|
||||
antis = [dict(c) for c in as_list(s.get("anticondition")) + as_list(s.get("anticonditions"))]
|
||||
bucket, position, weight = s.get("bucket"), s.get("spawnablePositionType", s.get("context")), s.get("weight")
|
||||
for name in s.get("presets", []):
|
||||
p = self.presets.get(name.split(":")[-1])
|
||||
if p is None:
|
||||
continue
|
||||
bucket = p.get("bucket", bucket)
|
||||
position = p.get("spawnablePositionType", p.get("context", position))
|
||||
if p.get("condition"):
|
||||
if conds:
|
||||
for c in conds:
|
||||
for k, v in p["condition"].items():
|
||||
if k in MERGED_FIELDS_SKIP:
|
||||
continue
|
||||
if isinstance(v, list):
|
||||
c[k] = list(c.get(k, [])) + v
|
||||
else:
|
||||
c.setdefault(k, v)
|
||||
else:
|
||||
conds.append(dict(p["condition"]))
|
||||
if p.get("anticondition"):
|
||||
antis.append(dict(p["anticondition"]))
|
||||
if s.get("type") == "pokemon":
|
||||
species, extra = parse_props(s.get("pokemon", ""))
|
||||
members = [{"species": species, "props": extra, "level": parse_range(s.get("level", s.get("levelRange"))),
|
||||
"role": "single"}]
|
||||
else:
|
||||
members = []
|
||||
herd = parse_range(s.get("levelRange", s.get("level")))
|
||||
for m in s.get("herdablePokemon", []):
|
||||
species, extra = parse_props(m.get("pokemon", ""))
|
||||
lv = parse_range(m.get("levelRange"), herd)
|
||||
if m.get("isLeader"): # leaders list 1-100; the herd's own range is what they spawn at
|
||||
lv = [max(lv[0], herd[0]), min(lv[1], herd[1])]
|
||||
members.append({"species": species, "props": extra, "level": lv,
|
||||
"role": "leader" if m.get("isLeader") else "follower", "weight": m.get("weight"),
|
||||
"max": m.get("maxTimes")})
|
||||
excluded = set()
|
||||
for a in antis:
|
||||
excluded |= self.btags.resolve_all(a.get("biomes", []))
|
||||
out_conds, biomes, zones = [], set(), set()
|
||||
for c in conds or [{}]:
|
||||
oc = self._condition(c)
|
||||
oc["biomes"] = sorted(set(oc["biomes"]) - excluded)
|
||||
out_conds.append(oc)
|
||||
biomes |= set(oc["biomes"])
|
||||
zones |= set(oc["zones"])
|
||||
mults = as_list(s.get("weightMultiplier")) + as_list(s.get("weightMultipliers"))
|
||||
return {
|
||||
"id": s.get("id") or fid, "file": rel, "pool": fid, "type": s["type"], "bucket": bucket,
|
||||
"weight": weight, "percentage": s.get("percentage"), "position": position,
|
||||
"level": parse_range(s.get("level", s.get("levelRange"))) if s.get("type") == "pokemon-herd" else members[0]["level"],
|
||||
"max_herd": s.get("maxHerdSize"), "members": members, "presets": s.get("presets", []),
|
||||
"conditions": out_conds, "anticonditions": [self._condition(a, anti=True) for a in antis],
|
||||
"biomes": sorted(biomes), "zones": sorted(zones), "multipliers": [{"multiplier": m.get("multiplier"),
|
||||
"when": self._condition(m.get("condition", {}))["text"]}
|
||||
for m in mults],
|
||||
"drops": s.get("drops"),
|
||||
}
|
||||
|
||||
def _condition(self, c, anti=False):
|
||||
"""Condition JSON -> island biomes, structure/block requirements and readable tokens."""
|
||||
isl = self.island
|
||||
if c.get("biomes"):
|
||||
allb = self.btags.resolve_all(c["biomes"])
|
||||
here = isl.island_biomes(allb)
|
||||
else:
|
||||
allb, here = None, sorted(b for b in isl.biomes if isl.biomes[b]["kind"] != "nether")
|
||||
text = []
|
||||
# the companion mod's zone condition (mod/.../zone/Zones.kt): "zones" wins over "zone"
|
||||
zones = as_list(c["zones"]) if c.get("zones") is not None else as_list(c.get("zone"))
|
||||
if zones:
|
||||
text.append(("zones " if len(zones) > 1 else "zone ") + ", ".join(self._zone_label(z) for z in zones))
|
||||
if c.get("dimensions"):
|
||||
text.append("dimension " + ", ".join(c["dimensions"]))
|
||||
if c.get("structures"):
|
||||
st, which = isl.structure_status(c["structures"])
|
||||
text.append("in/near " + ", ".join(_short(x) for x in c["structures"]))
|
||||
# the structure decides where this can happen: only biomes the structures generate in
|
||||
if not anti:
|
||||
where = {b for w in which for b in isl.structures[w]["biomes"]}
|
||||
here = [b for b in here if b in where]
|
||||
else:
|
||||
st, which = None, []
|
||||
tr = c.get("timeRange")
|
||||
if tr:
|
||||
text.append(tr if tr in TIME_RANGES else f"time {tr}")
|
||||
if c.get("moonPhase") is not None:
|
||||
phases = []
|
||||
for part in str(c["moonPhase"]).split(","):
|
||||
lo, _, hi = part.partition("-")
|
||||
if lo.strip().isdigit():
|
||||
phases += [MOON[i % 8] for i in range(int(lo), int(hi or lo) + 1)]
|
||||
else:
|
||||
phases.append(part.strip())
|
||||
text.append("moon: " + ", ".join(phases))
|
||||
if c.get("canSeeSky") is True:
|
||||
text.append("open sky")
|
||||
elif c.get("canSeeSky") is False:
|
||||
text.append("no sky (under cover)")
|
||||
if c.get("isRaining") is True:
|
||||
text.append("raining")
|
||||
elif c.get("isRaining") is False:
|
||||
text.append("not raining")
|
||||
if c.get("isThundering"):
|
||||
text.append("thunderstorm")
|
||||
lo, hi = c.get("minSkyLight"), c.get("maxSkyLight")
|
||||
if (lo, hi) not in ((None, None), (0, 15)) and not (lo in (None, 0) and hi in (None, 15)):
|
||||
text.append(f"sky light {lo if lo is not None else 0}–{hi if hi is not None else 15}")
|
||||
lo, hi = c.get("minLight"), c.get("maxLight")
|
||||
if lo is not None or hi is not None:
|
||||
text.append(f"light {lo if lo is not None else 0}–{hi if hi is not None else 15}")
|
||||
lo, hi = c.get("minY"), c.get("maxY")
|
||||
if lo is not None and hi is not None:
|
||||
text.append(f"y {int(lo)}…{int(hi)}")
|
||||
elif lo is not None:
|
||||
text.append(f"y ≥ {int(lo)}")
|
||||
elif hi is not None:
|
||||
text.append(f"y ≤ {int(hi)}")
|
||||
for axis in ("X", "Z"):
|
||||
lo, hi = c.get("min" + axis), c.get("max" + axis)
|
||||
if lo is not None or hi is not None:
|
||||
text.append(f"{axis.lower()} {'' if lo is None else int(lo)}…{'' if hi is None else int(hi)}")
|
||||
if c.get("minHeight") is not None or c.get("maxHeight") is not None:
|
||||
text.append(f"space {c.get('minHeight', '')}–{c.get('maxHeight', '')} high")
|
||||
near = c.get("neededNearbyBlocks") or []
|
||||
base = c.get("neededBaseBlocks") or []
|
||||
if base and base != ["#cobblemon:natural"]:
|
||||
text.append("standing on " + ", ".join(_short(b) for b in base))
|
||||
if near:
|
||||
text.append("near " + ", ".join(_short(b) for b in near))
|
||||
if c.get("fluid"):
|
||||
text.append("in " + _short(c["fluid"]))
|
||||
if c.get("isSlimeChunk"):
|
||||
text.append("slime chunk")
|
||||
if c.get("isPokeSnack") is True:
|
||||
text.append("lured by a Poké Snack")
|
||||
elif c.get("isPokeSnack") is False and not anti:
|
||||
pass
|
||||
if c.get("minLureLevel"):
|
||||
text.append(f"Lure {c['minLureLevel']}+" + (f" (max {c['maxLureLevel']})" if c.get("maxLureLevel") else ""))
|
||||
if c.get("bait"):
|
||||
text.append("bait " + _short(c["bait"]))
|
||||
if c.get("rodType"):
|
||||
text.append("rod " + _short(c["rodType"]))
|
||||
if c.get("markers"):
|
||||
text.append("markers " + ", ".join(c["markers"]))
|
||||
if anti and c.get("biomes"):
|
||||
text.insert(0, "biomes " + ", ".join(_short(b) for b in c["biomes"]))
|
||||
return {
|
||||
"biomes": here, "all_ids": sorted(allb) if allb else [],
|
||||
"structures": {"refs": c.get("structures", []), "status": st, "present": which},
|
||||
"near": [sorted(self.blocktags.resolve(b)) for b in near],
|
||||
"near_refs": near, "base": [sorted(self.blocktags.resolve(b)) for b in base], "base_refs": base,
|
||||
"poke_snack": c.get("isPokeSnack") is True, "zones": zones, "text": text,
|
||||
}
|
||||
|
||||
def _zone_label(self, zid):
|
||||
z = self.zones.get(zid)
|
||||
if not z:
|
||||
self.unknown_zones.add(zid)
|
||||
return zid
|
||||
return f"{z['name']} (Lv {z['levels'][0]}–{z['levels'][1]})"
|
||||
|
||||
|
||||
def load_zones():
|
||||
"""{zone id: {name, levels}} from world/zones.json (surface zones and caves)."""
|
||||
path = ROOT / "world/zones.json"
|
||||
if not path.exists():
|
||||
return {}
|
||||
plan = json.loads(path.read_text())
|
||||
return {z["id"]: {"name": z.get("name", z["id"]), "levels": z.get("levels", [None, None])}
|
||||
for z in plan.get("zones", []) + plan.get("caves", [])}
|
||||
|
||||
|
||||
def _short(ref):
|
||||
"""'#cobblemon:natural' -> '#natural', 'minecraft:oak_leaves' -> 'oak leaves'."""
|
||||
tag = ref.startswith("#")
|
||||
ns, path = split_id(ref)
|
||||
name = path.replace("_", " ").replace("/", ": ")
|
||||
if tag:
|
||||
return "#" + (name if ns in ("minecraft", "cobblemon", "c") else f"{ns}:{name}")
|
||||
return name
|
||||
Reference in new issue
Block a user