From 5cae9252c3133487cf4cc8d6ad2551f8607fc1a4 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 31 Oct 2026 19:13:55 +0100 Subject: [PATCH] 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 --- .../spawn_pool_world/button_isle.json | 132 ++++++------------ .../spawn_pool_world/deep_dark.json | 100 ++++++++----- docs/addon-status.md | 4 +- docs/balance.md | 5 +- docs/checklists/phase-5.md | 4 + docs/design.md | 11 +- docs/licenses.md | 2 + docs/zone-species.md | 8 +- .../dev/cobblesurvival/CobbleSurvival.kt | 2 + .../cobblesurvival/selftest/BreedingTests.kt | 47 +++++++ .../dev/cobblesurvival/selftest/SelfTest.kt | 2 +- .../cobblesurvival/world/SpecialPokemon.kt | 28 +++- pack/index.toml | 10 ++ pack/mods/cloth-config.pw.toml | 13 ++ pack/mods/cobbreeding.pw.toml | 13 ++ pack/pack.toml | 2 +- tools/check_deps.py | 14 +- tools/smoke_allowlist.txt | 4 + tools/zone_spawns.py | 10 +- world/zones.json | 11 +- 20 files changed, 274 insertions(+), 148 deletions(-) create mode 100644 mod/src/main/kotlin/dev/cobblesurvival/selftest/BreedingTests.kt create mode 100644 pack/mods/cloth-config.pw.toml create mode 100644 pack/mods/cobbreeding.pw.toml diff --git a/datapacks/core/data/cobblesurvival/spawn_pool_world/button_isle.json b/datapacks/core/data/cobblesurvival/spawn_pool_world/button_isle.json index c774c52..01d34e3 100644 --- a/datapacks/core/data/cobblesurvival/spawn_pool_world/button_isle.json +++ b/datapacks/core/data/cobblesurvival/spawn_pool_world/button_isle.json @@ -455,7 +455,7 @@ "timeRange": "day", "zone": "button_isle" }, - "id": "button_isle-combee-male-1-33", + "id": "button_isle-combee-male-1-31", "level": "2-10", "pokemon": "combee male", "presets": [ @@ -494,7 +494,7 @@ "timeRange": "day", "zone": "button_isle" }, - "id": "button_isle-combee-male-2-34", + "id": "button_isle-combee-male-2-32", "level": "2-10", "pokemon": "combee male", "presets": [ @@ -512,58 +512,6 @@ "multiplier": 5.0 } }, - { - "anticondition": { - "biomes": [ - "#cobblemon:is_ocean" - ] - }, - "bucket": "rare", - "condition": { - "biomes": [ - "#cobblemon:is_overworld" - ], - "isSlimeChunk": true, - "maxSkyLight": 15, - "minSkyLight": 8, - "zone": "button_isle" - }, - "id": "button_isle-ditto-1-31", - "level": "4-10", - "pokemon": "ditto", - "presets": [ - "natural" - ], - "spawnablePositionType": "grounded", - "type": "pokemon", - "weight": 100.0 - }, - { - "anticondition": { - "biomes": [ - "#cobblemon:is_ocean" - ] - }, - "bucket": "ultra-rare", - "condition": { - "biomes": [ - "#cobblemon:is_overworld" - ], - "isSlimeChunk": false, - "maxSkyLight": 15, - "minSkyLight": 8, - "zone": "button_isle" - }, - "id": "button_isle-ditto-2-32", - "level": "4-10", - "pokemon": "ditto", - "presets": [ - "natural" - ], - "spawnablePositionType": "grounded", - "type": "pokemon", - "weight": 60.0 - }, { "anticondition": { "biomes": [ @@ -796,7 +744,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-meditite-2-46", + "id": "button_isle-meditite-2-44", "level": "5-10", "pokemon": "meditite", "presets": [ @@ -825,7 +773,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-meditite-3-47", + "id": "button_isle-meditite-3-45", "level": "5-10", "pokemon": "meditite", "presets": [ @@ -859,7 +807,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-meowth-1-66", + "id": "button_isle-meowth-1-64", "level": "4-10", "pokemon": "meowth", "presets": [ @@ -905,7 +853,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-meowth-2-67", + "id": "button_isle-meowth-2-65", "level": "4-10", "pokemon": "meowth", "presets": [ @@ -939,7 +887,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-meowth-3-68", + "id": "button_isle-meowth-3-66", "level": "4-10", "pokemon": "meowth", "presets": [ @@ -977,7 +925,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-meowth-4-69", + "id": "button_isle-meowth-4-67", "level": "4-10", "pokemon": "meowth", "presets": [ @@ -1022,7 +970,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-meowth-alolan-5-70", + "id": "button_isle-meowth-alolan-5-68", "level": "4-10", "pokemon": "meowth alolan", "presets": [ @@ -1061,7 +1009,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-meowth-alolan-6-71", + "id": "button_isle-meowth-alolan-6-69", "level": "4-10", "pokemon": "meowth alolan", "presets": [ @@ -1098,7 +1046,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-meowth-alolan-7-72", + "id": "button_isle-meowth-alolan-7-70", "level": "4-10", "pokemon": "meowth alolan", "spawnablePositionType": "grounded", @@ -1176,7 +1124,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-morpeko-1-65", + "id": "button_isle-morpeko-1-63", "level": "2-10", "pokemon": "morpeko", "presets": [ @@ -1283,7 +1231,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-rattata-1-50", + "id": "button_isle-rattata-1-48", "level": "2-10", "pokemon": "rattata", "presets": [ @@ -1316,7 +1264,7 @@ "minY": 0, "zone": "button_isle" }, - "id": "button_isle-rattata-2-51", + "id": "button_isle-rattata-2-49", "level": "2-10", "pokemon": "rattata", "presets": [ @@ -1348,7 +1296,7 @@ "minY": 0, "zone": "button_isle" }, - "id": "button_isle-rattata-3-52", + "id": "button_isle-rattata-3-50", "level": "2-10", "pokemon": "rattata", "presets": [ @@ -1381,7 +1329,7 @@ "minSkyLight": 0, "zone": "button_isle" }, - "id": "button_isle-rattata-alolan-4-53", + "id": "button_isle-rattata-alolan-4-51", "level": "2-10", "pokemon": "rattata alolan", "presets": [ @@ -1407,7 +1355,7 @@ "maxY": 0, "zone": "button_isle" }, - "id": "button_isle-rattata-alolan-5-54", + "id": "button_isle-rattata-alolan-5-52", "level": "2-10", "pokemon": "rattata alolan", "presets": [ @@ -1431,7 +1379,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-sableye-1-37", + "id": "button_isle-sableye-1-35", "level": "2-10", "pokemon": "sableye", "presets": [ @@ -1454,7 +1402,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-scatterbug-1-48", + "id": "button_isle-scatterbug-1-46", "level": "2-10", "pokemon": "scatterbug", "presets": [ @@ -1511,7 +1459,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-skitty-1-41", + "id": "button_isle-skitty-1-39", "level": "2-10", "pokemon": "skitty", "presets": [ @@ -1544,7 +1492,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-skitty-2-42", + "id": "button_isle-skitty-2-40", "level": "2-10", "pokemon": "skitty", "presets": [ @@ -1575,7 +1523,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-skitty-4-43", + "id": "button_isle-skitty-4-41", "level": "2-10", "pokemon": "skitty", "presets": [ @@ -1602,7 +1550,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-skitty-5-44", + "id": "button_isle-skitty-5-42", "level": "2-10", "pokemon": "skitty", "presets": [ @@ -1629,7 +1577,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-smeargle-1-35", + "id": "button_isle-smeargle-1-33", "level": "2-10", "pokemon": "smeargle", "presets": [ @@ -1659,7 +1607,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-smeargle-2-36", + "id": "button_isle-smeargle-2-34", "level": "2-10", "pokemon": "smeargle", "presets": [ @@ -1688,7 +1636,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-spewpa-1-49", + "id": "button_isle-spewpa-1-47", "level": "9-10", "pokemon": "spewpa", "presets": [ @@ -1722,7 +1670,7 @@ "timeRange": "night", "zone": "button_isle" }, - "id": "button_isle-spinarak-1-38", + "id": "button_isle-spinarak-1-36", "level": "2-10", "pokemon": "spinarak", "presets": [ @@ -1749,7 +1697,7 @@ "minSkyLight": 0, "zone": "button_isle" }, - "id": "button_isle-spinarak-2-39", + "id": "button_isle-spinarak-2-37", "level": "2-10", "pokemon": "spinarak", "presets": [ @@ -1775,7 +1723,7 @@ "minY": 0, "zone": "button_isle" }, - "id": "button_isle-spinarak-3-40", + "id": "button_isle-spinarak-3-38", "level": "2-10", "pokemon": "spinarak", "presets": [ @@ -1804,7 +1752,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-sunkern-2-55", + "id": "button_isle-sunkern-2-53", "level": "2-10", "pokemon": "sunkern", "presets": [ @@ -1910,7 +1858,7 @@ ], "zone": "button_isle" }, - "id": "button_isle-wooper-3-56", + "id": "button_isle-wooper-3-54", "level": "2-10", "pokemon": "wooper", "presets": [ @@ -1943,7 +1891,7 @@ "minSkyLight": 0, "zone": "button_isle" }, - "id": "button_isle-wooper-4-57", + "id": "button_isle-wooper-4-55", "level": "2-10", "pokemon": "wooper", "presets": [ @@ -1975,7 +1923,7 @@ "minSkyLight": 0, "zone": "button_isle" }, - "id": "button_isle-wooper-8-58", + "id": "button_isle-wooper-8-56", "level": "2-10", "pokemon": "wooper", "spawnablePositionType": "fishing", @@ -1994,7 +1942,7 @@ "canSeeSky": true, "zone": "button_isle" }, - "id": "button_isle-wooper-paldean-26-61", + "id": "button_isle-wooper-paldean-26-59", "level": "2-10", "pokemon": "wooper paldean", "spawnablePositionType": "fishing", @@ -2011,7 +1959,7 @@ "minLureLevel": 1, "zone": "button_isle" }, - "id": "button_isle-wooper-paldean-27-62", + "id": "button_isle-wooper-paldean-27-60", "level": "2-10", "pokemon": "wooper paldean", "spawnablePositionType": "fishing", @@ -2039,7 +1987,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-wooper-paldean-31-63", + "id": "button_isle-wooper-paldean-31-61", "level": "2-10", "pokemon": "wooper paldean", "spawnablePositionType": "fishing", @@ -2058,7 +2006,7 @@ "minSkyLight": 8, "zone": "button_isle" }, - "id": "button_isle-wooper-paldean-32-64", + "id": "button_isle-wooper-paldean-32-62", "level": "2-10", "pokemon": "wooper paldean", "spawnablePositionType": "fishing", @@ -2089,7 +2037,7 @@ "rodType": "cobblemon:love_rod", "zone": "button_isle" }, - "id": "button_isle-wooper-true-15-59", + "id": "button_isle-wooper-true-15-57", "level": "2-10", "pokemon": "wooper wooper_heart=true", "spawnablePositionType": "fishing", @@ -2113,7 +2061,7 @@ "minSkyLight": 0, "zone": "button_isle" }, - "id": "button_isle-wooper-true-17-60", + "id": "button_isle-wooper-true-17-58", "level": "2-10", "pokemon": "wooper wooper_heart=true", "spawnablePositionType": "fishing", @@ -2164,7 +2112,7 @@ "minSkyLight": 0, "zone": "button_isle" }, - "id": "button_isle-wynaut-1-45", + "id": "button_isle-wynaut-1-43", "level": "2-10", "pokemon": "wynaut", "presets": [ diff --git a/datapacks/core/data/cobblesurvival/spawn_pool_world/deep_dark.json b/datapacks/core/data/cobblesurvival/spawn_pool_world/deep_dark.json index bc077ef..ed1a117 100644 --- a/datapacks/core/data/cobblesurvival/spawn_pool_world/deep_dark.json +++ b/datapacks/core/data/cobblesurvival/spawn_pool_world/deep_dark.json @@ -16,7 +16,7 @@ ], "zone": "deep_dark" }, - "id": "deep_dark-aegislash-2-29", + "id": "deep_dark-aegislash-2-31", "level": "45-60", "pokemon": "aegislash", "spawnablePositionType": "grounded", @@ -33,7 +33,7 @@ "minY": 0, "zone": "deep_dark" }, - "id": "deep_dark-bronzong-1-30", + "id": "deep_dark-bronzong-1-32", "level": "45-60", "pokemon": "bronzong", "presets": [], @@ -54,7 +54,7 @@ ], "zone": "deep_dark" }, - "id": "deep_dark-bronzong-2-31", + "id": "deep_dark-bronzong-2-33", "level": "45-60", "pokemon": "bronzong", "presets": [ @@ -76,7 +76,7 @@ ], "zone": "deep_dark" }, - "id": "deep_dark-carbink-1-25", + "id": "deep_dark-carbink-1-27", "level": "45-60", "pokemon": "carbink", "presets": [], @@ -103,7 +103,7 @@ ], "zone": "deep_dark" }, - "id": "deep_dark-coalossal-1-17", + "id": "deep_dark-coalossal-1-19", "level": "45-60", "pokemon": "coalossal", "presets": [], @@ -125,7 +125,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-corviknight-1-5", + "id": "deep_dark-corviknight-1-7", "level": "45-60", "pokemon": "corviknight", "presets": [ @@ -150,7 +150,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-crobat-1-20", + "id": "deep_dark-crobat-1-22", "level": "45-60", "pokemon": "crobat", "presets": [], @@ -173,7 +173,7 @@ "canSeeSky": false, "zone": "deep_dark" }, - "id": "deep_dark-crobat-4-21", + "id": "deep_dark-crobat-4-23", "level": "45-60", "pokemon": "crobat", "presets": [ @@ -197,6 +197,42 @@ } ] }, + { + "bucket": "rare", + "condition": { + "biomes": [ + "minecraft:deep_dark" + ], + "isSlimeChunk": true, + "maxSkyLight": 15, + "zone": "deep_dark" + }, + "id": "deep_dark-ditto-1-0", + "level": "45-60", + "pokemon": "ditto", + "presets": [], + "spawnablePositionType": "grounded", + "type": "pokemon", + "weight": 100.0 + }, + { + "bucket": "ultra-rare", + "condition": { + "biomes": [ + "minecraft:deep_dark" + ], + "isSlimeChunk": false, + "maxSkyLight": 15, + "zone": "deep_dark" + }, + "id": "deep_dark-ditto-2-1", + "level": "45-60", + "pokemon": "ditto", + "presets": [], + "spawnablePositionType": "grounded", + "type": "pokemon", + "weight": 60.0 + }, { "bucket": "uncommon", "condition": { @@ -209,7 +245,7 @@ ], "zone": "deep_dark" }, - "id": "deep_dark-doublade-2-28", + "id": "deep_dark-doublade-2-30", "level": "45-60", "pokemon": "doublade", "spawnablePositionType": "grounded", @@ -230,7 +266,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-dusclops-1-0", + "id": "deep_dark-dusclops-1-2", "level": "45-60", "pokemon": "dusclops", "presets": [], @@ -252,7 +288,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-dusknoir-1-1", + "id": "deep_dark-dusknoir-1-3", "level": "45-60", "pokemon": "dusknoir", "presets": [], @@ -270,7 +306,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-gallade-1-14", + "id": "deep_dark-gallade-1-16", "level": "45-60", "pokemon": "gallade", "presets": [], @@ -288,7 +324,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-gardevoir-1-13", + "id": "deep_dark-gardevoir-1-15", "level": "45-60", "pokemon": "gardevoir", "presets": [], @@ -306,7 +342,7 @@ "maxY": 0, "zone": "deep_dark" }, - "id": "deep_dark-gigalith-1-15", + "id": "deep_dark-gigalith-1-17", "level": "45-60", "pokemon": "gigalith", "presets": [], @@ -323,7 +359,7 @@ "maxSkyLight": 7, "zone": "deep_dark" }, - "id": "deep_dark-gigalith-2-16", + "id": "deep_dark-gigalith-2-18", "level": "45-60", "pokemon": "gigalith", "presets": [], @@ -343,7 +379,7 @@ ], "zone": "deep_dark" }, - "id": "deep_dark-glimmora-1-7", + "id": "deep_dark-glimmora-1-9", "level": "45-60", "pokemon": "glimmora", "presets": [], @@ -372,7 +408,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-golbat-1-18", + "id": "deep_dark-golbat-1-20", "level": "45-60", "pokemon": "golbat", "presets": [], @@ -395,7 +431,7 @@ "canSeeSky": false, "zone": "deep_dark" }, - "id": "deep_dark-golbat-4-19", + "id": "deep_dark-golbat-4-21", "level": "45-60", "pokemon": "golbat", "presets": [ @@ -435,7 +471,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-honchkrow-1-4", + "id": "deep_dark-honchkrow-1-6", "level": "45-60", "pokemon": "honchkrow", "presets": [ @@ -465,7 +501,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-houndstone-1-2", + "id": "deep_dark-houndstone-1-4", "level": "45-60", "pokemon": "houndstone", "presets": [], @@ -487,7 +523,7 @@ ], "zone": "deep_dark" }, - "id": "deep_dark-klinklang-2-9", + "id": "deep_dark-klinklang-2-11", "level": "49-60", "pokemon": "klinklang", "presets": [ @@ -512,7 +548,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-mabosstiff-1-23", + "id": "deep_dark-mabosstiff-1-25", "level": "45-60", "pokemon": "mabosstiff", "presets": [ @@ -540,7 +576,7 @@ "isSlimeChunk": true, "zone": "deep_dark" }, - "id": "deep_dark-milotic-1-6", + "id": "deep_dark-milotic-1-8", "level": "45-60", "pokemon": "milotic", "spawnablePositionType": "fishing", @@ -556,7 +592,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-mimikyu-1-3", + "id": "deep_dark-mimikyu-1-5", "level": "45-60", "pokemon": "mimikyu", "presets": [], @@ -573,7 +609,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-mrmime-1-10", + "id": "deep_dark-mrmime-1-12", "level": "45-60", "pokemon": "mrmime", "presets": [ @@ -598,7 +634,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-mrmime-galarian-4-11", + "id": "deep_dark-mrmime-galarian-4-13", "level": "45-60", "pokemon": "mrmime galarian", "presets": [ @@ -623,7 +659,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-mrrime-1-12", + "id": "deep_dark-mrrime-1-14", "level": "45-60", "pokemon": "mrrime", "presets": [ @@ -642,7 +678,7 @@ "maxSkyLight": 7, "zone": "deep_dark" }, - "id": "deep_dark-probopass-1-8", + "id": "deep_dark-probopass-1-10", "level": "45-60", "pokemon": "probopass", "presets": [], @@ -659,7 +695,7 @@ "maxSkyLight": 15, "zone": "deep_dark" }, - "id": "deep_dark-revavroom-1-24", + "id": "deep_dark-revavroom-1-26", "level": "45-60", "pokemon": "revavroom", "presets": [], @@ -682,7 +718,7 @@ "maxSkyLight": 7, "zone": "deep_dark" }, - "id": "deep_dark-steelix-1-22", + "id": "deep_dark-steelix-1-24", "level": "45-60", "pokemon": "steelix", "presets": [], @@ -699,7 +735,7 @@ "maxSkyLight": 7, "zone": "deep_dark" }, - "id": "deep_dark-zweilous-1-26", + "id": "deep_dark-zweilous-1-28", "level": "50-60", "pokemon": "zweilous", "presets": [], @@ -723,7 +759,7 @@ "maxY": 0, "zone": "deep_dark" }, - "id": "deep_dark-zweilous-2-27", + "id": "deep_dark-zweilous-2-29", "level": "50-60", "pokemon": "zweilous", "presets": [], diff --git a/docs/addon-status.md b/docs/addon-status.md index 07c91d5..a4e5c30 100644 --- a/docs/addon-status.md +++ b/docs/addon-status.md @@ -41,6 +41,8 @@ Checked 2026-10-28 against Cobblemon **1.8.4** (NeoForge, MC 1.21.1). Re-check b | Cobblemon TP-Pastures | pastured Pokémon teleported back, not recalled (Phase 2) | `cobblemon_tp_pastures-1.21.1-1.3.0.jar` (MR `so0hyqC2`) | MR + CF | ⚠️ Declares `[1.7.1,)`; loads on 1.8.4, behaviour still to be checked in-game | | Lootr | loot containers refill 1 hour after first opened, loot per player (Phase 2) | `lootr-neoforge-1.21.1-1.11.38.127.jar` (MR `SPOrIzX8`, CF `8968561`) | MR + CF | n/a (no Cobblemon dependency); converts Cobblemon's loot chests too | | Cobblemon HMs | field moves (Phase 5) | `cobblemon-hms-neoforge-4.1.0.jar` (MR `uC02CqRy`) | MR; also on CF (project `1587850`), not linked in the pack yet | ✅ Declares Cobblemon ≥ 1.8.0; loads and runs on 1.8.4 (self-tests, incl. Rock Smash on a regenerating vein). Added 2026-10-31 | +| Cobbreeding | breeding: eggs from Pasture Blocks, for Cobblebase's hatchery too (Phase 5) | `Cobbreeding-neoforge-2.4.1.jar` (MR `jzxflH2c`) | **MR only** (the authors won't upload to CurseForge) | ✅ Declares `[1.8.2,)`; loads and runs on 1.8.4 (self-tests). Logs one harmless client-mixin error on a dedicated server (allowlisted). Added 2026-10-31 | +| Cloth Config API | Cobbreeding's config screen (client only) | `cloth-config-15.0.140-neoforge.jar` (MR `izKINKFg`) | MR + CF | n/a (no Cobblemon dependency); side: client | | Better Advanced Tooltips | none: ships *inside* KubeJS as jar-in-jar | — | — | Modrinth marks it as a required KubeJS dependency, but KubeJS declares it optional and embeds it. Not referenced separately | JEI declares Minecraft `[1.21, 1.21.1)` in every 1.21.1 build, which a strict reading excludes; NeoForge 21.1.265 loads it anyway (server boot checked 2026-10-28). @@ -53,7 +55,7 @@ JEI declares Minecraft `[1.21, 1.21.1)` in every 1.21.1 build, which a strict re | Chunky | 1 (build tool only, not in the pack) | 1.4.23 (MR `LuFhm4eU`, CF project `485681`) | MR + CF | n/a. `tools/make_world.py` adds it to the test server only while pregenerating | | Cold Sweat | 3 | 2.4.4 (MR `UGoodVYD`, CF `9215368`) | MR + CF | **In the pack** (2026-10-29); same jar on both platforms | | Regenerating Ore Veins | 4 | 1.4.0 (CF `8066055`) | **CF only** | **In the pack** (2026-10-30); n/a (no Cobblemon dependency). Left out of the Modrinth export | -| Cobblebase | 4 | 2.0.1+1.8.0 (MR `pFYNQ6W2`) | MR has the 1.8 build; **CF only has 2.0.0+1.7.0** | ✅ 2.0.1 requires ≥ 1.8.0 and fixes a startup crash on 1.8. Modrinth lists Xaero's Minimap, Cobbreeding and Cloth Config as required, but the jar declares only Cobblemon and Kotlin for Forge. **In the pack** (2026-10-30) without Cobbreeding and Cloth Config; every skill behind a soft gate on the pastured Pokémon (companion mod, `progression_gates`). Breeder and Egg Hatcher do nothing without Cobbreeding | +| Cobblebase | 4 | 2.0.1+1.8.0 (MR `pFYNQ6W2`) | MR has the 1.8 build; **CF only has 2.0.0+1.7.0** | ✅ 2.0.1 requires ≥ 1.8.0 and fixes a startup crash on 1.8. Modrinth lists Xaero's Minimap, Cobbreeding and Cloth Config as required, but the jar declares only Cobblemon and Kotlin for Forge. **In the pack** (2026-10-30); every skill behind a soft gate on the pastured Pokémon (companion mod, `progression_gates`). Cobbreeding and Cloth Config joined on 2026-10-31, so Breeder and Egg Hatcher work | | Cobblemon Poke Power | 4 | 1.3.1 (MR `vGpaTxmN`) | **MR only** | ✅ Declares `[1.8.0,)`. Needs Matthiesen Core. **In the pack** (2026-10-30, Adrian: use the existing option, gated): the Power Block is gated by its recipe (redstone blocks from the Verdant Jungle). Jade is an optional extra it supports | | Cobblemon Quests (FTB Quests integration) | 8 | 1.2.0 (MR `8KeptEg2`; CF project `922979`) | MR + CF | ❌ Targets 1.7. Its own changelog says it does not work with FTB Quests ≥ 2101.1.19; the pack has 2101.1.37. Blocked until updated | | Quests & Teams Fixes (flexible-mode soft-lock fix) | 8 | — | MR | ❌ Only 1.20.1 builds exist. Test the soft-lock on FTB Quests 2101.1.37 ourselves | diff --git a/docs/balance.md b/docs/balance.md index 6ff95d4..0cc35fd 100644 --- a/docs/balance.md +++ b/docs/balance.md @@ -17,17 +17,18 @@ Every tunable lives in a config file or in this table. **Placeholder** means gue | Fainted Pokémon auto-recovery | never (`defaultFaintTimer` 1e9 s) | Decided (permadeath) | `config/cobblemon/main.json` | | | Zone level bands | Button Isle 2–10, Southwold 8–18, Westmarch 15–25, Verdant Jungle 18–30, Highland Meadows 25–35, Mangrove Mire 25–38, Sunscorch 30–45, Frostpeak 40–55, Shallows 5–20, Deep Sea 30–50, Lush Caves 12–28, Dripstone 22–38, Deep Dark 45–60, Basalt Furnace 55–70 | Decided (Adrian, 2026-10-30) | `world/zones.json` → `levels`, then `tools/zone_layout.py` and `tools/zone_spawns.py` | Zone shapes approved (Adrian, 2026-10-29) | | Evolution lines per zone (targets) | 15–35 per zone, scaled to the 377 lines available | Decided (Adrian, 2026-10-30) | `world/zones.json` → `lines` | Result per zone in `docs/zone-species.md` | -| Lines pinned to a zone | Durant, Torkoal, Heatmor → Basalt Furnace | Decided (Adrian, 2026-10-30) | `world/zones.json` → `species` | Basalt's own lines were all too big for its caves | +| Lines pinned to a zone | Durant, Torkoal, Heatmor → Basalt Furnace; Ditto → Deep Dark, rare bucket in slime chunks and ultra-rare elsewhere (Placeholder; Adrian, 2026-10-31: rare, in an endgame area) | Decided (Adrian, 2026-10-30) | `world/zones.json` → `species` | Basalt's own lines were all too big for its caves. Ditto breeds with anything, so it's an endgame find; Deep Dark target 21 lines, Button Isle 19, so nothing else moved | | Comfortable temperature range | 0.25–1.51 (Cold Sweat units; Cold Sweat: 0.4–1.51) | Decided (Adrian, 2026-10-30) | `config/coldsweat/main.toml` | Keeps the lowlands' nights livable without gear | | Windswept hills temperature | 45–72 °F (Cold Sweat: 24–58 °F) | Decided (Adrian, 2026-10-30) | `config/coldsweat/world.toml` | They lie in the starter lowlands too | | Insulating armour | fur set (cold 10/14/12/10), hide set (heat 10/14/12/10), as Cold Sweat; leather armour none (Cold Sweat: 5/7/6/5 both) | Decided (Adrian, 2026-10-30) | `config/coldsweat/item.toml` | Frostpeak at noon: about 1 min bare, 14 min in fur; Sunscorch at noon: about 2 min bare, 25 min in hide | | Sewn-in insulation | goat fur 2 cold, hoglin hide 2 heat, wool 0.5 cold, leather 0.5/0.5 (Cold Sweat: wool 1.5, leather 1/1, rabbit hide 1.5 heat) | Decided (Adrian, 2026-10-30) | `config/coldsweat/item.toml` | | | Goat Fur / Hoglin Hide recipes | 8 wool + 1 Heat Rock → 8 Goat Fur; 8 leather + 1 Icy Rock → 8 Hoglin Hide (a set takes 24) | Decided (Adrian, 2026-10-30) | `kubejs/server_scripts/insulation.js` | Heat and Icy Rocks come from the guardians before Frostpeak and Sunscorch (archaeology finds too) | | Guardian respawn | 5 hours of game time after being beaten | Decided (Adrian, 2026-10-29) | `world/zones.json` → `guardian_respawn_hours` | Fixed spots | -| Guardian strength | strongest Pokémon spawning in the zone, band top + 5 levels, 1.5× size, uncatchable | Decided (Adrian, 2026-10-30) | `tools/zone_spawns.py` (`GUARDIAN_*`), `world/zones.json` → `guardian.species` | | +| Guardian strength | strongest Pokémon spawning in the zone, band top + 5 levels, 1.5× size, uncatchable | Decided (Adrian, 2026-10-30) | `tools/zone_spawns.py` (`GUARDIAN_*`), `world/zones.json` → `guardian.species` | Southwold (Pinsir) and Frostpeak (Tyranitar) are pinned: they tie on base stats with Hawlucha and Archaludon | | Guardian drops | Button Isle 10 Poké Balls + 4 Potions; Southwold 6 Great Balls + 4 Exp. Candy S; Westmarch and Jungle Mire Waders + 4 Pecha Berries + 2 Exp. Candy M; Mire and Highlands 3 Icy Rocks + 4 Super Potions; Sunscorch 3 Heat Rocks + 2 Exp. Candy L; Frostpeak and Deep Dark 3 Icy Rocks + 2 Exp. Candy L; Deep Sea Mystic Water + 3 Exp. Candy M | Decided (Adrian, 2026-10-30) | `world/zones.json` → `guardian.drops` | 3 rocks make one full fur or hide set | | Mire poison | Poison I for 7 s, renewed every 5 s while in the Mire without Mire Waders | Decided (Adrian, 2026-10-30) | `datapacks/core/data/cobblesurvival/zone_hazards/mire.json` | Vanilla Poison stops at half a heart | | Field moves (Cobblemon HMs) | mod defaults: all 39 abilities on, no level minimum, no fullness or friendship cost; Rock Smash breaks up to 32 connected ore blocks with no cooldown | Placeholder | `config/cobblemon_hms.json` (generated by the mod; the pack ships none yet) | Rock Smash mines any ore, whatever pickaxe the player has. HMs' `minLevel` is the player's XP level, not the Pokémon's | +| Breeding (Cobbreeding) | mod defaults: an egg every 8,000–14,000 ticks per breeding pasture, hatch multiplier 1.0 (about 10 min for 20 egg cycles), Masuda shiny boost ×4, no random eggs from two Dittos, no gate | Placeholder | `config/cobbreeding/main.json` (written by the mod; the pack ships none) | Adrian, 2026-10-31: breeding without a gate | | Level pressure | full experience up to 5 levels above the zone's band, then 10 % less per level, at least 10 % (Exp. Candy and commands exempt) | Decided (Adrian, 2026-10-30) | `world/zones.json` → `level_pressure` | E.g. on Button Isle (up to Lv 10): Lv 15 full, Lv 20 half, Lv 24+ a tenth | | Strong flyer | ridable through the air with top speed ≥ 60 or top stamina ≥ 85 (Cobblemon ride stats) | Decided (Adrian, 2026-10-30) | `tools/zone_spawns.py` (`STRONG_FLYER_*`) | 29 species | | High zone for strong flyers | band starts at Lv 30 or more (Sunscorch, Frostpeak, Deep Sea, Deep Dark, Basalt Furnace) | Decided (Adrian, 2026-10-30) | `tools/zone_spawns.py` (`HIGH_ZONE_LEVEL`) | | diff --git a/docs/checklists/phase-5.md b/docs/checklists/phase-5.md index 0faa1ac..a2723b5 100644 --- a/docs/checklists/phase-5.md +++ b/docs/checklists/phase-5.md @@ -19,6 +19,7 @@ - Mining drops its loot at the Pokémon's feet; an **Item Gatherer** in the same pasture carries it into the chest (Cobblebase's design). Without one, the loot despawns after 5 minutes. - A gated job doesn't stick: assigned to a Pokémon that doesn't pass, it's taken back at once (gone from Buffs → Active) with a message saying what the Pokémon needs. Automated: `gated_jobs_are_taken_back`. - Mining, the Ore Finder and the Evolution-item Finder only give the ores and evolution stones of the pasture's zone; Mining no longer gives type gems. Automated: `base_jobs_dig_their_zones_ores`. +- **Breeding (2026-10-31):** Cobbreeding is in, without a gate; Cobblebase's hatchery takes its eggs. Ditto only spawns in the Deep Dark now, and rarely; HMs' Trainer Ditto can't be caught. Automated: `cobbreeding_eggs_fit_the_hatchery`, `trainer_ditto_cant_be_caught`. - **Shoulder passives:** a shoulder Pokémon gives an effect by type (Flying slow falling, Electric speed, Rock/Ground/Steel haste; gated: Fire warmth, Ice frigidness, Water water breathing, Ghost/Dark night vision). Automated: `shoulder_pokemon_give_passives`. ## In game (Prism instance) @@ -31,6 +32,9 @@ - [ ] With a pasture on Button Isle, Mining brings coal, tumblestones, cobblestone, gravel, flint and fossils, but no iron, copper, gold, diamonds or gems. In Westmarch it brings raw iron and thunder stones instead of coal. - [ ] Craft and place a Power Block in survival: no guardian needed. - [ ] Put Pidgey (or another Flying type) on your shoulder: slow falling. A low-level Fire type on your shoulder: a message about warmth; with a sun stone or lapis held, Cold Sweat's warmth shows. +- [ ] Put two compatible Pokémon (e.g. two Eevee, or any Pokémon and a Ditto) in a Pasture Block and switch breeding on with its button: after 7–12 minutes an egg sits in the pasture's nest. Carry it: it hatches after a while. +- [ ] Put an egg in a chest next to a pasture with a Pokémon working as Egg Hatcher (Deep Dark gate: Lv 60, or holding a diamond or dusk stone): it takes the egg and hatches it. +- [ ] Throw a ball at HMs' Trainer Ditto: it can't be caught, but it still trades. - [ ] Look at the other abilities your Pokémon offer on the wheel (Cut, Strength, Surf boosts, ...) and note any you want switched off or tuned. ## Ask Adrian diff --git a/docs/design.md b/docs/design.md index 27d2609..21fe585 100644 --- a/docs/design.md +++ b/docs/design.md @@ -148,7 +148,7 @@ Order of play the bands suggest: Button Isle → Southwold → Westmarch or Jung **Unique drops.** Only Pokémon live on the island, so a mob's drop needs another source. `tools/drop_audit.py` checks every item a Pokémon or a vanilla mob drops that a recipe or brewing consumes, and writes `docs/pokemon-drops.md` (item, wild Pokémon with zone and chance, other sources, consumers). Gaps it found, filled through Cobblemon species additions (`datapacks/core/data/cobblesurvival/species_additions/`, placeholders): Blaze Rod (Carkol in Westmarch, Torkoal in the Basalt Furnace; needed for brewing stands, blaze powder and Eyes of Ender), Ghast Tear (Drifloon, Dusclops), Breeze Rod (Whimsicott), Shulker Shell (Shuckle, Cloyster), Nether Star (Charizard). The tide armour trim and the wither skeleton skull stay without a source (cosmetic here). A zone spawn with its own drops overrides the species' drops, so the audit also fails if an addition targets such a spawn (Magmortar's and Centiskorch's did). Self-test: `drop_additions_apply`. -**Base jobs and power.** [Cobblebase](https://modrinth.com/project/cobblebase) (Pokémon in a pasture work: harvesting, producing, finding, buffs, ...) and [Cobblemon Poke Power](https://modrinth.com/project/lIn7oRFm) (Electric-type Pokémon in a Power Block make Forge Energy; nothing in the pack uses the energy yet, Phase 6 machines). Phase 4 first gated them with flags: a skill or the Power Block opened once its owner had beaten a zone's Alpha guardian. **Replaced by soft gates on 2026-10-31** (Adrian: guardians must not set flags; see "Soft gates" under Phase 5). The companion mod still puts every Cobblebase skill executor behind the gate, by reflection, when the server starts (`progress/CobblebaseGate.kt`); a skill missing from the table stays off, so an update can't add ungated skills. Cobbreeding is not installed, so Breeder and Egg Hatcher do nothing. +**Base jobs and power.** [Cobblebase](https://modrinth.com/project/cobblebase) (Pokémon in a pasture work: harvesting, producing, finding, buffs, ...) and [Cobblemon Poke Power](https://modrinth.com/project/lIn7oRFm) (Electric-type Pokémon in a Power Block make Forge Energy; nothing in the pack uses the energy yet, Phase 6 machines). Phase 4 first gated them with flags: a skill or the Power Block opened once its owner had beaten a zone's Alpha guardian. **Replaced by soft gates on 2026-10-31** (Adrian: guardians must not set flags; see "Soft gates" under Phase 5). The companion mod still puts every Cobblebase skill executor behind the gate, by reflection, when the server starts (`progress/CobblebaseGate.kt`); a skill missing from the table stays off, so an update can't add ungated skills. Breeder and Egg Hatcher work since Cobbreeding came in (Phase 5, "Breeding"). **Catching high-level Pokémon** (Adrian, 2026-10-30). Measured against the trainer's strongest party Pokémon (`catching/Catching.kt`, values in `datapacks/core/data/cobblesurvival/catching/rules.json`): @@ -204,6 +204,15 @@ Self-tests: `gates_cover_every_cobblebase_skill`, `cobblebase_skills_need_level_ Self-tests: `gated_jobs_are_taken_back`, `base_jobs_dig_their_zones_ores`. +**Breeding** (Adrian, 2026-10-31: add Cobbreeding without a gate; only Ditto should be rare, in an endgame area or gated behind something else). Cobblebase's hatchery only takes [Cobbreeding](https://modrinth.com/mod/cobbreeding)'s eggs, so its Breeder and Egg Hatcher did nothing before. + +- Cobbreeding 2.4.1: two compatible Pokémon (or one and a Ditto) in a Pasture Block, breeding switched on with the pasture's button, lay an egg in its nest now and then (every 8,000–14,000 ticks); an egg hatches in a player's inventory (about 10 minutes for 20 egg cycles). Defaults kept: two Dittos make no random eggs, Masuda shiny boost ×4, hoppers may pull eggs out of a pasture. Open from the start, no gate; Cobblebase's Breeder and Egg Hatcher stay behind the Deep Dark gate. Cloth Config comes along for its client config screen. +- **Ditto** moved from Button Isle to the Deep Dark (Lv 45–60), with both of Cobblemon's entries: rare bucket in slime chunks, ultra-rare elsewhere. `world/zones.json` → `species` takes the long form `{"zone": "deep_dark", "all_entries": true}` for it, because when no stage fits a band `tools/zone_spawns.py` keeps one entry, which was the slime-chunk one. The Deep Dark's line target went up by one and Button Isle's down by one, so no other line moved; the Southwold and Frostpeak guardians are pinned (Pinsir, Tyranitar), since they tie on base stats with Hawlucha and Archaludon and the tool's pick between ties changed from run to run. +- HMs' visiting **Trainer Ditto** is made uncatchable (`world/SpecialPokemon.kt`); it still trades TMs. +- Cobbreeding lists a mixin into Cobblemon's client-only pasture screen among its common ones, so a dedicated server logs one harmless "invalid dist" error at start; allowlisted in `tools/smoke_allowlist.txt`. `tools/check_deps.py` now skips dependencies meant for the other side (Cobbreeding needs Cloth Config on clients only). + +Self-tests: `cobbreeding_eggs_fit_the_hatchery` (Cobblebase's bridge takes Cobbreeding's eggs), `trainer_ditto_cant_be_caught`. + ## Notes for Phase 6 (researched 2026-10-31) Adrian asked for existing mods for the multiblocks, or at least their logic. Phase 6's machines need a pattern-checked structure with a controller, a Pokémon stored inside (keeping its data, visible), safe return when the structure breaks, and state that survives unload and restart. Power is already Poke Power's (Phase 4), so the first machine is the Large Furnace. Nothing on NeoForge 1.21.1 puts a Pokémon into a multiblock; the structure part has several options: diff --git a/docs/licenses.md b/docs/licenses.md index f3fc654..db1863a 100644 --- a/docs/licenses.md +++ b/docs/licenses.md @@ -43,6 +43,8 @@ Read each license before adding a mod, and add a row here in the same change. | Cobblebase | notlown | MPL-2.0 | [Modrinth](https://modrinth.com/project/cobblebase) | Phase 4: Pokémon jobs at the base (both). The 1.8 build is Modrinth only | | Cobblemon Poke Power | Matthiesen-dev (Adammatthiesen) | MIT | [Modrinth](https://modrinth.com/project/lIn7oRFm) | Phase 4: Pokémon-powered Forge Energy (both). Not on CurseForge | | Cobblemon HMs | shiero | shiero's Mods License (in the jar): free to use, also in free modpacks; any monetized use (paid pack, paid server perks) needs an agreement with the author | [Modrinth](https://modrinth.com/project/cobblemon-hms) | Phase 5: field moves (both). Also on [CurseForge](https://www.curseforge.com/projects/1587850), not linked in the pack yet | +| Cobbreeding | Ludichat, Fuzuki | "MIT, no AI, no reupload" ([text](https://fuzuki.dev/~fuzuki/mit-no-ai-no-reupload.txt)): MIT terms, but the mod may not be used to train or improve machine-learning models, and may not be reuploaded unchanged to CurseForge or Modrinth except bundled in a modpack | [Modrinth](https://modrinth.com/mod/cobbreeding), [source](https://gitlab.com/ludichat/Cobbreeding) | Phase 5: breeding (both). Referenced only, never copied. Not on CurseForge by the authors' choice | +| Cloth Config API | shedaniel | LGPL-3.0-only | [Modrinth](https://modrinth.com/mod/cloth-config) | Cobbreeding dependency (client) | | Regenerating Ore Veins | Stargazer | MIT (jar metadata) | [CurseForge](https://www.curseforge.com/minecraft/mc-mods/regenerating-ore-veins) | Phase 4: the only ore source besides chest loot (both). Not on Modrinth | | WorldGen Editor | waterflane | CC BY-NC-SA 4.0 | [Modrinth](https://modrinth.com/mod/worldgen-editor) / [CurseForge](https://www.curseforge.com/minecraft/mc-mods/worldgen-editor), [source](https://github.com/waterflane/WorldGen-Editor) | Non-commercial, share-alike. See the derived files below | diff --git a/docs/zone-species.md b/docs/zone-species.md index fd7577e..a56c055 100644 --- a/docs/zone-species.md +++ b/docs/zone-species.md @@ -2,9 +2,9 @@ Generated by `tools/zone_spawns.py` from Cobblemon's spawn data and `world/zones.json`; do not edit by hand. Each evolution line spawns in one zone; the stages listed are the ones whose levels reach the zone's band. Pin a line to another zone in `world/zones.json` → `species`. -## Button Isle (Lv 2–10, 26 lines) +## Button Isle (Lv 2–10, 25 lines) -Bidoof, Bonsly, Caterpie (Butterfree, Caterpie, Metapod), Combee, Ditto, Gimmighoul, Hoothoot, Kricketot (Kricketot, Kricketune), Ledyba, Meditite, Meowth, Morpeko, Nincada, Pachirisu, Rattata, Sableye, Scatterbug (Scatterbug, Spewpa), Skitty, Smeargle, Spinarak, Sunkern, Tarountula, Weedle (Beedrill, Kakuna, Weedle), Wooper, Wurmple (Beautifly, Cascoon, Dustox, Silcoon, Wurmple), Wynaut +Bidoof, Bonsly, Caterpie (Butterfree, Caterpie, Metapod), Combee, Gimmighoul, Hoothoot, Kricketot (Kricketot, Kricketune), Ledyba, Meditite, Meowth, Morpeko, Nincada, Pachirisu, Rattata, Sableye, Scatterbug (Scatterbug, Spewpa), Skitty, Smeargle, Spinarak, Sunkern, Tarountula, Weedle (Beedrill, Kakuna, Weedle), Wooper, Wurmple (Beautifly, Cascoon, Dustox, Silcoon, Wurmple), Wynaut ## Frostpeak Range (Lv 40–55, 43 lines) @@ -50,9 +50,9 @@ Amaura, Anorith, Archen, Barboach, Carnivine, Cranidos, Cutiefly (Cutiefly, Ribo Bagon (Bagon, Shelgon), Baltoy (Baltoy, Claydol), Beldum (Beldum, Metang), Cleffa (Clefable, Clefairy, Cleffa), Comfey, Drowzee (Drowzee, Hypno), Druddigon, Elgyem, Espurr (Espurr, Meowstic), Fidough (Dachsbun, Fidough), Geodude (Geodude, Golem, Graveler), Indeedee, Klefki, Lunatone, Milcery (Alcremie, Milcery), Munna (Munna, Musharna), Orthworm, Sigilyph, Solosis (Duosion, Solosis), Solrock, Swirlix (Slurpuff, Swirlix) -## Deep Dark (Lv 45–60, 21 lines) +## Deep Dark (Lv 45–60, 22 lines) -Bronzor (Bronzong), Carbink, Deino (Zweilous), Duskull (Dusclops, Dusknoir), Feebas (Milotic), Glimmet (Glimmora), Greavard (Houndstone), Honedge (Aegislash, Doublade), Klink (Klinklang), Maschiff (Mabosstiff), Mime Jr. (Mr. Mime, Mr. Rime), Mimikyu, Murkrow (Honchkrow), Nosepass (Probopass), Onix (Steelix), Ralts (Gallade, Gardevoir), Roggenrola (Gigalith), Rolycoly (Coalossal), Rookidee (Corviknight), Varoom (Revavroom), Zubat (Crobat, Golbat) +Bronzor (Bronzong), Carbink, Deino (Zweilous), Ditto, Duskull (Dusclops, Dusknoir), Feebas (Milotic), Glimmet (Glimmora), Greavard (Houndstone), Honedge (Aegislash, Doublade), Klink (Klinklang), Maschiff (Mabosstiff), Mime Jr. (Mr. Mime, Mr. Rime), Mimikyu, Murkrow (Honchkrow), Nosepass (Probopass), Onix (Steelix), Ralts (Gallade, Gardevoir), Roggenrola (Gigalith), Rolycoly (Coalossal), Rookidee (Corviknight), Varoom (Revavroom), Zubat (Crobat, Golbat) ## Basalt Furnace (Lv 55–70, 16 lines) diff --git a/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt b/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt index 6ca22b5..4154780 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/CobbleSurvival.kt @@ -14,6 +14,7 @@ import dev.cobblesurvival.progress.Gates import dev.cobblesurvival.selftest.SelfTest import dev.cobblesurvival.world.OnlyPokemon import dev.cobblesurvival.world.SharedLoot +import dev.cobblesurvival.world.SpecialPokemon import dev.cobblesurvival.zone.Guardians import dev.cobblesurvival.zone.InZoneCondition import dev.cobblesurvival.zone.LevelPressure @@ -77,6 +78,7 @@ class CobbleSurvival { Catching.register() Wanderers.register() SharedLoot.register() + SpecialPokemon.register() ZoneHazards.register() LevelPressure.register() SelfTest.register() diff --git a/mod/src/main/kotlin/dev/cobblesurvival/selftest/BreedingTests.kt b/mod/src/main/kotlin/dev/cobblesurvival/selftest/BreedingTests.kt new file mode 100644 index 0000000..42db6a5 --- /dev/null +++ b/mod/src/main/kotlin/dev/cobblesurvival/selftest/BreedingTests.kt @@ -0,0 +1,47 @@ +package dev.cobblesurvival.selftest + +import dev.cobblesurvival.world.SpecialPokemon +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items + +/** + * Breeding (Adrian, 2026-10-31): Cobbreeding is in, open from the start, and its eggs are what Cobblebase's hatchery + * takes (Cobblebase finds them through its own bridge, by reflection). Ditto is a rare Deep Dark find, so HMs' visiting + * Trainer Ditto can't be caught. + */ +object BreedingTests { + private val EGG = ResourceLocation.parse("cobbreeding:pokemon_egg") + private const val BRIDGE = "notlown.cobblebase.core.CobbreedingBridge" + + fun all() = listOf( + SelfTestCase("cobbreeding_eggs_fit_the_hatchery", 20, ::eggsFitTheHatchery), + SelfTestCase("trainer_ditto_cant_be_caught", 40, ::trainerDittoCantBeCaught), + ) + + private fun eggsFitTheHatchery(s: Scenario) { + val egg = BuiltInRegistries.ITEM.get(EGG) + s.check(egg != Items.AIR) { "Cobbreeding isn't installed: no $EGG" } + val type = Class.forName(BRIDGE) + val bridge = type.getField("INSTANCE").get(null) + s.check(type.getMethod("isLoaded").invoke(bridge) as Boolean) { "Cobblebase doesn't see Cobbreeding" } + s.check(type.getMethod("isPokemonEgg", ItemStack::class.java).invoke(bridge, ItemStack(egg)) as Boolean) { + "Cobblebase's hatchery doesn't take Cobbreeding's eggs" + } + s.helper.succeed() + } + + private fun trainerDittoCantBeCaught(s: Scenario) { + s.player() + val pokemon = s.pokemon("ditto", 20) + pokemon.persistentData.putBoolean("dittohms_trainer", true) + val ordinary = s.pokemon("ditto", 20) + s.wild(pokemon, s.at(4.5, 1.0, 4.5)) + s.wild(ordinary, s.at(10.5, 1.0, 10.5)) + s.helper.startSequence() + .thenWaitUntil { s.check(pokemon.isUncatchable()) { "HMs' Trainer Ditto can be caught" } } + .thenExecute { s.check(!ordinary.isUncatchable() && !SpecialPokemon.isTrainerDitto(ordinary)) { "an ordinary wild Ditto became uncatchable" } } + .thenSucceed() + } +} diff --git a/mod/src/main/kotlin/dev/cobblesurvival/selftest/SelfTest.kt b/mod/src/main/kotlin/dev/cobblesurvival/selftest/SelfTest.kt index 476c64f..3761f84 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/selftest/SelfTest.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/selftest/SelfTest.kt @@ -54,7 +54,7 @@ object SelfTest { private val cases: List get() = PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all() + GuardianTests.all() + VeinTests.all() + OreNodeTests.all() + DropTests.all() + ProgressionTests.all() + - CatchingTests.all() + WorldRulesTests.all() + FieldMoveTests.all() + ShoulderTests.all() + BaseLootTests.all() + CatchingTests.all() + WorldRulesTests.all() + FieldMoveTests.all() + ShoulderTests.all() + BaseLootTests.all() + BreedingTests.all() private var running: List = emptyList() private var forced: List = emptyList() diff --git a/mod/src/main/kotlin/dev/cobblesurvival/world/SpecialPokemon.kt b/mod/src/main/kotlin/dev/cobblesurvival/world/SpecialPokemon.kt index 1349c92..04d2ca4 100644 --- a/mod/src/main/kotlin/dev/cobblesurvival/world/SpecialPokemon.kt +++ b/mod/src/main/kotlin/dev/cobblesurvival/world/SpecialPokemon.kt @@ -1,17 +1,43 @@ package dev.cobblesurvival.world +import com.cobblemon.mod.common.entity.pokemon.PokemonEntity import com.cobblemon.mod.common.pokemon.Pokemon +import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty import dev.cobblesurvival.zone.Guardians +import net.minecraft.server.TickTask +import net.minecraft.server.level.ServerLevel +import net.neoforged.neoforge.common.NeoForge +import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent /** * Wild Pokémon that the island's rules for wild Pokémon (strays despawning, hunting weaker trainers) leave alone: * guardians, anything uncatchable, and other mods' characters that are Pokémon underneath, such as Cobblemon HMs' * visiting Trainer Ditto, which sells TMs. + * + * The Trainer Ditto is also made uncatchable (Adrian, 2026-10-31: Ditto, which breeds with anything, is a rare find in + * the Deep Dark; a Ditto visiting every half hour would hand them out early). It still trades. */ object SpecialPokemon { /** Cobblemon HMs marks its Trainer Ditto with this in the Pokémon's persistent data. */ private const val HMS_TRAINER = "dittohms_trainer" + fun register() { + NeoForge.EVENT_BUS.addListener(::onEntityJoin) + } + fun isExempt(pokemon: Pokemon): Boolean = - pokemon.isUncatchable() || pokemon.persistentData.contains(Guardians.TAG) || pokemon.persistentData.contains(HMS_TRAINER) + pokemon.isUncatchable() || pokemon.persistentData.contains(Guardians.TAG) || isTrainerDitto(pokemon) + + fun isTrainerDitto(pokemon: Pokemon): Boolean = pokemon.persistentData.contains(HMS_TRAINER) + + /** Checked a tick later, since HMs may mark its trader after the entity joins. */ + private fun onEntityJoin(event: EntityJoinLevelEvent) { + val entity = event.entity as? PokemonEntity ?: return + val level = event.level as? ServerLevel ?: return + level.server.tell(TickTask(level.server.tickCount + 1) { keepTrainerDittoFree(entity.pokemon) }) + } + + fun keepTrainerDittoFree(pokemon: Pokemon) { + if (isTrainerDitto(pokemon) && !pokemon.isUncatchable()) UncatchableProperty.uncatchable().apply(pokemon) + } } diff --git a/pack/index.toml b/pack/index.toml index f358c5f..0bb31e8 100644 --- a/pack/index.toml +++ b/pack/index.toml @@ -111,6 +111,11 @@ file = "mods/balm.pw.toml" hash = "603cdc47142b5aa5729cbbf265a473574c9e2b55561de4318e9842d3d4b3b995" metafile = true +[[files]] +file = "mods/cloth-config.pw.toml" +hash = "66b2e57fad1d958893206d82ff878159b7fdfef64ccb74fe1232b71012409953" +metafile = true + [[files]] file = "mods/cobblebase.pw.toml" hash = "586a0c3388e78fea91123f6c88907cc2cc2c5f2b87a15d7c050787d2c8c78cee" @@ -146,6 +151,11 @@ file = "mods/cobblemon_tp_pastures.pw.toml" hash = "2cb849c31c6c51e394b254850243112c6a1a93a3b3534de1ebb9add05a90dd2d" metafile = true +[[files]] +file = "mods/cobbreeding.pw.toml" +hash = "aa8f86c0deec2e5ff6a20360be15748c5281faaa0acf45f15044eff42f182ad9" +metafile = true + [[files]] file = "mods/cold-sweat.pw.toml" hash = "96b2b071c1669e5f71237adc77d9474d0dc5544509cebc0b242478e40915a4d4" diff --git a/pack/mods/cloth-config.pw.toml b/pack/mods/cloth-config.pw.toml new file mode 100644 index 0000000..61281c4 --- /dev/null +++ b/pack/mods/cloth-config.pw.toml @@ -0,0 +1,13 @@ +name = "Cloth Config API" +filename = "cloth-config-15.0.140-neoforge.jar" +side = "client" + +[download] +url = "https://cdn.modrinth.com/data/9s6osm5g/versions/izKINKFg/cloth-config-15.0.140-neoforge.jar" +hash-format = "sha512" +hash = "aaf9b010955b8cd294e5a92f069985b18729fd5e2cf22d351f1dff9680f15488688803ec41e77e941cbde130ceb535014ca4c868047d80ab69c2d508e216654d" + +[update] +[update.modrinth] +mod-id = "9s6osm5g" +version = "izKINKFg" diff --git a/pack/mods/cobbreeding.pw.toml b/pack/mods/cobbreeding.pw.toml new file mode 100644 index 0000000..b0c2bae --- /dev/null +++ b/pack/mods/cobbreeding.pw.toml @@ -0,0 +1,13 @@ +name = "Cobbreeding" +filename = "Cobbreeding-neoforge-2.4.1.jar" +side = "both" + +[download] +url = "https://cdn.modrinth.com/data/ItmVb4zY/versions/jzxflH2c/Cobbreeding-neoforge-2.4.1.jar" +hash-format = "sha512" +hash = "1c986d7d7ce331bd9a1d4a3f4b1da6cee8e420e294189c4ad876a38884f4229b2a3dd8b3a4a72e9873fa50ac1980f60255d78936e15c0bb833d3177a431b749c" + +[update] +[update.modrinth] +mod-id = "ItmVb4zY" +version = "jzxflH2c" diff --git a/pack/pack.toml b/pack/pack.toml index cd1d798..b9678ba 100644 --- a/pack/pack.toml +++ b/pack/pack.toml @@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0" [index] file = "index.toml" hash-format = "sha256" -hash = "d8359ad9c1f98c8c72503f0ce16773d7721a1a4095cfdef15ca3e985736ae6ff" +hash = "c890308691806215f70da2c23f27010a9533c24e645a1e7cc127410e4c456453" [versions] minecraft = "1.21.1" diff --git a/tools/check_deps.py b/tools/check_deps.py index d25538d..e3f4057 100644 --- a/tools/check_deps.py +++ b/tools/check_deps.py @@ -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 [--neoforge VER] [--minecraft VER] +Usage: tools/check_deps.py [--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") diff --git a/tools/smoke_allowlist.txt b/tools/smoke_allowlist.txt index 5b22b5d..538906c 100644 --- a/tools/smoke_allowlist.txt +++ b/tools/smoke_allowlist.txt @@ -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 diff --git a/tools/zone_spawns.py b/tools/zone_spawns.py index 14d9f10..2f1319c 100755 --- a/tools/zone_spawns.py +++ b/tools/zone_spawns.py @@ -23,7 +23,9 @@ datapacks/core/data/cobblesurvival/spawn_pool_world/.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) diff --git a/world/zones.json b/world/zones.json index 994bba7..c1ec033 100644 --- a/world/zones.json +++ b/world/zones.json @@ -29,14 +29,15 @@ "dawn_stone": {"zone": "frostpeak", "stone": "cobblemon:dawn_stone_ore", "deepslate": "cobblemon:deepslate_dawn_stone_ore", "regen_minutes": 75, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 10, "attempts": 2}}, "dusk_stone": {"zone": "deep_dark", "deepslate": "cobblemon:deepslate_dusk_stone_ore", "regen_minutes": 75, "nodes": {"chance": 0.1, "tries": 8, "radius": [0, 1], "y": [-64, -24]}} }, - "species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt"}, + "species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt", + "ditto": {"zone": "deep_dark", "all_entries": true}}, "zones": [ { - "id": "button_isle", "guardian": {"drops": [["cobblemon:poke_ball", 10], ["cobblemon:potion", 4]]}, "lines": 20, "name": "Button Isle", "levels": [2, 10], "hazard": "none", "color": "#a6d96a", + "id": "button_isle", "guardian": {"drops": [["cobblemon:poke_ball", 10], ["cobblemon:potion", 4]]}, "lines": 19, "name": "Button Isle", "levels": [2, 10], "hazard": "none", "color": "#a6d96a", "rule": {"within": 300} }, { - "id": "frostpeak", "guardian": {"drops": [["cobblemon:icy_rock", 3], ["cobblemon:exp_candy_l", 2]]}, "lines": 30, "name": "Frostpeak Range", "levels": [40, 55], "hazard": "cold (Cold Sweat): needs wool insulation", "color": "#f2f7ff", + "id": "frostpeak", "guardian": {"species": "tyranitar", "drops": [["cobblemon:icy_rock", 3], ["cobblemon:exp_candy_l", 2]]}, "lines": 30, "name": "Frostpeak Range", "levels": [40, 55], "hazard": "cold (Cold Sweat): needs wool insulation", "color": "#f2f7ff", "rule": {"biomes": ["minecraft:jagged_peaks"], "absorb": {"minecraft:windswept_hills": 96, "minecraft:cherry_grove": 144}} }, { @@ -60,7 +61,7 @@ "rule": {"region": {"z_max": 0, "x_max": -250}} }, { - "id": "southwold", "guardian": {"drops": [["cobblemon:great_ball", 6], ["cobblemon:exp_candy_s", 4]]}, "lines": 35, "name": "Southwold", "levels": [8, 18], "hazard": "none", "color": "#7cc576", + "id": "southwold", "guardian": {"species": "pinsir", "drops": [["cobblemon:great_ball", 6], ["cobblemon:exp_candy_s", 4]]}, "lines": 35, "name": "Southwold", "levels": [8, 18], "hazard": "none", "color": "#7cc576", "rule": {"region": {"z_min": 0}} }, { @@ -75,7 +76,7 @@ "caves": [ {"id": "lush_caves", "types": ["grass", "bug", "fairy", "poison", "water"], "lines": 20, "name": "Lush Caves", "biome": "minecraft:lush_caves", "levels": [12, 28], "hazard": "none"}, {"id": "dripstone", "types": ["rock", "ground", "steel", "fairy", "psychic", "dragon"], "lines": 20, "name": "Dripstone Depths", "biome": "minecraft:dripstone_caves", "levels": [22, 38], "hazard": "none"}, - {"id": "deep_dark", "types": ["ghost", "dark", "steel", "poison", "psychic", "rock"], "guardian": {"drops": [["cobblemon:icy_rock", 3], ["cobblemon:exp_candy_l", 2]]}, "lines": 20, "name": "Deep Dark", "biome": "minecraft:deep_dark", "levels": [45, 60], "hazard": "darkness (vanilla sculk; no Warden)"}, + {"id": "deep_dark", "types": ["ghost", "dark", "steel", "poison", "psychic", "rock"], "guardian": {"drops": [["cobblemon:icy_rock", 3], ["cobblemon:exp_candy_l", 2]]}, "lines": 21, "name": "Deep Dark", "biome": "minecraft:deep_dark", "levels": [45, 60], "hazard": "darkness (vanilla sculk; no Warden)"}, {"id": "basalt", "types": ["fire", "rock", "ground", "steel", "dark"], "lines": 15, "name": "Basalt Furnace", "biome": "minecraft:basalt_deltas", "levels": [55, 70], "hazard": "heat (Cold Sweat)"} ] }