Files
claudeandClaude Opus 5.5 4083791944 Phase 9: reproducible builds, balance from the server, spark check, play feedback
Build: every tool pinned (JDK, packwiz, Gradle plugins, Python packages), exports
normalised, tools/check_reproducible.py proves two clean builds are byte-identical.

Server-side balance: a server can load config/cobblesurvival/datapack.zip in place of
the jar's data, /reload applies it, tools/server_update.py packages it and refuses
when a client change is needed. JEI drop pages are sent by the server. Code-held
balance numbers moved to tuning/server.json.

Performance: spark in the pack (server only), tools/perf_test.py with fake players
whose chunks really are active, docs/performance.md.

Adrian's feedback: default keys R, U, J, §; drop pages link to Cobblemon Info and show
real chances; bosses only hurt in battles; battle XP halved; channels between the
islands low level; temperature ×3 with insulation doubled; hazards start at once; no
item dots on the map; a wild attack starts a battle; the starter only faints; quest
book on join with a basics page and an HMs page; regional forms obtainable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 16:26:30 +01:00

128 lines
5.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Write the pack's versions of Cobblebase's mining and finder loot tables: a pasture only yields its zone's ores.
Adrian (2026-10-31): Cobblebase's Mining skill should only dig up the ores of the zone its pasture is in. Its tables,
and those of the Ore Finder and the Evolution-item Finder, which hand out the same ores and evolution stones, ignore
the island's zones. This copies them from the Cobblebase jar in build/server/mods and puts the loot condition
`cobblesurvival:in_zone` on every ore and evolution stone, naming the zone of its tier in world/zones.json. The mining
tables also lose the type gems (crafting shortcuts to gate materials such as the Heat Rock; ruins archaeology still
has them) and gain the zone ores Cobblebase leaves out (quartz, the evolution stones). The companion mod takes
Cobblebase's built-in copies of these tables out at server start (ore/BaseLoot.kt), so these files are what is rolled.
Output: datapacks/core/data/cobblebase/loot_table/<table>.json (derived from Cobblebase, MPL-2.0).
Usage: tools/cobblebase_loot.py
"""
import json
import sys
import zipfile
from pathlib import Path
import lock
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "datapacks/core/data/cobblebase/loot_table"
CONDITION = "cobblesurvival:in_zone"
# What an ore tier of world/zones.json drops; evolution-stone tiers drop their stone.
TIER_ITEMS = {
"coal": ["minecraft:coal"],
"copper": ["minecraft:raw_copper"],
"iron": ["minecraft:raw_iron"],
"gold": ["minecraft:raw_gold"],
"redstone": ["minecraft:redstone"],
"lapis": ["minecraft:lapis_lazuli"],
"emerald": ["minecraft:emerald"],
"amethyst": ["minecraft:amethyst_shard"],
"diamond": ["minecraft:diamond"],
"quartz": ["minecraft:quartz"],
"netherite": ["minecraft:ancient_debris", "minecraft:netherite_scrap"],
}
# Table families: which items get the zone condition, which are dropped, and where missing zone items are added
# (rarity suffix and weight, like Cobblebase's neighbouring entries).
FAMILIES = {
"mining_": {"zoned": ("ore", "stone"), "drop_gems": True, "add": {"ore": ("uncommon", 8), "stone": ("rare", 3)}},
"finder_ore_": {"zoned": ("ore",), "drop_gems": False, "add": {"ore": ("uncommon", 8)}},
"finder_evo_": {"zoned": ("stone",), "drop_gems": False, "add": {"stone": ("common", 10)}},
}
def cobblebase_jar():
return lock.mod_jar("cobblebase")
def zone_items():
"""item id -> (kind, zone) for every ore tier of world/zones.json."""
ores = json.loads((ROOT / "world/zones.json").read_text())["ores"]
items = {}
for tier, ore in ores.items():
if tier.endswith("_stone"):
drops, kind = [f"cobblemon:{tier}"], "stone"
elif tier in TIER_ITEMS:
drops, kind = TIER_ITEMS[tier], "ore"
else:
sys.exit(f"ore tier {tier} has no drop in TIER_ITEMS")
for item in drops:
items[item] = (kind, ore["zone"])
return items
def rewrite(table, family, items):
spec = FAMILIES[family]
seen = set()
for pool in table["pools"]:
kept = []
for entry in pool["entries"]:
name = entry.get("name", "")
if spec["drop_gems"] and name.endswith("_gem"):
continue
kind, zone = items.get(name, (None, None))
if kind in spec["zoned"]:
entry = dict(entry, conditions=[{"condition": CONDITION, "zones": [zone]}])
seen.add(name)
kept.append(entry)
pool["entries"] = kept
return seen
def main():
items = zone_items()
jar = zipfile.ZipFile(cobblebase_jar())
prefix = "data/cobblebase/loot_table/"
names = sorted(n[len(prefix):-5] for n in jar.namelist() if n.startswith(prefix) and n.endswith(".json"))
OUT.mkdir(parents=True, exist_ok=True)
written = []
for family, spec in FAMILIES.items():
tables = {name: json.loads(jar.read(f"{prefix}{name}.json")) for name in names if name.startswith(family)}
if not tables:
sys.exit(f"Cobblebase has no {family}* loot tables any more")
seen = set()
for table in tables.values():
seen |= rewrite(table, family, items)
for item, (kind, zone) in sorted(items.items()):
if kind not in spec["zoned"] or item in seen:
continue
rarity, weight = spec["add"][kind]
target = tables.get(f"{family}{rarity}")
if target is None:
sys.exit(f"no table {family}{rarity} to add {item} to")
target["pools"][0]["entries"].append({
"type": "minecraft:item", "name": item, "weight": weight,
"conditions": [{"condition": CONDITION, "zones": [zone]}],
})
for name, table in tables.items():
(OUT / f"{name}.json").write_text(json.dumps(table, indent=2, ensure_ascii=False) + "\n")
written.append(name)
print(f"wrote {len(written)} tables to {OUT.relative_to(ROOT)}: {', '.join(written)}")
by_zone = {}
for item, (kind, zone) in items.items():
by_zone.setdefault(zone, []).append(item.split(":")[1])
for zone, zoned in sorted(by_zone.items()):
print(f" {zone}: {', '.join(sorted(zoned))}")
if __name__ == "__main__":
main()