- Button Isle is peaceful: wild Pokémon only fight a player who hit them first (tuning/server.json peaceful_zones); its apricorns and coal make Poké Balls (4 apricorns + coal or charcoal), and the world report checks it has enough. - Quest book: no boilerplate; each zone page says what is particular to it, and every zone that needs gear or a field move to be safe has its own quest. New page on the bigger mods (Cold Sweat, Fight or Flight, Cobblebase, breeding, veins, shared loot, maps and info). - The Mire's poison stops 10 blocks below the surface. - Player battles never cost a Pokémon (CobbleHardcoreMon config, mixins, and the companion mod's own permadeath). - tools/item_audit.py: every item with a use has a source on the island; new sources for the gaps it found (species additions, three recipes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
155 lines
7.6 KiB
Python
Executable File
155 lines
7.6 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 = {
|
||
"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()
|