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>
220 lines
11 KiB
Python
Executable File
220 lines
11 KiB
Python
Executable File
#!/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()
|