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>
128 lines
5.2 KiB
Python
Executable File
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()
|