Phase 5: breeding with Cobbreeding, Ditto a rare Deep Dark find
Adrian (2026-10-31): add Cobbreeding without a gate; only Ditto should
be rare, in an endgame area or gated behind something else.
- Cobbreeding 2.4.1 (Modrinth only) and Cloth Config (client) in the
pack; Cobblebase's hatchery takes Cobbreeding's eggs.
- Ditto moved from Button Isle to the Deep Dark with both of Cobblemon's
entries (rare in slime chunks, ultra-rare elsewhere): zone_spawns.py
pins take {"zone", "all_entries"}; Deep Dark target +1, Button Isle -1
so no other line moved; Southwold and Frostpeak guardians pinned (they
tie on base stats and flipped between runs).
- HMs' Trainer Ditto is made uncatchable; it still trades.
- check_deps.py skips other-side dependencies; Cobbreeding's harmless
client-mixin log line allowlisted.
- Self-tests cobbreeding_eggs_fit_the_hatchery, trainer_ditto_cant_be_caught;
docs, balance table, Phase 5 checklist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b304025c6
commit
5cae9252c3
20 files changed
+274
-148
No files matched your search
+8
-6
@@ -11,9 +11,10 @@ Build metadata after "+" is ignored, so Cobblemon "1.8.4+1.21.1" compares as
|
||||
1.8.4; qualifiers follow Maven (21-beta < 21 < 21.1.265). A mismatch on the `minecraft` dependency is only a warning: every JEI
|
||||
build for 1.21.1 declares "[1.21, 1.21.1)" and NeoForge 21.1 still loads it
|
||||
(checked on the test server, 2026-10-28), so this check is stricter than the
|
||||
loader there.
|
||||
loader there. A dependency limited to the other side (`side = "CLIENT"` in a
|
||||
server folder, e.g. Cobbreeding's Cloth Config) is skipped.
|
||||
|
||||
Usage: tools/check_deps.py <mods_dir> [--neoforge VER] [--minecraft VER]
|
||||
Usage: tools/check_deps.py <mods_dir> [--neoforge VER] [--minecraft VER] [--side server|client]
|
||||
"""
|
||||
import argparse
|
||||
import io
|
||||
@@ -65,7 +66,7 @@ def in_range(version, spec):
|
||||
return False
|
||||
|
||||
|
||||
def read_mods(data, name, found, deps):
|
||||
def read_mods(data, name, found, deps, side):
|
||||
z = zipfile.ZipFile(io.BytesIO(data))
|
||||
names = set(z.namelist())
|
||||
if "META-INF/neoforge.mods.toml" in names:
|
||||
@@ -78,10 +79,10 @@ def read_mods(data, name, found, deps):
|
||||
for mod_id, entries in toml.get("dependencies", {}).items():
|
||||
for dep in entries:
|
||||
required = dep.get("type", "required" if dep.get("mandatory", True) else "optional") == "required"
|
||||
if required:
|
||||
if required and dep.get("side", "BOTH").upper() in ("BOTH", side.upper()):
|
||||
deps.append((mod_id, dep["modId"], dep.get("versionRange", "*"), name))
|
||||
for inner in (n for n in names if n.startswith("META-INF/jarjar/") and n.endswith(".jar")):
|
||||
read_mods(z.read(inner), f"{name}!{inner.rsplit('/', 1)[-1]}", found, deps)
|
||||
read_mods(z.read(inner), f"{name}!{inner.rsplit('/', 1)[-1]}", found, deps, side)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -89,11 +90,12 @@ def main():
|
||||
parser.add_argument("mods_dir", type=Path)
|
||||
parser.add_argument("--neoforge")
|
||||
parser.add_argument("--minecraft", default="1.21.1")
|
||||
parser.add_argument("--side", choices=["server", "client"], default="server")
|
||||
args = parser.parse_args()
|
||||
|
||||
found, deps = {}, []
|
||||
for jar in sorted(args.mods_dir.glob("*.jar")):
|
||||
read_mods(jar.read_bytes(), jar.name, found, deps)
|
||||
read_mods(jar.read_bytes(), jar.name, found, deps, args.side)
|
||||
if args.neoforge:
|
||||
found["neoforge"] = (args.neoforge, "loader")
|
||||
found["minecraft"] = (args.minecraft, "game")
|
||||
|
||||
@@ -10,3 +10,7 @@ Failed to parse jigsaw replacement state 'minecraft:dirt_block'
|
||||
# Cobblemon 1.8.4 bug: habitats/parched_peat_bog7.nbt has a jigsaw block whose final state is the misspelt
|
||||
# "minecraft:strucutre_void". Logged at worldgen; the piece still generates (as air there).
|
||||
Failed to parse jigsaw replacement state 'minecraft:strucutre_void'
|
||||
# Cobbreeding 2.4.1 lists PastureGUIMixin (a mixin into Cobblemon's client-only pasture screen) among its
|
||||
# common mixins, so on a dedicated server Mixin looks up the screen's superclass once while preparing and
|
||||
# NeoForge logs this. The screen never loads on a server; the server and Cobbreeding work (self-tests).
|
||||
Attempted to load class net/minecraft/client/gui/screens/Screen for invalid dist DEDICATED_SERVER
|
||||
@@ -23,7 +23,9 @@ datapacks/core/data/cobblesurvival/spawn_pool_world/<zone>.json:
|
||||
doesn't spawn there. If no stage fits, the closest one spawns across the whole band. Every
|
||||
stage that reaches the band spawns up to its top (Adrian, 2026-10-31; Cobblemon's own ranges
|
||||
stop where the next stage takes over, which left e.g. Dusclops at Lv 45–46 in the Deep Dark).
|
||||
3. world/zones.json → "species" pins a line to a zone by hand: {"pikachu": "southwold"}.
|
||||
3. world/zones.json → "species" pins a line to a zone by hand: {"pikachu": "southwold"}. The long form
|
||||
{"ditto": {"zone": "deep_dark", "all_entries": true}} also keeps all of the line's entries when no stage
|
||||
fits the band (otherwise the closest stage brings one entry).
|
||||
4. In cave zones every entry spawns in the cave's biome, and sky-light minimums, "can see sky", time
|
||||
of day, excluded biomes and the "natural" ground preset are dropped (there's no sky or day underground).
|
||||
5. Strong flyers (STRONG_FLYER_*) only spawn in zones whose band starts at HIGH_ZONE_LEVEL or more.
|
||||
@@ -180,7 +182,9 @@ def main():
|
||||
continue
|
||||
lines[line_root(member, species)].append((entry, set(s["biomes"]), member, s["level"]))
|
||||
|
||||
pinned = {f"cobblemon:{k}" if ":" not in k else k: v for k, v in plan.get("species", {}).items()}
|
||||
pins = {f"cobblemon:{k}" if ":" not in k else k: v for k, v in plan.get("species", {}).items()}
|
||||
pinned = {root: pin["zone"] if isinstance(pin, dict) else pin for root, pin in pins.items()}
|
||||
all_entries = {root for root, pin in pins.items() if isinstance(pin, dict) and pin.get("all_entries")}
|
||||
flying_lines = {root for root in lines if root in strong_flyers}
|
||||
|
||||
def allowed(root, zid):
|
||||
@@ -269,6 +273,8 @@ def main():
|
||||
# No stage reaches the band: the closest stage spawns across all of it.
|
||||
e, b, m, lv = min(usable, key=lambda u: min(abs(u[3][0] - hi), abs(u[3][1] - lo)))
|
||||
fitting = [(e, m, lo, hi)]
|
||||
if root in all_entries:
|
||||
fitting = [(ue, m, lo, hi) for ue, _, um, ulv in usable if um == m and ulv == lv]
|
||||
seen = set()
|
||||
for entry, member, a, b in fitting:
|
||||
spawn = copy.deepcopy(entry)
|
||||
|
||||
Reference in new issue
Block a user