Cobblemon Survival Pack: phases 0-2 and Phase 3 zones and hazards
Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus the Kotlin companion mod (cobblesurvival) and its built-in data pack. - Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and export tooling, Prism instance sync, test server with smoke checks. - Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26), Adrian's biome list, no natural ore, structures without ore or vanilla mobs, no Nether portals, world template builder and world report. - Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap fixes), Revival Charm, expensive Healing Machine, Lootr refreshing loot, in-game self-tests on Minecraft's GameTest framework. - Phase 3 so far: 14 zones with their own spawn pools and level bands (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition), Cold Sweat heat and cold as gates with boss-drop insulation recipes, spawn survey and hazard self-tests in tools/quick_check.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
398301f0be
232 files changed
+72665
No files matched your search
@@ -0,0 +1,793 @@
|
||||
"""The field guide model: items, recipes, loot, Pokémon and what is obtainable on the island.
|
||||
|
||||
Every way to get something is a *source* with a base status (from the island
|
||||
model) and *needs*: groups of node keys (item:, block:, mob:, species:,
|
||||
spawn:) of which at least one per group must be obtainable. A fixpoint over
|
||||
all nodes then decides what the island can provide, so an item whose only
|
||||
recipe needs Nether quartz is flagged even though the recipe exists.
|
||||
"""
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
from .datasource import BUILD, norm_id, split_id
|
||||
from .loot import Loot
|
||||
from .spawns import Spawns, parse_props
|
||||
from .tags import Tags
|
||||
from .world import CODE_LOOT, RANK, Island
|
||||
|
||||
STATUS = {v: k for k, v in RANK.items()}
|
||||
REGISTRIES = ("worldgen/biome", "item", "block", "worldgen/structure", "worldgen/placed_feature", "entity_type", "fluid")
|
||||
# Recipe types -> the station item that must exist (crafting tables and furnaces are always reachable).
|
||||
STATIONS = {
|
||||
"minecraft:blasting": "minecraft:blast_furnace", "minecraft:smoking": "minecraft:smoker",
|
||||
"minecraft:campfire_cooking": "minecraft:campfire", "minecraft:stonecutting": "minecraft:stonecutter",
|
||||
"minecraft:smithing_transform": "minecraft:smithing_table", "minecraft:smithing_trim": "minecraft:smithing_table",
|
||||
"cobblemon:cooking_pot": "#cobblemon:campfire_pots", "cobblemon:cooking_pot_shapeless": "#cobblemon:campfire_pots",
|
||||
"cobblemon:brewing_stand": "minecraft:brewing_stand",
|
||||
}
|
||||
RECIPE_LABEL = {
|
||||
"crafting_shaped": "Crafting", "crafting_shapeless": "Crafting (shapeless)", "smelting": "Smelting",
|
||||
"blasting": "Blast furnace", "smoking": "Smoker", "campfire_cooking": "Campfire", "stonecutting": "Stonecutter",
|
||||
"smithing_transform": "Smithing", "cooking_pot": "Cooking Pot", "cooking_pot_shapeless": "Cooking Pot (shapeless)",
|
||||
"brewing_stand": "Brewing stand",
|
||||
}
|
||||
# KubeJS's own crafting types (used when a recipe needs KubeJS features) read like vanilla's.
|
||||
KUBEJS_TYPES = {"kubejs:shaped": "minecraft:crafting_shaped", "kubejs:shapeless": "minecraft:crafting_shapeless"}
|
||||
# Creative-only or operator items: listed, but never flagged as "no source".
|
||||
TECHNICAL = {"cobblemon:pokemon_model", "cobblemon:npc_editor", "cobblemon:habitat_block",
|
||||
"ftblibrary:fluid_container", "ftblibrary:icon_item", "ftbquests:barrier", "ftbquests:stage_barrier", "ftbquests:detector",
|
||||
"ftbquests:custom_icon", "ftbquests:missing_item", "ftbquests:lootcrate", "ftbquests:banner",
|
||||
"ftbquests:loot_crate_opener", "ftbquests:screen_1", "ftbquests:screen_3", "ftbquests:screen_5",
|
||||
"ftbquests:screen_7", "ftbquests:task_screen_configurator", "ftbquests:progress_detector"}
|
||||
CATEGORIES = [ # first match wins
|
||||
("Poké Balls", ["#cobblemon:poke_balls"]), ("Poké Rods", ["#cobblemon:poke_rods"]),
|
||||
("Evolution items", ["#cobblemon:evolution_items", "#cobblemon:evolution_stones"]),
|
||||
("Fossils", ["#cobblemon:fossils"]), ("Berries", ["#cobblemon:berries"]),
|
||||
("Apricorns", ["#cobblemon:apricorns", "#cobblemon:apricorn_sprouts"]), ("Mints", ["#cobblemon:mints", "#cobblemon:mint_leaves", "#cobblemon:mint_seeds"]),
|
||||
("Medicine", ["#cobblemon:potions", "#cobblemon:remedies", "#cobblemon:revives", "#cobblemon:ethers",
|
||||
"#cobblemon:full_heal_bottles", "#cobblemon:restores"]),
|
||||
("Vitamins & candy", ["#cobblemon:vitamins", "#cobblemon:candies", "#cobblemon:experience_candies",
|
||||
"#cobblemon:iv_candies", "#cobblemon:mochis", "#cobblemon:feathers", "#cobblemon:ability_changers"]),
|
||||
("Held items", ["#cobblemon:held/is_held_item"]), ("Type gems", ["#cobblemon:type_gems"]),
|
||||
("Food & cooking", ["#cobblemon:poke_food", "#cobblemon:regional_delicacies", "#cobblemon:sweets",
|
||||
"#cobblemon:aprijuices", "#cobblemon:teacups", "#cobblemon:herbs", "#cobblemon:seeds"]),
|
||||
("Machines", ["#cobblemon:machines", "#cobblemon:fossil_machine_parts", "#cobblemon:pokedex"]),
|
||||
]
|
||||
|
||||
|
||||
class Model:
|
||||
def __init__(self, stack, log=print, extras=(), kubejs=None):
|
||||
self.stack, self.log, self.kubejs = stack, log, kubejs
|
||||
self.tags = {r: Tags(stack, r) for r in REGISTRIES}
|
||||
self.itags = self.tags["item"]
|
||||
log("island: biomes, structures, natural blocks, mobs")
|
||||
self.island = Island(stack, self.tags, log)
|
||||
log("spawns")
|
||||
self.spawns = Spawns(stack, self.tags, self.island)
|
||||
# Spawn pools a pack.mcmeta filter hides (Cobblemon's own, replaced by the zone pools): not in the game,
|
||||
# but resolved the same way for tools that plan from them (tools/zone_spawns.py).
|
||||
self.hidden_spawns = Spawns(stack, self.tags, self.island, pools=stack.hidden("spawn_pool_world"))
|
||||
self.loot = Loot(stack, self.itags)
|
||||
self.lang = self._lang()
|
||||
self.sources = defaultdict(list) # node key -> [source]
|
||||
self.uses = defaultdict(list) # item -> [use]
|
||||
self.recipes = {}
|
||||
self.pack_recipes = set() # recipe ids added or changed by the pack's KubeJS scripts
|
||||
self.referenced = set()
|
||||
self.unresolved = defaultdict(set)
|
||||
self._registry_vanilla()
|
||||
log("species")
|
||||
self._species()
|
||||
log("recipes")
|
||||
self._recipes()
|
||||
self._kubejs(kubejs)
|
||||
log("loot tables")
|
||||
self._loot_sources()
|
||||
self._drops()
|
||||
self._berries()
|
||||
self._fossils()
|
||||
self._interactions()
|
||||
self._manual()
|
||||
for extra in extras:
|
||||
self._extra(extra)
|
||||
self._registry_mods()
|
||||
self._world_nodes()
|
||||
self._spawn_nodes()
|
||||
self._evolutions()
|
||||
self._uses()
|
||||
log("solving availability")
|
||||
self._solve()
|
||||
|
||||
# --- names --------------------------------------------------------------------------------
|
||||
def _lang(self):
|
||||
lang = {}
|
||||
for rel, idxs in self.stack.index.items():
|
||||
if rel.endswith("/lang/en_us.json") and rel.startswith("assets/"):
|
||||
for d in self.stack.all_json(rel):
|
||||
lang.update(d)
|
||||
return lang
|
||||
|
||||
def item_name(self, iid):
|
||||
ns, path = split_id(iid)
|
||||
for key in (f"item.{ns}.{path}", f"block.{ns}.{path}"):
|
||||
if key in self.lang:
|
||||
return self.lang[key]
|
||||
return path.replace("_", " ").title()
|
||||
|
||||
def biome_name(self, b):
|
||||
ns, path = split_id(b)
|
||||
return self.lang.get(f"biome.{ns}.{path}", path.replace("_", " ").title())
|
||||
|
||||
def entity_name(self, e):
|
||||
ns, path = split_id(e)
|
||||
return self.lang.get(f"entity.{ns}.{path}", path.replace("_", " ").title())
|
||||
|
||||
def structure_name(self, s):
|
||||
ns, path = split_id(s)
|
||||
return path.split("/")[-1].replace("_", " ").title() + ("" if ns == "minecraft" else f" ({ns})")
|
||||
|
||||
def tooltip(self, iid):
|
||||
ns, path = split_id(iid)
|
||||
lines = []
|
||||
for suffix in (".tooltip", ".tooltip_1", ".tooltip_2", ".desc"):
|
||||
for kind in ("item", "block"):
|
||||
if f"{kind}.{ns}.{path}{suffix}" in self.lang:
|
||||
lines.append(self.lang[f"{kind}.{ns}.{path}{suffix}"])
|
||||
return lines
|
||||
|
||||
# --- registries ---------------------------------------------------------------------------
|
||||
def _registry_vanilla(self):
|
||||
rep = BUILD / ".cache/datagen-1.21.1/reports/registries.json"
|
||||
if rep.exists():
|
||||
reg = json.loads(rep.read_text())
|
||||
self.vanilla_items = set(reg["minecraft:item"]["entries"])
|
||||
self.vanilla_blocks = set(reg["minecraft:block"]["entries"])
|
||||
self.vanilla_entities = set(reg["minecraft:entity_type"]["entries"])
|
||||
else:
|
||||
self.log(f"note: {rep} missing; vanilla registries guessed from the language file")
|
||||
self.vanilla_items = {"minecraft:" + k.split(".", 2)[2] for k in self.lang
|
||||
if k.startswith(("item.minecraft.", "block.minecraft.")) and k.count(".") == 2}
|
||||
self.vanilla_blocks = {"minecraft:" + k.split(".", 2)[2] for k in self.lang
|
||||
if k.startswith("block.minecraft.") and k.count(".") == 2}
|
||||
self.vanilla_entities = set()
|
||||
self.blocks = set(self.vanilla_blocks) | {k for k in self.stack.ids("blockstates", root="assets")
|
||||
if not k.startswith("minecraft:")}
|
||||
|
||||
def _registry_mods(self):
|
||||
"""Mod items: item models that have a name in the language file or are used as an item in data."""
|
||||
models = {k for k in self.stack.ids("models/item", root="assets") if not k.startswith("minecraft:")}
|
||||
named = set()
|
||||
for k in self.lang:
|
||||
parts = k.split(".")
|
||||
if len(parts) == 3 and parts[0] in ("item", "block") and parts[1] != "minecraft":
|
||||
named.add(f"{parts[1]}:{parts[2]}")
|
||||
mods = {m for m in models if m in named or m in self.referenced}
|
||||
# Items a KubeJS startup script registers have no model in any jar (KubeJS makes it at runtime);
|
||||
# the server's item registry from the KubeJS export has them.
|
||||
mods |= {i for i in (self.kubejs or {}).get("items", []) if not i.startswith("minecraft:")}
|
||||
self.mod_items = sorted(mods)
|
||||
self.items = self.vanilla_items | set(self.mod_items)
|
||||
missing = {r for r in self.referenced if r not in self.items and not r.startswith("#")
|
||||
and split_id(r)[0] in self.stack.modids}
|
||||
for r in sorted(missing):
|
||||
self.unresolved["item ids used in data but not registered (mod not installed?)"].add(r)
|
||||
|
||||
# --- species --------------------------------------------------------------------------------
|
||||
def _species(self):
|
||||
self.species = {}
|
||||
for sid, rel in self.stack.ids("species").items():
|
||||
d = self.stack.json(rel) or {}
|
||||
ns, path = split_id(sid)
|
||||
key = f"{ns}:{path.split('/')[-1]}"
|
||||
forms = d.get("forms", [])
|
||||
self.species[key] = {
|
||||
"id": key, "name": self.lang.get(f"{ns}.species.{key.split(':')[1]}.name", d.get("name", key)),
|
||||
"dex": d.get("nationalPokedexNumber", 0), "types": [t for t in (d.get("primaryType"), d.get("secondaryType")) if t],
|
||||
"implemented": d.get("implemented", False) is True, "labels": d.get("labels", []),
|
||||
"pre": norm_id("cobblemon:" + d["preEvolution"].split()[0]) if d.get("preEvolution") else None,
|
||||
"evolutions": [dict(e, _form=None) for e in d.get("evolutions", [])] +
|
||||
[dict(e, _form=f.get("name")) for f in forms for e in (f.get("evolutions") or [])],
|
||||
"drops": d.get("drops"), "pokedex": d.get("pokedex", []),
|
||||
"forms": [{"name": f.get("name"), "aspects": f.get("aspects", []), "pokedex": f.get("pokedex", []),
|
||||
"battle_only": f.get("battleOnly", False)} for f in forms],
|
||||
}
|
||||
for e in (d.get("drops") or {}).get("entries", []):
|
||||
self.referenced.add(norm_id(e["item"]))
|
||||
|
||||
def species_name(self, sid):
|
||||
return self.species.get(sid, {}).get("name") or sid.split(":")[-1].title()
|
||||
|
||||
# --- recipes --------------------------------------------------------------------------------
|
||||
def _conditions_ok(self, d):
|
||||
conds = d.get("neoforge:conditions")
|
||||
return all(self._cond(c) for c in conds) if conds else True
|
||||
|
||||
def _cond(self, c):
|
||||
t = c.get("type", "")
|
||||
if t == "neoforge:mod_loaded":
|
||||
return c.get("modid") in self.stack.modids
|
||||
if t == "neoforge:not":
|
||||
return not self._cond(c.get("value", {}))
|
||||
if t == "neoforge:and":
|
||||
return all(self._cond(v) for v in c.get("values", []))
|
||||
if t == "neoforge:or":
|
||||
return any(self._cond(v) for v in c.get("values", []))
|
||||
if t == "neoforge:false":
|
||||
return False
|
||||
if t == "neoforge:tag_empty":
|
||||
return not self.itags.resolve("#" + c.get("tag", ""))
|
||||
if t == "neoforge:item_exists":
|
||||
return True
|
||||
return True
|
||||
|
||||
def ingredient(self, ing):
|
||||
"""Ingredient JSON -> {"items": [...], "tag": label or None}."""
|
||||
if ing is None:
|
||||
return None
|
||||
if isinstance(ing, list):
|
||||
items, tags = [], []
|
||||
for i in ing:
|
||||
r = self.ingredient(i)
|
||||
if r:
|
||||
items += r["items"]
|
||||
tags.append(r["tag"])
|
||||
return {"items": sorted(set(items)), "tag": " / ".join(t for t in tags if t) or None}
|
||||
if isinstance(ing, str):
|
||||
ing = {"tag": ing[1:]} if ing.startswith("#") else {"item": ing}
|
||||
t = ing.get("type", "")
|
||||
if "item" in ing:
|
||||
iid = norm_id(ing["item"])
|
||||
self.referenced.add(iid)
|
||||
return {"items": [iid], "tag": None}
|
||||
if "tag" in ing:
|
||||
items = sorted(self.itags.resolve("#" + ing["tag"]))
|
||||
if not items:
|
||||
self.unresolved["empty item tags in recipes"].add("#" + ing["tag"])
|
||||
return {"items": items, "tag": "#" + norm_id(ing["tag"])}
|
||||
if t in ("neoforge:compound",):
|
||||
return self.ingredient(ing.get("children", []))
|
||||
if t == "neoforge:difference":
|
||||
base, sub = self.ingredient(ing.get("base")), self.ingredient(ing.get("subtracted"))
|
||||
return {"items": sorted(set(base["items"]) - set(sub["items"] if sub else [])), "tag": base["tag"]}
|
||||
if t == "neoforge:intersection":
|
||||
parts = [self.ingredient(c) for c in ing.get("children", [])]
|
||||
items = set(parts[0]["items"]) if parts else set()
|
||||
for p in parts[1:]:
|
||||
items &= set(p["items"])
|
||||
return {"items": sorted(items), "tag": None}
|
||||
if "items" in ing:
|
||||
return self.ingredient(ing["items"])
|
||||
self.unresolved["unknown ingredient types"].add(t or json.dumps(ing)[:60])
|
||||
return {"items": [], "tag": None}
|
||||
|
||||
def _recipes(self):
|
||||
for rid, rel in sorted(self.stack.ids("recipe").items()):
|
||||
d = self.stack.json(rel)
|
||||
if d and self._conditions_ok(d):
|
||||
self._recipe(rid, d)
|
||||
|
||||
def _recipe(self, rid, d, script=None):
|
||||
"""One recipe JSON -> self.recipes[rid] and a source for its result. script: the KubeJS script line
|
||||
that added or changed it."""
|
||||
rtype = norm_id(d.get("type", ""))
|
||||
rtype = KUBEJS_TYPES.get(rtype, rtype)
|
||||
res = d.get("result")
|
||||
if isinstance(res, str):
|
||||
res = {"id": res}
|
||||
if not isinstance(res, dict) or not (res.get("id") or res.get("item")):
|
||||
return # special recipes (dyeing, map cloning, trims) have no fixed result
|
||||
out = norm_id(res.get("id") or res.get("item"))
|
||||
count = res.get("count", 1)
|
||||
grid, slots, extra = None, [], {}
|
||||
if rtype.endswith(("crafting_shaped", "cooking_pot")):
|
||||
keys = {k: self.ingredient(v) for k, v in d.get("key", {}).items()}
|
||||
pattern = d.get("pattern", [])
|
||||
grid = [[keys.get(ch) if ch != " " else None for ch in row] for row in pattern]
|
||||
for row in grid:
|
||||
slots += [s for s in row if s]
|
||||
elif rtype.endswith(("crafting_shapeless", "cooking_pot_shapeless")):
|
||||
slots = [self.ingredient(i) for i in d.get("ingredients", [])]
|
||||
elif rtype.endswith(("smelting", "blasting", "smoking", "campfire_cooking", "stonecutting")):
|
||||
slots = [self.ingredient(d.get("ingredient"))]
|
||||
extra = {"time": d.get("cookingtime")}
|
||||
elif rtype.endswith("smithing_transform"):
|
||||
slots = [self.ingredient(d.get(k)) for k in ("template", "base", "addition")]
|
||||
elif rtype == "cobblemon:brewing_stand":
|
||||
slots = [self.ingredient(d.get("input")), self.ingredient(d.get("bottle"))]
|
||||
else:
|
||||
for k in ("ingredient", "ingredients", "input", "base", "addition", "template", "bottle"):
|
||||
if k in d:
|
||||
v = self.ingredient(d[k])
|
||||
slots += [v] if v else []
|
||||
if "key" in d:
|
||||
slots += [self.ingredient(v) for v in d["key"].values()]
|
||||
self.unresolved["recipe types read generically"].add(rtype)
|
||||
slots = [s for s in slots if s]
|
||||
self.referenced.add(out)
|
||||
label = RECIPE_LABEL.get(rtype.split(":")[1], rtype) + (" (pack recipe)" if script else "")
|
||||
recipe = {"id": rid, "type": rtype, "label": label, "result": out, "count": count, "grid": grid, "slots": slots,
|
||||
**extra, **({"script": script} if script else {})}
|
||||
self.recipes[rid] = recipe
|
||||
needs = [s["items"] and [f"item:{i}" for i in s["items"]] or [] for s in slots]
|
||||
station = STATIONS.get(rtype)
|
||||
if station:
|
||||
needs.append([f"item:{i}" for i in self.itags.resolve(station)])
|
||||
self.add(f"item:{out}", "recipe", f"{recipe['label']}", needs=needs, recipe=rid)
|
||||
|
||||
def _remove_recipe(self, rid):
|
||||
r = self.recipes.pop(rid, None)
|
||||
if r:
|
||||
key = f"item:{r['result']}"
|
||||
self.sources[key] = [s for s in self.sources[key] if s.get("recipe") != rid]
|
||||
return r
|
||||
|
||||
def _kubejs(self, export):
|
||||
"""What the pack's KubeJS scripts did to the recipes, read from the test server (kubejs_export.py)."""
|
||||
if not export:
|
||||
self.unresolved["KubeJS recipes not read (no export; see docs/field-guide.md)"].add("pack/kubejs/server_scripts")
|
||||
return
|
||||
for rid in export["removed"]:
|
||||
if not self._remove_recipe(rid):
|
||||
self.unresolved["recipes KubeJS removed that the data does not have"].add(rid)
|
||||
for rid, d in sorted(export["recipes"].items()):
|
||||
self._remove_recipe(rid) # changed in place (replaceInput, ...): the export has the final JSON
|
||||
marker = d.get("_kubejs_changed_marker", {})
|
||||
self._recipe(rid, d, script=f"{marker.get('source')}:{marker.get('line')}")
|
||||
self.pack_recipes.add(rid)
|
||||
if export["stale"]:
|
||||
self.unresolved["KubeJS recipe export older than the pack (rerun tools/fieldguide/build.py)"].add(
|
||||
f"exported {export['exported']}")
|
||||
|
||||
# --- loot -------------------------------------------------------------------------------
|
||||
def _loot_places(self):
|
||||
"""loot table -> [(place text, status, needs)] for chests, archaeology and features."""
|
||||
places = defaultdict(list)
|
||||
for sid, s in self.island.structures.items():
|
||||
if not RANK[s["status"]] and not s["loot"]:
|
||||
s["loot"] = self.island.walk(s["data"], start=sid)["loot"] | {
|
||||
f"minecraft:{p}" for p in CODE_LOOT.get(s["type"], [])}
|
||||
label = self.structure_name(sid) + ("" if RANK[s["status"]] else " (not on the island)")
|
||||
for t in s["loot"]:
|
||||
places[t].append((label, s["status"], sid))
|
||||
for t, f in self.island.feature_loot.items():
|
||||
st = self.island.biome_status(f["biomes"])
|
||||
if RANK[st]:
|
||||
via = sorted(f["via"])[0]
|
||||
places[t].append((via.split(":")[-1].split("/")[-1].replace("_", " ").title() + " (worldgen feature)",
|
||||
st, via))
|
||||
for tid in list(self.loot.ids):
|
||||
ns, path = split_id(tid)
|
||||
if ns == "cobblemon" and path.startswith("injection/"):
|
||||
target = path[len("injection/"):]
|
||||
targets = ([f"minecraft:chests/village/village_{v}_house" for v in
|
||||
("desert", "plains", "savanna", "snowy", "taiga")]
|
||||
if target == "chests/village_house" else [f"minecraft:{target}"])
|
||||
for tg in targets:
|
||||
for p in places.get(tg, []):
|
||||
places[tid].append((p[0] + " (Cobblemon extra loot)", p[1], p[2]))
|
||||
return places
|
||||
|
||||
def _loot_sources(self):
|
||||
places = self._loot_places()
|
||||
alpha_spawns = None
|
||||
for tid in sorted(self.loot.ids):
|
||||
ns, path = split_id(tid)
|
||||
if ns not in self.stack.modids:
|
||||
continue # data for a mod that is not installed (Cobblemon ships some for Adorn, Botany Pots)
|
||||
data = self.loot.table(tid) or {}
|
||||
drops = self.loot.flatten(tid)
|
||||
if not drops:
|
||||
continue
|
||||
for d in drops:
|
||||
self.referenced.add(d["item"])
|
||||
ltype = data.get("type", "")
|
||||
common = {"table": tid}
|
||||
if path.startswith("blocks/"):
|
||||
block = f"{ns}:{path[7:]}"
|
||||
for d in drops:
|
||||
self.add(f"item:{d['item']}", "block", f"Break {self.item_name(block)}",
|
||||
needs=[[f"block:{block}"]], block=block, **_chance(d), **common)
|
||||
elif path.startswith("entities/"):
|
||||
mob = f"{ns}:{path[9:].split('/')[0]}"
|
||||
for d in drops:
|
||||
self.add(f"item:{d['item']}", "mob", f"Drop from {self.entity_name(mob)}",
|
||||
needs=[[f"mob:{mob}"]], mob=mob, **_chance(d), **common)
|
||||
elif tid in places:
|
||||
ps = places[tid]
|
||||
best = max((p[1] for p in ps), key=RANK.get)
|
||||
needs = [["item:minecraft:brush"]] if ltype == "minecraft:archaeology" else []
|
||||
kind = "archaeology" if ltype == "minecraft:archaeology" else "chest"
|
||||
where = sorted({p[0] for p in ps if p[1] == best})
|
||||
for d in drops:
|
||||
self.add(f"item:{d['item']}", kind,
|
||||
("Brush suspicious blocks in " if kind == "archaeology" else "Loot in ") + _join(where),
|
||||
base=best, needs=needs, places=where, **_chance(d), **common)
|
||||
elif path.startswith(("gameplay/fishing", "fishing/pokerod", "injection/gameplay/fishing")):
|
||||
rod = (["item:" + i for i in self.itags.resolve("#cobblemon:poke_rods")] if path.startswith("fishing/")
|
||||
else ["item:minecraft:fishing_rod"])
|
||||
if path.startswith("gameplay/fishing/") and path != "gameplay/fishing":
|
||||
continue # reached through gameplay/fishing
|
||||
for d in drops:
|
||||
self.add(f"item:{d['item']}", "fishing", "Fishing" + (" with a Poké Rod" if path.startswith("fishing/") else ""),
|
||||
needs=[rod], **_chance(d), **common)
|
||||
elif path.startswith("alpha/"):
|
||||
if alpha_spawns is None:
|
||||
alpha_spawns = self._alpha_spawns()
|
||||
typ = path.split("/")[2].split("_")[0] if path.startswith("alpha/types/") else None
|
||||
keys = [f"spawn:{i}" for i, t in alpha_spawns if typ is None or typ in t]
|
||||
for d in drops:
|
||||
self.add(f"item:{d['item']}", "alpha", "Defeat a wild Alpha Pokémon" + (f" ({typ} type)" if typ else ""),
|
||||
needs=[keys], **_chance(d), **common)
|
||||
elif ns == "minecraft" and path.startswith(("gameplay/", "shearing/")):
|
||||
mob = {"cat_morning_gift": "cat", "hero_of_the_village": "villager", "piglin_bartering": "piglin",
|
||||
"sniffer_digging": "sniffer", "panda_sneeze": "panda", "armadillo_shed": "armadillo",
|
||||
"bogged": "bogged"}
|
||||
key = next((v for k, v in mob.items() if k in path), None)
|
||||
if key is None:
|
||||
self.unresolved["loot tables with no known place"].add(tid)
|
||||
continue
|
||||
for d in drops:
|
||||
self.add(f"item:{d['item']}", "gameplay", path.split("/")[-1].replace("_", " ").capitalize(),
|
||||
needs=[[f"mob:minecraft:{key}"]], **_chance(d), **common)
|
||||
elif not self._is_nested(tid):
|
||||
self.unresolved["loot tables with no known place"].add(tid)
|
||||
|
||||
def _is_nested(self, tid):
|
||||
"""Is this table only used from inside other tables (e.g. cobblemon:sets/...)?"""
|
||||
if not hasattr(self, "_nested"):
|
||||
self._nested = {norm_id(s) for other in self.loot.ids for s in _strings(self.loot.table(other) or {})
|
||||
if ":" in s and norm_id(s) in self.loot.ids and norm_id(s) != other}
|
||||
return tid in self._nested
|
||||
|
||||
def _alpha_spawns(self):
|
||||
out = []
|
||||
for i, e in enumerate(self.spawns.entries):
|
||||
for m in e["members"]:
|
||||
if any(p == "alpha=true" for p in m["props"]):
|
||||
out.append((i, self.species.get(m["species"], {}).get("types", [])))
|
||||
return out
|
||||
|
||||
def _drops(self):
|
||||
"""Pokémon drops: species JSON 'drops', overridden per spawn by a spawn's own 'drops'."""
|
||||
by_species = defaultdict(list)
|
||||
override = defaultdict(list)
|
||||
for i, e in enumerate(self.spawns.entries):
|
||||
for m in e["members"]:
|
||||
(override if e.get("drops") else by_species)[m["species"]].append(i)
|
||||
if e.get("drops"):
|
||||
for d in _drop_entries(e["drops"]):
|
||||
self.add(f"item:{d['item']}", "pokemon", f"Defeat wild {self.species_name(m['species'])} (spawn {e['id']})",
|
||||
needs=[[f"spawn:{i}"]], species=m["species"], chance=d["chance"])
|
||||
self.referenced.add(d["item"])
|
||||
for sid, sp in self.species.items():
|
||||
for d in _drop_entries(sp.get("drops")):
|
||||
keys = [f"spawn:{i}" for i in by_species.get(sid, [])]
|
||||
if keys:
|
||||
self.add(f"item:{d['item']}", "pokemon", f"Defeat wild {sp['name']}",
|
||||
needs=[keys], species=sid, chance=d["chance"])
|
||||
else:
|
||||
self.add(f"item:{d['item']}", "pokemon", f"Defeat wild {sp['name']} (it has no wild spawn)",
|
||||
base="unavailable", species=sid, chance=d["chance"])
|
||||
|
||||
def _berries(self):
|
||||
groves = self.btags.resolve("#minecraft:is_overworld") # berry groves are added to these biomes
|
||||
for bid, rel in self.stack.ids("berries").items():
|
||||
d = self.stack.json(rel) or {}
|
||||
biomes = set()
|
||||
for sc in d.get("spawnConditions", []):
|
||||
v = sc.get("variant", "")
|
||||
if v.endswith("all_biome"):
|
||||
biomes |= groves
|
||||
elif v.endswith("preferred_biome"):
|
||||
biomes |= self.tags["worldgen/biome"].resolve_all(["#" + t for t in d.get("preferredBiomeTags", [])]) & groves
|
||||
elif v.endswith("specific_biome"):
|
||||
biomes |= self.tags["worldgen/biome"].resolve_all(["#" + sc["biome"]]) & groves
|
||||
here = self.island.island_biomes(biomes)
|
||||
if biomes:
|
||||
self.add(f"item:{bid}", "berry", "Wild berry trees in " + _join([self.biome_name(b) for b in here] or ["(no island biome)"]),
|
||||
base=self.island.biome_status(here), biomes=here)
|
||||
for partner, result in d.get("mutations", {}).items():
|
||||
if norm_id(bid) < norm_id(partner): # each pair is listed in both berries
|
||||
self.add(f"item:{norm_id(result)}", "mutation",
|
||||
f"Berry mutation: plant {self.item_name(bid)} next to {self.item_name(partner)}",
|
||||
needs=[[f"item:{bid}"], [f"item:{norm_id(partner)}"]], parents=[bid, norm_id(partner)])
|
||||
|
||||
def _fossils(self):
|
||||
parts = [[f"item:{i}"] for i in sorted(self.itags.resolve("#cobblemon:fossil_machine_parts"))]
|
||||
for fid, rel in self.stack.ids("fossils").items():
|
||||
d = self.stack.json(rel) or {}
|
||||
sp, props = parse_props(d.get("result", ""))
|
||||
items = [norm_id(f) for f in d.get("fossils", [])]
|
||||
self.add(f"species:{sp}", "fossil", "Revive from " + " + ".join(self.item_name(i) for i in items),
|
||||
needs=[[f"item:{i}"] for i in items] + parts, fossils=items)
|
||||
for i in items:
|
||||
self.uses[i].append({"kind": "fossil", "text": f"Revives {self.species_name(sp)} (Fossil Analyzer + Restoration Tank)",
|
||||
"species": sp})
|
||||
|
||||
def _interactions(self):
|
||||
for pid, rel in self.stack.ids("pokemon_interactions").items():
|
||||
d = self.stack.json(rel) or {}
|
||||
targets = [parse_props(r.get("target", ""))[0] for r in d.get("requirements", []) if r.get("variant") == "properties"]
|
||||
for inter in d.get("interactions", []):
|
||||
held = [r.get("itemCondition") for r in inter.get("requirements", []) if r.get("variant") == "owner_held_item"]
|
||||
tool = sorted(self.itags.resolve_all(held)) if held else []
|
||||
for eff in inter.get("effects", []):
|
||||
if eff.get("variant") not in ("drop_item", "give_item") or not eff.get("item"):
|
||||
continue
|
||||
item = norm_id(eff["item"])
|
||||
self.referenced.add(item)
|
||||
for sp in targets:
|
||||
how = f"Use {_join([h.lstrip('#') for h in held]) or 'an item'} on your {self.species_name(sp)}"
|
||||
self.add(f"item:{item}", "interaction", how,
|
||||
needs=[[f"species:{sp}"]] + ([[f"item:{t}" for t in tool]] if tool else []), species=sp)
|
||||
|
||||
def _manual(self):
|
||||
from pathlib import Path
|
||||
cfg = json.loads((Path(__file__).parent / "manual_sources.json").read_text())
|
||||
for s in cfg["sources"]:
|
||||
self.add(f"item:{s['item']}", "manual", s["how"], needs=s.get("needs", []))
|
||||
axes = [f"item:{i}" for i in self.itags.resolve("#minecraft:axes")]
|
||||
for item in sorted(self.vanilla_items | {k for k in self.stack.ids("models/item", root="assets")}):
|
||||
ns, path = split_id(item)
|
||||
if path.startswith("stripped_"):
|
||||
self.add(f"item:{item}", "manual", "Strip with an axe", needs=[[f"item:{ns}:{path[9:]}"], axes])
|
||||
if cfg.get("concrete_from_powder"):
|
||||
for b in sorted(self.vanilla_items):
|
||||
if b.endswith("_concrete_powder"):
|
||||
self.add(f"item:{b[:-7]}", "manual", "Place concrete powder in water", needs=[[f"item:{b}"]])
|
||||
|
||||
def _extra(self, path):
|
||||
"""Extra sources from a JSON file (ore veins, trades, ...); KubeJS recipes come from _kubejs."""
|
||||
d = json.loads(open(path).read())
|
||||
for s in d.get("sources", []):
|
||||
self.add(f"item:{norm_id(s['item'])}", s.get("kind", "extra"), s["how"], base=s.get("status", "available"),
|
||||
needs=s.get("needs", []))
|
||||
for rid in d.get("remove_recipes", []):
|
||||
self._remove_recipe(rid)
|
||||
|
||||
# --- world nodes: blocks, mobs ---------------------------------------------------------------
|
||||
def _world_nodes(self):
|
||||
for blk in self.blocks | set(self.island.natural):
|
||||
st, biomes = self.island.natural_status(blk)
|
||||
n = self.island.natural.get(blk)
|
||||
if n:
|
||||
where = [self.biome_name(b) for b in biomes] or []
|
||||
structs = [self.structure_name(v[10:]) for v in n["via"] if v.startswith("structure:")]
|
||||
self.add(f"block:{blk}", "natural", "Generates naturally", base=st, biomes=biomes, structures=sorted(set(structs)),
|
||||
where=where)
|
||||
if blk in self.items:
|
||||
self.add(f"block:{blk}", "placed", "Placed by a player", needs=[[f"item:{blk}"]])
|
||||
for mob, entries in self.island.mobs.items():
|
||||
for e in entries:
|
||||
where = sorted(e["where"])
|
||||
text = {"biome": "Spawns in " + _join([self.biome_name(b) for b in where]),
|
||||
"structure": "Spawns in " + _join([self.structure_name(s) for s in where]),
|
||||
"special": _join(where).capitalize()}[e["kind"]]
|
||||
self.add(f"mob:{mob}", e["kind"], text, base=e["status"])
|
||||
for mob, (item, note) in self.island.item_mobs.items():
|
||||
self.add(f"mob:{mob}", "item", note.capitalize(), needs=[[f"item:{item}"]])
|
||||
|
||||
def _spawn_nodes(self):
|
||||
self.spawn_by_species = defaultdict(list)
|
||||
self._spawn_sources("spawn", self.spawns.entries)
|
||||
self._spawn_sources("hidden_spawn", self.hidden_spawns.entries) # no species source: they never spawn
|
||||
for i, e in enumerate(self.spawns.entries):
|
||||
for m in e["members"]:
|
||||
self.spawn_by_species[m["species"]].append(i)
|
||||
self.add(f"species:{m['species']}", "wild", f"Wild spawn {e['id']}", needs=[[f"spawn:{i}"]], spawn=i)
|
||||
for sid, sp in self.species.items():
|
||||
if "starter" in sp["labels"]:
|
||||
self.add(f"species:{sid}", "starter", "Starter Pokémon (chosen once per player)")
|
||||
|
||||
def _spawn_sources(self, prefix, entries):
|
||||
rods = [f"item:{i}" for i in self.itags.resolve("#cobblemon:poke_rods")]
|
||||
for i, e in enumerate(entries):
|
||||
for c in e["conditions"]:
|
||||
base = self.island.biome_status(c["biomes"])
|
||||
if c["structures"]["refs"]:
|
||||
base = STATUS[min(RANK[base], RANK[c["structures"]["status"] or "unavailable"])]
|
||||
needs = []
|
||||
if c["near"]:
|
||||
needs.append([f"block:{b}" for grp in c["near"] for b in grp])
|
||||
if c["base"]:
|
||||
needs.append([f"block:{b}" for grp in c["base"] for b in grp])
|
||||
if c["poke_snack"]:
|
||||
needs.append(["item:cobblemon:poke_snack"])
|
||||
if e["position"] == "fishing":
|
||||
needs.append(rods)
|
||||
self.add(f"{prefix}:{i}", "spawn", " · ".join(c["text"]) or "anywhere", base=base, needs=needs)
|
||||
|
||||
def _evolutions(self):
|
||||
self.evo_from = defaultdict(list)
|
||||
for sid, sp in self.species.items():
|
||||
for e in sp["evolutions"]:
|
||||
target, props = parse_props(e.get("result", ""))
|
||||
if not target:
|
||||
continue
|
||||
how, needs, base = [], [[f"species:{sid}"]], "available"
|
||||
variant = e.get("variant")
|
||||
ctx = e.get("requiredContext")
|
||||
if variant == "item_interact" and ctx:
|
||||
items = sorted(self.itags.resolve(ctx)) if str(ctx).startswith("#") else [norm_id(ctx)]
|
||||
how.append("use " + _join([self.item_name(i) for i in items]))
|
||||
needs.append([f"item:{i}" for i in items])
|
||||
for i in items:
|
||||
self.referenced.add(i)
|
||||
self.uses[i].append({"kind": "evolution", "text": f"Evolves {sp['name']} into {self.species_name(target)}",
|
||||
"species": target, "from": sid})
|
||||
elif variant == "trade":
|
||||
how.append("trade" + (f" for {self.species_name(norm_id('cobblemon:' + ctx))}" if ctx else ""))
|
||||
for r in e.get("requirements", []):
|
||||
text, rneeds, rbase = self._requirement(r, sp, target)
|
||||
if text:
|
||||
how.append(text)
|
||||
needs += rneeds
|
||||
base = STATUS[min(RANK[base], RANK[rbase])]
|
||||
desc = f"Evolve {sp['name']}" + (f" ({e['_form']} form)" if e.get("_form") else "") + (": " + ", ".join(how) if how else "")
|
||||
self.add(f"species:{target}", "evolution", desc, base=base, needs=needs, species=sid,
|
||||
props=props, how=how)
|
||||
self.evo_from[sid].append({"to": target, "how": how, "props": props, "form": e.get("_form")})
|
||||
|
||||
def _requirement(self, r, sp, target):
|
||||
"""Evolution requirement -> (text, needs, base status)."""
|
||||
v = r.get("variant", "")
|
||||
if v == "level":
|
||||
return f"level {r.get('minLevel', '?')}" + (f"–{r['maxLevel']}" if r.get("maxLevel") else ""), [], "available"
|
||||
if v == "held_item":
|
||||
cond = r.get("itemCondition", "")
|
||||
items = sorted(self.itags.resolve(cond)) if str(cond).startswith("#") else [norm_id(cond)]
|
||||
for i in items:
|
||||
self.referenced.add(i)
|
||||
self.uses[i].append({"kind": "evolution", "text": f"Held by {sp['name']} to evolve into {self.species_name(target)}",
|
||||
"species": target, "from": sp["id"]})
|
||||
return "holding " + _join([self.item_name(i) for i in items]), [[f"item:{i}" for i in items]], "available"
|
||||
if v == "friendship":
|
||||
return f"friendship {r.get('amount')}", [], "available"
|
||||
if v == "time_range":
|
||||
return r.get("range", ""), [], "available"
|
||||
if v == "biome":
|
||||
biomes = self.tags["worldgen/biome"].resolve(r.get("biomeCondition", "")) if r.get("biomeCondition") else set()
|
||||
here = self.island.island_biomes(biomes)
|
||||
text = "in " + (_join([self.biome_name(b) for b in here]) if here else r.get("biomeCondition", "") + " (not on the island)")
|
||||
return text, [], self.island.biome_status(here) if here else "unavailable"
|
||||
if v == "structure":
|
||||
st, which = self.island.structure_status([r.get("structureCondition", "")])
|
||||
return f"near {r.get('structureCondition', '').lstrip('#')}", [], st
|
||||
if v == "negate":
|
||||
inner, _, _ = self._requirement(r.get("inner", {}), sp, target)
|
||||
return f"not ({inner})", [], "available"
|
||||
if v == "weather":
|
||||
return "raining" if r.get("isRaining") else "thunderstorm" if r.get("isThundering") else "clear weather", [], "available"
|
||||
if v == "moon_phase":
|
||||
return f"moon {str(r.get('moonPhase', '')).lower().replace('_', ' ')}", [], "available"
|
||||
if v == "has_move":
|
||||
return f"knows {r.get('move')}", [], "available"
|
||||
if v == "has_move_type":
|
||||
return f"knows a {r.get('type')} move", [], "available"
|
||||
if v == "properties":
|
||||
return r.get("target", ""), [], "available"
|
||||
if v == "party_member":
|
||||
return f"party has {r.get('target')}", [], "available"
|
||||
if v == "advancement":
|
||||
return f"advancement {r.get('requiredAdvancement')}", [], "available"
|
||||
if v in ("blocks_traveled", "damage_taken", "recoil", "defeat", "use_move", "battle_critical_hits"):
|
||||
return f"{v.replace('_', ' ')} {r.get('amount', '')} {r.get('move', '') or r.get('target', '')}".strip(), [], "available"
|
||||
if v in ("stat_compare", "stat_equal"):
|
||||
return " ".join(str(x) for x in r.values() if x != v), [], "available"
|
||||
if v == "property_range":
|
||||
return f"{r.get('feature')} {r.get('range')}", [], "available"
|
||||
self.unresolved["evolution requirement variants shown raw"].add(v)
|
||||
return v, [], "available"
|
||||
|
||||
# --- uses -----------------------------------------------------------------------------------
|
||||
def _uses(self):
|
||||
for rid, r in self.recipes.items():
|
||||
seen = set()
|
||||
for s in r["slots"]:
|
||||
for i in s["items"]:
|
||||
if i not in seen:
|
||||
seen.add(i)
|
||||
self.uses[i].append({"kind": "recipe", "recipe": rid, "text": f"{r['label']}: {self.item_name(r['result'])}",
|
||||
"result": r["result"]})
|
||||
held = self.itags.resolve("#cobblemon:held/is_held_item")
|
||||
for i in held:
|
||||
self.uses[i].append({"kind": "held", "text": "Held item" + (": " + " ".join(self.tooltip(i)) if self.tooltip(i) else "")})
|
||||
for eid, rel in self.stack.ids("spawn_bait_effects").items():
|
||||
d = self.stack.json(rel) or {}
|
||||
item = norm_id(d.get("item", eid))
|
||||
effects = [e.get("type", "").split(":")[-1].replace("_", " ") for e in d.get("effects", [])]
|
||||
self.uses[item].append({"kind": "bait", "text": "Bait / Poké Snack effect: " + _join(sorted(set(effects)))})
|
||||
for i, e in enumerate(self.spawns.entries):
|
||||
for c in e["conditions"]:
|
||||
refs = [r for r in c["near_refs"] + c["base_refs"] if not r.startswith("#")]
|
||||
for ref in refs:
|
||||
for m in e["members"]:
|
||||
self.uses[norm_id(ref)].append({"kind": "spawn", "text": f"Nearby block that attracts {self.species_name(m['species'])}",
|
||||
"species": m["species"]})
|
||||
|
||||
# --- solving ----------------------------------------------------------------------------------
|
||||
def add(self, key, kind, text, base="available", needs=(), **data):
|
||||
self.sources[key].append({"kind": kind, "text": text, "base": base, "needs": [list(g) for g in needs], **data})
|
||||
|
||||
def _solve(self):
|
||||
status = defaultdict(int)
|
||||
changed, passes = True, 0
|
||||
while changed:
|
||||
changed, passes = False, passes + 1
|
||||
for key, srcs in self.sources.items():
|
||||
best = status[key]
|
||||
if best == 2:
|
||||
continue
|
||||
for src in srcs:
|
||||
v = RANK[src["base"]]
|
||||
for group in src["needs"]:
|
||||
v = min(v, max((status[k] for k in group), default=0))
|
||||
if v <= best:
|
||||
break
|
||||
best = max(best, v)
|
||||
if best != status[key]:
|
||||
status[key] = best
|
||||
changed = True
|
||||
self.status = status
|
||||
self.passes = passes
|
||||
for key, srcs in self.sources.items():
|
||||
for src in srcs:
|
||||
v, blocked = RANK[src["base"]], []
|
||||
for group in src["needs"]:
|
||||
g = max((status[k] for k in group), default=0)
|
||||
if g < 2:
|
||||
blocked.append(group)
|
||||
v = min(v, g)
|
||||
src["status"] = STATUS[v]
|
||||
blocked = [g for g in blocked if max((status[k] for k in g), default=0) < RANK[src["base"]]]
|
||||
blocked.sort(key=lambda g: max((status[k] for k in g), default=0)) # worst first
|
||||
src["blocked_by"] = [g[:6] for g in blocked][:3]
|
||||
|
||||
def get_status(self, key):
|
||||
return STATUS[self.status.get(key, 0)]
|
||||
|
||||
@property
|
||||
def btags(self):
|
||||
return self.tags["worldgen/biome"]
|
||||
|
||||
|
||||
def _chance(d):
|
||||
p = d["p"]
|
||||
text = "always" if p >= 0.995 else f"≈{p * 100:.3g}%"
|
||||
lo, hi = d["count"]
|
||||
if (lo, hi) != (1, 1):
|
||||
text += f", ×{int(lo)}" + (f"–{int(hi)}" if hi != lo else "")
|
||||
if d["notes"]:
|
||||
text += " (" + ", ".join(d["notes"]) + ")"
|
||||
return {"chance": text}
|
||||
|
||||
|
||||
def _drop_entries(drops):
|
||||
out = []
|
||||
for e in (drops or {}).get("entries", []):
|
||||
if "percentage" in e:
|
||||
text = f"{e['percentage']:g}%"
|
||||
elif "quantityRange" in e:
|
||||
text = f"×{e['quantityRange']}"
|
||||
else:
|
||||
text = "×" + str(e.get("quantity", 1))
|
||||
out.append({"item": norm_id(e["item"]), "chance": text})
|
||||
return out
|
||||
|
||||
|
||||
def _join(items, limit=6):
|
||||
items = list(items)
|
||||
if len(items) > limit:
|
||||
return ", ".join(items[:limit]) + f" and {len(items) - limit} more"
|
||||
return ", ".join(items)
|
||||
|
||||
|
||||
def _strings(obj):
|
||||
if isinstance(obj, str):
|
||||
yield obj
|
||||
elif isinstance(obj, dict):
|
||||
for v in obj.values():
|
||||
yield from _strings(v)
|
||||
elif isinstance(obj, list):
|
||||
for v in obj:
|
||||
yield from _strings(v)
|
||||
Reference in new issue
Block a user