Files
claudeandClaude Opus 5.5 6361636da7 WIP: ore nodes, catching rules, bee nest fix, smaller Deep Dark
Done and checked: surface and cave-floor ore nodes (companion mod
ore/OreNodes.kt), bigger and rarer underground veins; template with nodes
passed the world check and the island quick check (node self-tests).

Built, not yet tested: catching rules (catch rate falls with the level
gap to the trainer's strongest Pokemon, failed catch enrages, strong wild
Pokemon hunt weaker trainers; catching/, Fight or Flight config), bee
nests emptied when their bee is turned away.

In progress: the Deep Dark no longer fills the deepslate layer (split by
temperature: cold = Deep Dark, hot = basalt deltas, warm = ordinary caves;
world/biomes.json), zone layout regenerated; cave zone types added to
world/zones.json but tools/zone_spawns.py doesn't use them yet; template
not rebuilt for the new caves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:44:17 +01:00

220 lines
11 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Write Regenerating Ore Veins' config and the companion mod's ore node table from the zone plan.
world/zones.json → "ores" lists the tiers (Adrian, 2026-10-30: small ore nodes on the surface, larger veins
underground that are harder to find, and the cave zones' ores as nodes on the cave floor). This tool writes
pack/config/regenerating_ore_veins/:
* areas.json: each surface zone that has a tier, as boxes from the zone grid (build/zone-layout.json,
24-block cells; rows of cells merged into rectangles), named <zone>_<n>, reaching from bedrock to
the build limit. The mod's boxes are centre plus half-size, so a box covers its cells except their
last row and column of blocks: no box reaches into a neighbouring zone.
* veins.json: a surface tier's underground veins ("veins"): one vein, or two when the tier has "stone" and
"deepslate" blocks (an upper vein from y 0 up, "<tier>_deep" below), limited to its zone's boxes and kept
out of the cave biomes, which are zones of their own. The mod checks the area for every block and the
biome at a vein's centre. Then every tier's "<tier>_node": the ore node (the stone ore, or the tier's
blocks), placed only by the companion mod, so its area lies far outside the world border, where the mod
never generates it by itself. The same goes for "selftest" (a single coal block the self-tests place).
* global.json: regenerators can't be broken by players (allow_breaking false), so nobody can remove a
vein for good; explosions don't destroy veins.
* vein_locator.json: the Vein Locator from a compass and a copper ingot (mid-game) instead of netherite.
It also writes datapacks/core/data/cobblesurvival/ore_nodes/island.json, the node table the companion mod reads
(ore/OreNodes.kt: which node, in which zone, how often per chunk, on the surface or on a cave floor), and
world/vein-sources.json, the nodes and veins as item sources for the field guide (tools/fieldguide/build.py reads it
by default): what each tier's blocks drop and in which zone.
Re-run after changing the zone plan or the grid (tools/zone_layout.py) and commit the result.
Usage: tools/zone_ores.py
"""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "pack/config/regenerating_ore_veins"
LAYOUT = ROOT / "build/zone-layout.json"
BOTTOM, TOP = -64, 320
SELFTEST_AREA = "selftest_nowhere"
SOURCES = ROOT / "world/vein-sources.json"
NODES = ROOT / "datapacks/core/data/cobblesurvival/ore_nodes/island.json"
# What an ore block drops, where it isn't the block itself or "<name>_ore" → "<name>".
DROPS = {"iron": "raw_iron", "copper": "raw_copper", "gold": "raw_gold", "lapis": "lapis_lazuli",
"nether_quartz": "quartz", "amethyst_block": "amethyst_block", "budding_amethyst": "amethyst_shard"}
def drop_of(block):
namespace, name = block.split(":")
base = name.removeprefix("deepslate_").removeprefix("dripstone_").removeprefix("terracotta_")
base = base.removesuffix("_ore") if base.endswith("_ore") else base
return f"{namespace}:{DROPS.get(base, base)}"
def rectangles(mask, size):
"""Cover the True cells of a size×size row-major mask with rectangles (col0, row0, col1, row1)."""
open_runs, done = {}, []
for row in range(size + 1):
runs = set()
col = 0
while row < size and col < size:
if mask[row * size + col]:
start = col
while col < size and mask[row * size + col]:
col += 1
runs.add((start, col - 1))
col += 1
for run in list(open_runs):
if run not in runs:
done.append((run[0], open_runs.pop(run), run[1], row - 1))
for run in runs:
open_runs.setdefault(run, row)
return sorted(done, key=lambda r: (r[1], r[0]))
def zone_areas(layout, zone_id):
size, step, radius = layout["size"], layout["step"], layout["radius"]
index = layout["zones"].index(zone_id)
mask = [value == index for value in layout["grid"]]
areas = []
for n, (c0, r0, c1, r1) in enumerate(rectangles(mask, size)):
# Cell i covers -radius - step/2 + i*step ... +step-1 (as in zone/Zones.kt).
x0, z0 = c0 * step - radius - step // 2, r0 * step - radius - step // 2
w, h = (c1 - c0 + 1) * step, (r1 - r0 + 1) * step
areas.append({"dimension": "minecraft:overworld", "name": f"{zone_id}_{n}", "type": "box",
"x": x0 + w // 2 - 1, "y": (BOTTOM + TOP) // 2, "z": z0 + h // 2 - 1,
"dimx": w // 2 - 1, "dimy": (TOP - BOTTOM) // 2, "dimz": h // 2 - 1})
return areas
def vein(vein_id, blocks, spec, shape, defaults, y_range, where):
"""One entry of veins.json: spec gives the regrowth time, shape the size and spacing."""
seconds = spec["regen_minutes"] * 60
jitter = round(seconds * spec.get("jitter", defaults["jitter"]))
return {
"id": vein_id,
"blocks": [b for b, _ in blocks],
"weights": [w for _, w in blocks],
"dimension_whitelist": ["minecraft:overworld"],
**where,
"shape": "sphere",
"min_radius": shape["radius"][0],
"max_radius": shape["radius"][1],
"attempts": shape.get("attempts", defaults["attempts"]),
"min_y": y_range[0],
"max_y": y_range[1],
"chunk_minimum_generation_separation": shape.get("spacing", 64),
"fill_factor": shape.get("fill", defaults["fill"]),
"regeneration_interval_seconds": seconds,
"regeneration_interval_jitter": {"range_min": -jitter, "range_max": jitter},
}
def selftest_vein():
return {"id": "selftest", "blocks": ["minecraft:coal_ore"], "dimension_whitelist": ["minecraft:overworld"],
"area_whitelist": [SELFTEST_AREA], "shape": "sphere", "min_radius": 0, "max_radius": 0,
"attempts": 1, "min_y": 0, "max_y": 0, "chunk_minimum_generation_separation": 64,
"fill_factor": 1.0, "regeneration_interval_seconds": 3600,
"regeneration_interval_jitter": {"range_min": 0, "range_max": 0}}
def main():
if not LAYOUT.exists():
sys.exit(f"{LAYOUT.relative_to(ROOT)} is missing: run tools/zone_layout.py first.")
layout = json.loads(LAYOUT.read_text())
plan = json.loads((ROOT / "world/zones.json").read_text())
defaults = plan["ore_defaults"]
caves = {c["id"]: c["biome"] for c in plan["caves"]}
surface = {z["id"] for z in plan["zones"]}
areas, area_names, veins, nodes = [], {}, [], []
place_only = {"area_whitelist": [SELFTEST_AREA]}
for tier, spec in plan["ores"].items():
zone = spec["zone"]
if zone not in caves and zone not in surface:
sys.exit(f"ore tier {tier}: unknown zone {zone}")
# A node is the stone ore; deepslate where there is no stone ore or the node sits below y 0 (deep caves).
deep = not spec.get("stone") or (zone in caves and spec["nodes"]["y"][1] < 0)
node_blocks = [tuple(b) for b in spec["blocks"]] if "blocks" in spec else [(spec["deepslate" if deep else "stone"], 1)]
node_shape = {**spec["nodes"], "fill": 1.0 if spec["nodes"]["radius"][1] == 0 else defaults["node_fill"]}
veins.append(vein(f"{tier}_node", node_blocks, spec, node_shape, defaults, (0, 0), place_only))
node = {"vein": f"{tier}_node", "zone": zone, "where": "cave" if zone in caves else "surface",
"chance": spec["nodes"]["chance"], "radius": spec["nodes"]["radius"][1]}
if zone in caves:
node.update({"biome": caves[zone], "tries": spec["nodes"]["tries"], "y": spec["nodes"]["y"]})
nodes.append(node)
if "veins" not in spec:
continue
if zone not in area_names:
boxes = zone_areas(layout, zone)
if not boxes:
sys.exit(f"zone {zone} (ore tier {tier}) has no cells in the grid")
areas += boxes
area_names[zone] = [a["name"] for a in boxes]
where = {"area_whitelist": area_names[zone], "biome_blacklist": sorted(caves.values())}
shape = spec["veins"]
low, high = shape["y"]
if "blocks" in spec:
veins.append(vein(tier, spec["blocks"], spec, shape, defaults, (low, high), where))
continue
if spec.get("stone") and high >= 0:
veins.append(vein(tier, [(spec["stone"], 1)], spec, shape, defaults, (max(low, 0), high), where))
if spec.get("deepslate") and low < 0:
deep_id = f"{tier}_deep" if spec.get("stone") and high >= 0 else tier
veins.append(vein(deep_id, [(spec["deepslate"], 1)], spec, shape, defaults, (low, min(high, -1)), where))
areas.append({"dimension": "minecraft:overworld", "name": SELFTEST_AREA, "type": "box",
"x": 29_000_000, "y": 0, "z": 29_000_000, "dimx": 0, "dimy": 0, "dimz": 0})
veins.append(selftest_vein())
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "areas.json").write_text(json.dumps(areas, indent=1) + "\n")
(OUT / "veins.json").write_text(json.dumps(veins, indent=1) + "\n")
(OUT / "global.json").write_text(json.dumps({
"allow_breaking": False,
"break_hardness": 5.0,
"default_regeneration_seconds": 3600,
"default_jitter_interval": {"range_min": 0, "range_max": 0},
"regeneration_smoke_particles": True,
"destroyed_by_explosives": False,
"default_fill_factor": defaults["fill"],
}, indent=2) + "\n")
(OUT / "vein_locator.json").write_text(json.dumps({
"allow_crafting": True,
"allow_use": True,
"crafting_recipe": ["minecraft:compass", "minecraft:copper_ingot"],
"craft_any_order": True,
"chance_to_break": 0.75,
"use_durability": False,
"durability": 2,
"stack_size": 64,
}, indent=2) + "\n")
NODES.parent.mkdir(parents=True, exist_ok=True)
NODES.write_text(json.dumps({
"_comment": "Generated by tools/zone_ores.py from world/zones.json; read by the companion mod (ore/OreNodes.kt).",
"nodes": nodes}, indent=1) + "\n")
names = {z["id"]: z["name"] for z in plan["zones"] + plan["caves"]}
sources = []
for tier, spec in plan["ores"].items():
blocks = [b for b, _ in spec.get("blocks", [])] + [spec[k] for k in ("stone", "deepslate") if spec.get(k)]
where = (f"ore nodes on the cave floor in {names[spec['zone']]}" if spec["zone"] in caves
else f"ore nodes on the ground and veins underground in {names[spec['zone']]}")
for item in dict.fromkeys(drop_of(b) for b in blocks):
sources.append({"item": item, "how": f"Regenerating {tier.replace('_', ' ')} {where}",
"kind": "ore vein", "needs": []})
SOURCES.write_text(json.dumps({
"_comment": "Generated by tools/zone_ores.py from world/zones.json; extra item sources for the field guide.",
"sources": sources}, indent=1) + "\n")
per_zone = {}
for tier, spec in plan["ores"].items():
per_zone.setdefault(spec["zone"], []).append(tier)
print(f"wrote {len(veins)} veins ({len(plan['ores'])} tiers) and {len(areas)} areas to {OUT.relative_to(ROOT)}, "
f"{len(nodes)} node kinds to {NODES.relative_to(ROOT)}")
for zone, tiers in per_zone.items():
boxes = len(area_names.get(zone, []))
print(f" {zone}: {', '.join(tiers)}" + (f" ({boxes} boxes)" if boxes else " (cave nodes)"))
if __name__ == "__main__":
main()