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

+138
View File
@@ -0,0 +1,138 @@
"""Loot tables flattened to (item, approximate chance, count, notes).
Chances are estimates for a reader (expected rolls x weight share x
random_chance conditions); conditions that are not random become notes.
"""
from .datasource import norm_id
def number(v, default=1.0):
"""A loot number provider -> (min, max)."""
if v is None:
return default, default
if isinstance(v, (int, float)):
return float(v), float(v)
t = v.get("type", "minecraft:uniform")
if "min" in v or "max" in v:
lo, hi = number(v.get("min"), 0)[0], number(v.get("max"), 0)[1]
return lo, hi
if t.endswith("binomial"):
n = number(v.get("n"))[1]
return 0.0, n
if t.endswith("constant"):
return float(v.get("value", 1)), float(v.get("value", 1))
return default, default
def condition_effect(conds):
"""(chance factor, notes) of a list of loot conditions."""
factor, notes = 1.0, []
for c in conds or []:
t = c.get("condition", "")
if t.endswith("random_chance"):
ch = c.get("chance", 1)
factor *= ch if isinstance(ch, (int, float)) else number(ch)[1]
elif t.endswith("random_chance_with_enchanted_bonus"):
ch = c.get("unenchanted_chance", 0)
factor *= ch if isinstance(ch, (int, float)) else 0.05
notes.append("Looting raises it")
elif t.endswith("random_chance_with_looting"):
factor *= c.get("chance", 1)
elif t.endswith("table_bonus"):
factor *= (c.get("chances") or [1])[0]
elif t.endswith("killed_by_player"):
notes.append("player kill")
elif t.endswith("match_tool"):
pred = c.get("predicate", {})
enchant = str(pred.get("predicates", {}).get("minecraft:enchantments", "")) + str(pred.get("enchantments", ""))
if "silk_touch" in enchant:
notes.append("Silk Touch")
elif pred.get("items"):
items = pred["items"] if isinstance(pred["items"], list) else [pred["items"]]
notes.append("with " + ", ".join(i.split(":")[-1].replace("_", " ") for i in items))
else:
notes.append("specific tool")
elif t.endswith("block_state_property"):
props = c.get("properties", {})
if "age" in props or "stage" in props:
notes.append("when fully grown" if str(props.get("age", props.get("stage"))) not in ("0",) else "when young")
elif t.endswith("inverted"):
_, inner_n = condition_effect([c.get("term", {})])
if "Silk Touch" in inner_n:
notes.append("without Silk Touch")
elif t.endswith("entity_properties") and "on_fire" in str(c):
notes.append("cooked if on fire")
elif t.endswith("any_of") or t.endswith("alternative"):
pass
return factor, notes
class Loot:
def __init__(self, stack, item_tags):
self.stack, self.item_tags = stack, item_tags
self.ids = stack.ids("loot_table")
self.cache = {}
def table(self, tid):
return self.stack.json(self.ids[tid]) if tid in self.ids else None
def flatten(self, tid, _depth=0):
"""[{item, p, count:[lo,hi], notes}] for a loot table id or inline table."""
if isinstance(tid, str):
tid = norm_id(tid)
if tid in self.cache:
return self.cache[tid]
data = self.table(tid)
else:
data = tid
out = []
if data and _depth < 8:
for pool in data.get("pools", []):
lo, hi = number(pool.get("rolls", 1))
rolls = (lo + hi) / 2 # bonus_rolls only apply with Luck
pf, pnotes = condition_effect(pool.get("conditions"))
entries = list(self._entries(pool.get("entries", [])))
total = sum(w for _, w in entries) or 1
for e, w in entries:
ef, enotes = condition_effect(e.get("conditions"))
share = rolls * pf * ef * w / total
for d in self._entry_items(e, _depth):
out.append({"item": d["item"], "p": min(1.0, share * d["p"]),
"count": d["count"], "notes": sorted(set(pnotes + enotes + d["notes"]))})
if isinstance(tid, str):
self.cache[tid] = out
return out
def _entries(self, entries):
"""(entry, weight) pairs; composite entries are opened up."""
for e in entries:
t = e.get("type", "")
if t.endswith(("alternatives", "group", "sequence")):
for child, w in self._entries(e.get("children", [])):
yield child, w
else:
yield e, e.get("weight", 1)
def _entry_items(self, e, depth):
t = e.get("type", "")
lo, hi = 1.0, 1.0
notes = []
for f in e.get("functions", []):
fn = f.get("function", "")
if fn.endswith("set_count"):
lo, hi = number(f.get("count"))
elif fn.endswith(("furnace_smelt",)):
notes.append("cooked if on fire")
count = [lo, hi]
if t.endswith(":item"):
return [{"item": norm_id(e["name"]), "p": 1.0, "count": count, "notes": notes}]
if t.endswith(":tag"):
items = sorted(self.item_tags.resolve("#" + e["name"]))
if e.get("expand"):
return [{"item": i, "p": 1.0 / max(1, len(items)), "count": count, "notes": notes} for i in items]
return [{"item": i, "p": 1.0, "count": count, "notes": notes + ["all items of a tag"]} for i in items]
if t.endswith("loot_table"):
ref = e.get("value", e.get("name"))
nested = self.flatten(ref, depth + 1)
return [dict(d, notes=d["notes"] + notes) for d in nested]
return []