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:
claudeandClaude Opus 5.5 committed 2026-10-31 19:13:55 +01:00
1 parent 5b304025c6
commit 5cae9252c3
20 files changed
+274 -148

No files matched your search

@@ -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()
@@ -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()
}
}
@@ -54,7 +54,7 @@ object SelfTest {
private val cases: List<SelfTestCase> 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<Scenario> = emptyList()
private var forced: List<ChunkPos> = emptyList()
@@ -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)
}
}