#!/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()