Ore veins: make_world.py loads the island again row by row until Regenerating Ore Veins has filled every chunk (/cobblesurvival veins unfilled), cave tiers sit at their biome's depths, small zones get denser veins, showcase veins are full size. The world report checks surface tiers against the zone grid, cave tiers within two biome cells of their cave, and fails on unfilled chunks; the template passes (37,689 chunks). Drops: tools/drop_audit.py checks every creature drop a recipe or brewing needs (docs/pokemon-drops.md). Blaze rods, ghast tears, breeze rods, shulker shells and nether stars now drop from Pokemon through species additions (self-test drop_additions_apply). The field guide counts vanilla mobs as absent, applies species additions and reads the veins as sources. Cobblebase tried (loads, self-tests pass) and left out until Adrian decides its scope. Phase 4 checklist with the open questions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
7.7 KiB
Python
Executable File
156 lines
7.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Phase 4 drop audit: every crafting input that comes from a creature must have a source on the island.
|
||
|
||
Only Pokémon live on the island, so whatever a vanilla mob would drop has to come from somewhere else:
|
||
a Pokémon's drop, a chest, a vein, a craft. This builds the field guide's model of the pack (no server; it
|
||
uses the last KubeJS recipe export, so run tools/fieldguide/build.py after changing KubeJS scripts) and audits
|
||
every item that a Pokémon or a vanilla mob drops and that a recipe consumes (crafting, smelting, smithing,
|
||
the pack's KubeJS recipes) or that brewing needs (brewing lives in code, so BREWING lists it).
|
||
|
||
Writes docs/pokemon-drops.md: per item, the wild Pokémon that drop it with their zones and chances, its other
|
||
sources, and what consumes it. Fails when an audited item has no source on the island, unless NO_SOURCE_OK
|
||
says why that's intended, and when the pack's species additions give drops to a Pokémon whose zone spawn has
|
||
drops of its own (a spawn's drops replace the species' drops, so the addition would never apply in the wild).
|
||
|
||
Usage: tools/drop_audit.py
|
||
"""
|
||
import json
|
||
import sys
|
||
from collections import defaultdict
|
||
from pathlib import Path
|
||
|
||
sys.dont_write_bytecode = True
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from fieldguide import datasource, kubejs_export # noqa: E402
|
||
from fieldguide.model import Model # noqa: E402
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
OUT = ROOT / "docs/pokemon-drops.md"
|
||
POOLS = ROOT / "datapacks/core/data/cobblesurvival/spawn_pool_world"
|
||
ADDITIONS = ROOT / "datapacks/core/data/cobblesurvival/species_additions"
|
||
VEIN_SOURCES = ROOT / "world/vein-sources.json"
|
||
|
||
# Brewing ingredients and the brewing stand's fuel (vanilla code, not data).
|
||
BREWING = {
|
||
"minecraft:nether_wart": "brewing: Awkward Potion", "minecraft:blaze_powder": "brewing: Strength; stand fuel",
|
||
"minecraft:ghast_tear": "brewing: Regeneration", "minecraft:magma_cream": "brewing: Fire Resistance",
|
||
"minecraft:glistering_melon_slice": "brewing: Healing", "minecraft:rabbit_foot": "brewing: Leaping",
|
||
"minecraft:spider_eye": "brewing: Poison", "minecraft:fermented_spider_eye": "brewing: Weakness, corrupting",
|
||
"minecraft:phantom_membrane": "brewing: Slow Falling", "minecraft:pufferfish": "brewing: Water Breathing",
|
||
"minecraft:golden_carrot": "brewing: Night Vision", "minecraft:turtle_helmet": "brewing: Turtle Master",
|
||
"minecraft:sugar": "brewing: Swiftness", "minecraft:redstone": "brewing: longer potions",
|
||
"minecraft:glowstone_dust": "brewing: stronger potions", "minecraft:gunpowder": "brewing: splash potions",
|
||
"minecraft:dragon_breath": "brewing: lingering potions", "minecraft:breeze_rod": "brewing: Wind Charging",
|
||
"minecraft:slime_block": "brewing: Oozing", "minecraft:cobweb": "brewing: Weaving",
|
||
}
|
||
# Audited items that may stay without a source, and why.
|
||
NO_SOURCE_OK = {
|
||
"cold_sweat:chameleon_molt": "Cold Sweat's chameleon armour and Thermolith are not part of the pack's progression",
|
||
"minecraft:dragon_breath": "the End's dragon is the only source by design (the End is reachable)",
|
||
"minecraft:tide_armor_trim_smithing_template": "cosmetic (an armour trim); elder guardians don't live on the island",
|
||
"minecraft:wither_skeleton_skull": "cosmetic here (the skull banner pattern); the Wither can't be built",
|
||
}
|
||
|
||
|
||
def zones_by_species():
|
||
zones = defaultdict(set)
|
||
for pool in sorted(POOLS.glob("*.json")):
|
||
for spawn in json.loads(pool.read_text()).get("spawns", []):
|
||
species = spawn["pokemon"].split()[0].split(":")[-1]
|
||
zones[f"cobblemon:{species}"].add(pool.stem)
|
||
return zones
|
||
|
||
|
||
def overridden_additions():
|
||
"""Species additions with drops whose target has a zone spawn with its own drops."""
|
||
targets = {json.loads(f.read_text())["target"].split(":")[-1]: f.name for f in sorted(ADDITIONS.glob("*.json"))
|
||
if "drops" in json.loads(f.read_text())}
|
||
clashes = []
|
||
for pool in sorted(POOLS.glob("*.json")):
|
||
for spawn in json.loads(pool.read_text()).get("spawns", []):
|
||
species = spawn["pokemon"].split()[0].split(":")[-1]
|
||
if species in targets and "drops" in spawn:
|
||
clashes.append(f"{targets[species]}: spawn {spawn['id']} has drops of its own")
|
||
return clashes
|
||
|
||
|
||
def consumers(model):
|
||
used = defaultdict(set)
|
||
for rid, recipe in model.recipes.items():
|
||
for slot in recipe.get("slots") or []:
|
||
for item in (slot or {}).get("items", []):
|
||
used[item].add(recipe["result"])
|
||
return used
|
||
|
||
|
||
def main():
|
||
stack = datasource.build_stack(log=lambda msg: None)
|
||
kubejs = kubejs_export.load()
|
||
if not kubejs or kubejs["stale"]:
|
||
print("note: the KubeJS recipe export is missing or older than the pack; run tools/fieldguide/build.py to refresh it")
|
||
model = Model(stack, log=lambda msg: None, extras=[VEIN_SOURCES] if VEIN_SOURCES.exists() else [], kubejs=kubejs)
|
||
zones = zones_by_species()
|
||
used = consumers(model)
|
||
|
||
audited = {}
|
||
for key, sources in model.sources.items():
|
||
if not key.startswith("item:"):
|
||
continue
|
||
item = key[5:]
|
||
if not any(s["kind"] in ("pokemon", "mob") for s in sources):
|
||
continue
|
||
uses = sorted(model.item_name(r) for r in used.get(item, ()))
|
||
if item in BREWING:
|
||
uses.insert(0, BREWING[item])
|
||
if uses:
|
||
audited[item] = (sources, uses)
|
||
|
||
rows, missing = [], []
|
||
for item in sorted(audited, key=model.item_name):
|
||
sources, uses = audited[item]
|
||
pokemon = defaultdict(set)
|
||
for s in sources:
|
||
if s["kind"] == "pokemon" and s["status"] == "available":
|
||
where = ", ".join(sorted(zones.get(s["species"], ()))) or "?"
|
||
pokemon[f"{model.species_name(s['species'])} ({where}, {s['chance']})"].add(1)
|
||
others = sorted({s["text"] for s in sources if s["status"] == "available" and s["kind"] not in ("pokemon", "mob")})
|
||
if not pokemon and not others:
|
||
if item not in NO_SOURCE_OK:
|
||
missing.append(item)
|
||
rows.append((model.item_name(item), item, sorted(pokemon), others, uses))
|
||
|
||
lines = [
|
||
"# Pokémon drops and what they're for",
|
||
"",
|
||
"Generated by `tools/drop_audit.py` (roadmap Phase 4, \"unique drops\"): every item a Pokémon or a vanilla mob "
|
||
"drops that a recipe or brewing consumes. Only Pokémon live on the island, so a mob's drop needs another source. "
|
||
"Zones are the wild Pokémon's home zones (`docs/zone-species.md`); chances are per defeat. Other sources are cut "
|
||
"to five.",
|
||
"",
|
||
"| Item | Wild Pokémon that drop it | Other sources | Consumed by |",
|
||
"| --- | --- | --- | --- |",
|
||
]
|
||
for name, item, pokemon, others, uses in rows:
|
||
source_note = "" if pokemon or others else ("**none**" if item not in NO_SOURCE_OK else f"none ({NO_SOURCE_OK[item]})")
|
||
lines.append(f"| {name} (`{item}`) | {'; '.join(pokemon) or source_note or '–'} | "
|
||
f"{'; '.join(others[:5]) + (' …' if len(others) > 5 else '') or '–'} | {', '.join(uses[:8])}"
|
||
f"{' …' if len(uses) > 8 else ''} |")
|
||
OUT.write_text("\n".join(lines) + "\n")
|
||
print(f"audited {len(rows)} items; wrote {OUT.relative_to(ROOT)}")
|
||
clashes = overridden_additions()
|
||
if missing:
|
||
print("NO SOURCE ON THE ISLAND:")
|
||
for item in missing:
|
||
print(f" - {item}: used by {', '.join(audited[item][1][:6])}")
|
||
if clashes:
|
||
print("SPECIES ADDITIONS THAT NEVER APPLY IN THE WILD:")
|
||
for clash in clashes:
|
||
print(f" - {clash}")
|
||
if missing or clashes:
|
||
sys.exit(1)
|
||
print("every audited item has a source")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|