Files
cobblemon-survival-pack/tools/drop_audit.py
T
claudeandClaude Opus 5.5 1a1fb93da8 Phase 4: veins in every chunk, drops for the mobs the island lacks
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>
2026-10-30 03:36:50 +01:00

156 lines
7.7 KiB
Python
Executable File
Raw 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
"""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()