Files
claudeandClaude Opus 5.5 d29e177f9d Phase 9: safe start island, better quest book, every useful item obtainable
- 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>
2026-11-10 15:04:09 +01:00

155 lines
7.6 KiB
Python
Executable File
Raw Permalink 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 = {
"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()