Phase 9: crafted summons for the legendaries

Adrian, 2026-11-09: legendaries, mythicals, Ultra Beasts and Paradox Pokemon
come from crafted summons. 22 Sigils (Naganadel evolves from Poipole), one
item told apart by custom data, from world/summons.json through
tools/summons.py: late-game recipes in three tiers (Lv 70, 80, 90), each used
in a fitting zone. A summoned legend fights as a boss (area attacks by level
and type, arena 32, the Guardian's Ward) but can be caught; on capture it
loses every boss mark. One at a time within 96 blocks.

Quest book: a Legends chapter, one quest per Sigil (97 quests). JEI lists each
Sigil as its own item, and Island Drops says where a legend is summoned; drops
Cobblemon names but doesn't have (Ho-Oh's Sacred Ash) get no slot.

Self-tests every_implemented_pokemon_is_obtainable (spawn, fossil or Sigil for
every implemented species) and sigil_summons_a_catchable_legend. Self-tests
and the island check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-11-09 13:03:50 +01:00
1 parent e206664b3d
commit f7ea371862
92 files changed
+4081 -21

No files matched your search

@@ -22,6 +22,7 @@ import dev.cobblesurvival.world.OnlyPokemon
import dev.cobblesurvival.world.SharedLoot
import dev.cobblesurvival.world.SpecialPokemon
import dev.cobblesurvival.progress.QuestEvents
import dev.cobblesurvival.summon.Summons
import dev.cobblesurvival.zone.GuardianBars
import dev.cobblesurvival.zone.Guardians
import dev.cobblesurvival.zone.InZoneCondition
@@ -70,6 +71,7 @@ class CobbleSurvival {
MOD_BUS.addListener(::onAddPackFinders)
InZoneCondition.register(MOD_BUS)
Machines.register(MOD_BUS)
Summons.register(MOD_BUS)
NeoForge.EVENT_BUS.addListener(::onRegisterCommands)
NeoForge.EVENT_BUS.addListener(::onServerStarted)
NeoForge.EVENT_BUS.addListener(::onPortalSpawn)
@@ -17,7 +17,9 @@ object DropSources {
class Reward(val item: String, val count: Int)
class Spawned(val zone: String, val levels: String)
class LineMember(val zone: String, val species: String)
class Pokemon(val species: String, val drops: List<Drop>, val zones: List<Spawned>, val line: List<LineMember>?)
class SummonedAt(val zone: String, val level: Int)
class Summon(val species: String, val name: String, val zone: String, val level: Int)
class Pokemon(val species: String, val drops: List<Drop>, val zones: List<Spawned>, val line: List<LineMember>?, val summon: SummonedAt?)
class SpawnDrops(val species: String, val zone: String, val drops: List<Drop>)
class Guardian(val zone: String, val species: String, val level: Int, val drops: List<Reward>, val respawn_hours: Double)
class EndBoss(val species: String, val level: Int, val xp_first_win: Int, val xp: Int, val respawn_hours: Double, val first_win: List<Reward>)
@@ -33,6 +35,7 @@ object DropSources {
class Data(
val zones: Map<String, Zone>,
val pokemon: List<Pokemon>,
val summons: List<Summon>,
val spawn_drops: List<SpawnDrops>,
val guardians: List<Guardian>,
val end_boss: EndBoss,
@@ -56,6 +59,9 @@ object DropSources {
return ItemStack(found, count.coerceIn(1, 99))
}
/** Cobblemon's own tables can name an item it doesn't have (Ho-Oh's Sacred Ash in 1.8.4): it never drops, so it gets no slot. */
fun exists(item: String): Boolean = BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(item))
/** The most a drop gives: "0-2" → 2. */
fun most(count: String?): Int = count?.substringAfter('-')?.toIntOrNull() ?: 1
}
@@ -4,25 +4,34 @@ import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.item.PokemonItem
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.summon.Summons
import mezz.jei.api.IModPlugin
import mezz.jei.api.JeiPlugin
import mezz.jei.api.gui.builder.IRecipeLayoutBuilder
import mezz.jei.api.gui.drawable.IDrawable
import mezz.jei.api.gui.ingredient.IRecipeSlotsView
import mezz.jei.api.ingredients.subtypes.ISubtypeInterpreter
import mezz.jei.api.ingredients.subtypes.UidContext
import mezz.jei.api.recipe.IFocusGroup
import mezz.jei.api.recipe.RecipeIngredientRole
import mezz.jei.api.recipe.RecipeType
import mezz.jei.api.recipe.category.IRecipeCategory
import mezz.jei.api.registration.IRecipeCatalystRegistration
import mezz.jei.api.registration.IRecipeCategoryRegistration
import mezz.jei.api.registration.IExtraIngredientRegistration
import mezz.jei.api.registration.IRecipeRegistration
import mezz.jei.api.registration.ISubtypeRegistration
import net.minecraft.ChatFormatting
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.Font
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.core.component.DataComponents
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.component.CustomData
import net.minecraft.world.item.component.ItemLore
/**
* The pack's own JEI pages (roadmap Phase 8; Adrian, 2026-11-02: every item that can drop gets a page naming where it
@@ -58,6 +67,19 @@ class DropSourcesJei : IModPlugin {
override fun getPluginUid(): ResourceLocation = ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "drop_sources")
/** Each Sigil counts as an item of its own, by the species in its custom data. */
override fun registerItemSubtypes(registration: ISubtypeRegistration) {
registration.registerSubtypeInterpreter(Summons.SIGIL.get(), object : ISubtypeInterpreter<ItemStack> {
override fun getSubtypeData(stack: ItemStack, context: UidContext): Any? = Summons.speciesOf(stack)
override fun getLegacyStringSubtypeInfo(stack: ItemStack, context: UidContext): String = Summons.speciesOf(stack).orEmpty()
})
}
/** The Sigils aren't in a creative tab (they need a species), so JEI wouldn't list them otherwise. */
override fun registerExtraIngredients(registration: IExtraIngredientRegistration) {
registration.addExtraItemStacks(sigils())
}
override fun registerCategories(registration: IRecipeCategoryRegistration) {
val gui = registration.jeiHelpers.guiHelper
registration.addRecipeCategories(*kinds.map { kind ->
@@ -177,18 +199,31 @@ class DropSourcesJei : IModPlugin {
private fun zoneRow(zone: String, levels: String) = Row(text(DropSources.zoneName(zone)), text("Lv ${levels.replace("-", "–")}"))
/** Each Sigil as its recipe makes it: the species, its name and where to use it. */
fun sigils(): List<ItemStack> = DropSources.data.summons.map { s ->
ItemStack(Summons.SIGIL.get()).apply {
val data = CompoundTag().apply { putString(Summons.KEY, s.species.substringAfter(':')) }
set(DataComponents.CUSTOM_DATA, CustomData.of(data))
set(DataComponents.ITEM_NAME, Component.literal("${s.name} Sigil"))
set(DataComponents.LORE, ItemLore(listOf(Component.literal("Use in ${DropSources.zoneName(s.zone)} to summon a Lv ${s.level} ${s.name}")
.withStyle { it.withColor(ChatFormatting.GRAY).withItalic(false) })))
}
}
fun pokemonPages(): List<Page> {
val data = DropSources.data
val wild = data.pokemon.map { p ->
val rows = if (p.zones.isNotEmpty()) {
p.zones.map { zoneRow(it.zone, it.levels) }
} else if (p.summon != null) {
listOf(Row(muted("Not wild. A Sigil summons it in:")), Row(text(DropSources.zoneName(p.summon.zone)), text("Lv ${p.summon.level}")))
} else {
listOf(Row(muted("Not wild. Its line lives in:"))) + p.line.orEmpty().map { Row(text(DropSources.zoneName(it.zone)), speciesName(it.species)) }
}
Page(picture(p.species), speciesName(p.species), "Drops", rows, p.drops.map(::dropOutput))
Page(picture(p.species), speciesName(p.species), "Drops", rows, p.drops.filter { DropSources.exists(it.item) }.map(::dropOutput))
}
val special = data.spawn_drops.map { s ->
Page(picture(s.species), speciesName(s.species), "Drops here", listOf(Row(text(DropSources.zoneName(s.zone)), text("instead"))), s.drops.map(::dropOutput))
Page(picture(s.species), speciesName(s.species), "Drops here", listOf(Row(text(DropSources.zoneName(s.zone)), text("instead"))), s.drops.filter { DropSources.exists(it.item) }.map(::dropOutput))
}
return wild + special
}
@@ -2,14 +2,22 @@ package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.api.drop.DropTable
import com.cobblemon.mod.common.api.drop.ItemDropEntry
import com.cobblemon.mod.common.api.fossil.Fossils
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Species
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.boss.BossDefs
import dev.cobblesurvival.boss.BossFights
import dev.cobblesurvival.boss.BossToughness
import dev.cobblesurvival.jei.DropSources
import dev.cobblesurvival.jei.DropSourcesJei
import dev.cobblesurvival.machine.MachineDefs
import dev.cobblesurvival.progress.QuestEvents
import dev.cobblesurvival.summon.Summons
import dev.cobblesurvival.world.SpecialPokemon
import dev.cobblesurvival.zone.Guardians
import dev.cobblesurvival.zone.Zones
import java.util.function.Consumer
@@ -32,8 +40,75 @@ object QuestTests {
SelfTestCase("quest_events_mark_zones", 200, ::zoneEvent),
SelfTestCase("drop_sources_match_the_server", 20, ::dropSources),
SelfTestCase("jei_pages_build", 20, ::jeiPages),
SelfTestCase("every_implemented_pokemon_is_obtainable", 20, ::obtainable),
SelfTestCase("sigil_summons_a_catchable_legend", 200, ::summon),
)
private fun rootOf(species: Species): Species {
var current = species
val seen = HashSet<Species>()
while (seen.add(current)) current = current.preEvolution?.species ?: return current
return current
}
/**
* Roadmap Phase 9 (Adrian, 2026-11-09): every species Cobblemon has a model for can be had without commands: its line
* spawns somewhere, comes from a fossil, or has a Sigil. A new Cobblemon version adding species fails this until
* they have a home.
*/
private fun obtainable(s: Scenario) {
val wild = CobblemonSpawnPools.WORLD_SPAWN_POOL.details.filterIsInstance<PokemonSpawnDetail>().mapNotNull { it.pokemon.species }
val fossils = Fossils.all().mapNotNull { it.result.species }
val summons = Summons.defs.keys
val reachable = (wild + fossils + summons).mapNotNull { PokemonSpecies.getByName(it.substringAfter(':')) }.map(::rootOf).toSet()
val missing = PokemonSpecies.implemented.filter { rootOf(it) !in reachable }
s.check(missing.isEmpty()) { "${missing.size} implemented Pokémon can't be obtained: ${missing.take(12).joinToString { it.name }}" }
s.helper.succeed()
}
/** A Sigil works only in its zone, summons a catchable legend that fights as a boss, and once caught it's ordinary. */
private fun summon(s: Scenario) {
val level = s.level
val player = s.player()
val sigil = DropSourcesJei.sigils().first { Summons.speciesOf(it) == "zapdos" }
val recipe = level.server.recipeManager.byKey(ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "summon_zapdos")).orElse(null)
?: throw GameTestAssertException("the Zapdos Sigil has no recipe")
val made = recipe.value.getResultItem(level.registryAccess())
s.check(Summons.speciesOf(made) == "zapdos" && made.hoverName.string == "Zapdos Sigil") { "the recipe makes '${made.hoverName.string}' for ${Summons.speciesOf(made)}" }
val missingRecipes = Summons.defs.keys.filter { level.server.recipeManager.byKey(ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "summon_$it")).isEmpty }
s.check(missingRecipes.isEmpty()) { "no recipe for the Sigils of $missingRecipes" }
val elsewhere = Zones.samples["button_isle"] ?: throw GameTestAssertException("no sample point for Button Isle")
val home = Zones.samples["westmarch"] ?: throw GameTestAssertException("no sample point for Westmarch")
fun ground(x: Int, z: Int) = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z).toDouble()
fun summoned() = level.getEntitiesOfClass(PokemonEntity::class.java, player.boundingBox.inflate(32.0)) { it.pokemon.persistentData.contains(Summons.TAG) }
s.afterTest { summoned().forEach { it.discard() } }
s.helper.startSequence()
.thenExecute {
level.getChunk(elsewhere.x shr 4, elsewhere.z shr 4)
s.moveTo(player, elsewhere.x + 0.5, ground(elsewhere.x, elsewhere.z), elsewhere.z + 0.5)
}
.thenWaitUntil { s.check(level.isPositionEntityTicking(player.blockPosition())) { "Button Isle isn't loaded yet" } }
.thenExecute {
s.check(!Summons.summon(level, player, sigil) && summoned().isEmpty()) { "the Sigil worked outside its zone" }
level.getChunk(home.x shr 4, home.z shr 4)
s.moveTo(player, home.x + 0.5, ground(home.x, home.z), home.z + 0.5)
}
.thenWaitUntil { s.check(level.isPositionEntityTicking(player.blockPosition())) { "Westmarch isn't loaded yet" } }
.thenExecute {
s.check(Summons.summon(level, player, sigil)) { "the Sigil didn't work in its zone" }
val legend = summoned().singleOrNull() ?: throw GameTestAssertException("${summoned().size} legends appeared")
val pokemon = legend.pokemon
s.check(pokemon.species.name == "Zapdos" && pokemon.level == Summons.defs.getValue("zapdos").level) { "summoned ${pokemon.species.name} Lv ${pokemon.level}" }
s.check(!pokemon.isUncatchable() && BossFights.defOf(legend) != null && BossToughness.isBoss(pokemon)) { "the legend isn't a catchable boss" }
s.check(SpecialPokemon.isExempt(pokemon)) { "the wild-Pokémon rules would remove the legend" }
s.check(!Summons.summon(level, player, sigil)) { "a second legend came while the first was here" }
Summons.released(pokemon)
s.check(!BossToughness.isBoss(pokemon) && !SpecialPokemon.isExempt(pokemon)) { "a caught legend still counts as a boss" }
legend.discard()
}
.thenSucceed()
}
/**
* Every JEI page builds (the same code JEI calls on the client), one per Pokémon, guardian, altar tier, vein zone
* and pasture list, and every item on them exists (an unknown id would show as a barrier).
@@ -0,0 +1,145 @@
package dev.cobblesurvival.summon
import com.cobblemon.mod.common.api.Priority
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import com.google.gson.Gson
import com.google.gson.JsonElement
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.boss.BossFights
import dev.cobblesurvival.zone.Zones
import net.minecraft.ChatFormatting
import net.minecraft.core.BlockPos
import net.minecraft.core.component.DataComponents
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.resources.ResourceManager
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.util.Mth
import net.minecraft.util.profiling.ProfilerFiller
import net.minecraft.world.InteractionHand
import net.minecraft.world.InteractionResultHolder
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Rarity
import net.minecraft.world.level.Level
import net.minecraft.world.phys.Vec3
import net.neoforged.bus.api.IEventBus
import net.neoforged.neoforge.common.NeoForge
import net.neoforged.neoforge.event.AddReloadListenerEvent
import net.neoforged.neoforge.registries.DeferredHolder
import net.neoforged.neoforge.registries.DeferredRegister
import java.util.function.Supplier
/**
* Crafted summons (Adrian, 2026-11-09): legendaries, mythicals, Ultra Beasts and Paradox Pokémon have no spawn data, so
* each has a Sigil, used in a fitting zone, that summons a catchable wild one. It fights as a boss would (area attacks
* and the Guardian's Ward, [BossFights]) until it's caught, when it becomes an ordinary Pokémon ([released]). One item,
* told apart by its custom data; the recipe's result also carries its name and where to use it, so the client needs
* none of this. Definitions: `data/cobblesurvival/summons/<species>.json`, written by tools/summons.py.
*/
object Summons : SimpleJsonResourceReloadListener(Gson(), "summons") {
/** Key in the sigil's custom data; its value is the species. */
const val KEY = "cobblesurvival_summon"
/** Key in a summoned Pokémon's persistent data, for as long as it's wild. */
const val TAG = "cobblesurvival_summoned"
/** How far ahead of the player it appears. */
private const val DISTANCE = 6.0
/** Only one at a time within this many blocks, so sigils can't be stacked into one place. */
private const val NEARBY = 96.0
private const val FLOOR_ABOVE = 4
private const val FLOOR_BELOW = 8
class Def(val species: String, val level: Int, val zone: String)
var defs: Map<String, Def> = emptyMap()
private set
private val ITEMS = DeferredRegister.createItems(CobbleSurvival.MOD_ID)
val SIGIL: DeferredHolder<Item, SummonSigil> = ITEMS.register("summon_sigil", Supplier { SummonSigil(Item.Properties().stacksTo(1).rarity(Rarity.EPIC)) })
fun register(bus: IEventBus) {
ITEMS.register(bus)
NeoForge.EVENT_BUS.addListener<AddReloadListenerEvent> { it.addListener(this) }
CobblemonEvents.POKEMON_CAPTURED.subscribe(Priority.NORMAL) { released(it.pokemon) }
}
override fun apply(files: Map<ResourceLocation, JsonElement>, resources: ResourceManager, profiler: ProfilerFiller) {
defs = files.values.map { it.asJsonObject }.associate {
it.get("species").asString to Def(it.get("species").asString, it.get("level").asInt, it.get("zone").asString)
}
CobbleSurvival.LOGGER.info("Loaded {} summons", defs.size)
}
/** Caught: from here on it's an ordinary Pokémon, without the boss rules. */
fun released(pokemon: Pokemon) {
pokemon.persistentData.remove(TAG)
pokemon.persistentData.remove(BossFights.TAG)
}
fun speciesOf(stack: ItemStack): String? =
stack.get(DataComponents.CUSTOM_DATA)?.copyTag()?.getString(KEY)?.takeIf { it.isNotEmpty() }
private fun tell(player: ServerPlayer, message: Component) = player.displayClientMessage(message, true)
/** Where it appears: on the floor a little ahead of the player, or level with them when there's none (at sea). */
private fun spot(level: ServerLevel, player: ServerPlayer): Vec3 {
val look = player.lookAngle
val ahead = player.position().add(Vec3(look.x, 0.0, look.z).normalize().scale(DISTANCE))
val x = Mth.floor(ahead.x)
val z = Mth.floor(ahead.z)
for (y in player.blockY + FLOOR_ABOVE downTo player.blockY - FLOOR_BELOW) {
val pos = BlockPos(x, y, z)
if (level.getBlockState(pos).isAir && level.getBlockState(pos.below()).isSolid) return Vec3.atBottomCenterOf(pos)
}
return ahead
}
/** Summons what [stack] calls for, if this is the place; true when it appeared and the sigil is spent. */
fun summon(level: ServerLevel, player: ServerPlayer, stack: ItemStack): Boolean {
val def = speciesOf(stack)?.let { defs[it] } ?: run {
tell(player, Component.translatableWithFallback("message.cobblesurvival.summon.blank", "This sigil is blank.").withStyle(ChatFormatting.RED))
return false
}
if (Zones.at(level, player.blockPosition())?.id != def.zone) {
tell(player, Component.translatableWithFallback("message.cobblesurvival.summon.wrong_zone", "This sigil only answers in %s.",
Zones.byId[def.zone]?.name ?: def.zone).withStyle(ChatFormatting.RED))
return false
}
if (level.getEntitiesOfClass(PokemonEntity::class.java, player.boundingBox.inflate(NEARBY)) { it.pokemon.persistentData.contains(TAG) }.isNotEmpty()) {
tell(player, Component.translatableWithFallback("message.cobblesurvival.summon.busy", "Another legend is already here.").withStyle(ChatFormatting.RED))
return false
}
val pokemon = PokemonProperties.parse("${def.species} level=${def.level}").create()
pokemon.persistentData.putBoolean(TAG, true)
pokemon.persistentData.putString(BossFights.TAG, "${CobbleSurvival.MOD_ID}:summon_${def.species}")
val entity = PokemonEntity(level, pokemon)
val at = spot(level, player)
entity.moveTo(at.x, at.y, at.z, player.yRot + 180f, 0f)
entity.setPersistenceRequired()
if (!level.addFreshEntity(entity)) return false
level.playSound(null, at.x, at.y, at.z, SoundEvents.WITHER_SPAWN, SoundSource.HOSTILE, 0.8f, 1.1f)
level.sendParticles(ParticleTypes.END_ROD, at.x, at.y + 1.0, at.z, 60, 1.2, 1.5, 1.2, 0.08)
player.sendSystemMessage(Component.translatableWithFallback("message.cobblesurvival.summon.answered",
"%s answers the sigil. Battle it and catch it: if it's knocked out, the sigil is lost.", pokemon.getDisplayName()).withStyle(ChatFormatting.GOLD))
CobbleSurvival.LOGGER.info("{} summoned {} at {}", player.gameProfile.name, def.species, BlockPos.containing(at).toShortString())
return true
}
}
/** A Sigil: use it in its zone (written on it) to summon its legendary. */
class SummonSigil(properties: Properties) : Item(properties) {
override fun use(level: Level, player: Player, hand: InteractionHand): InteractionResultHolder<ItemStack> {
val stack = player.getItemInHand(hand)
if (level is ServerLevel && player is ServerPlayer && Summons.summon(level, player, stack) && !player.isCreative) stack.shrink(1)
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide)
}
}
@@ -3,6 +3,7 @@ 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.boss.BossFights
import dev.cobblesurvival.zone.Guardians
import net.minecraft.server.TickTask
import net.minecraft.server.level.ServerLevel
@@ -11,7 +12,7 @@ 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'
* guardians, summoned legendaries while they're wild, 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
@@ -26,7 +27,7 @@ object SpecialPokemon {
}
fun isExempt(pokemon: Pokemon): Boolean =
pokemon.isUncatchable() || pokemon.persistentData.contains(Guardians.TAG) || isTrainerDitto(pokemon)
pokemon.isUncatchable() || pokemon.persistentData.contains(Guardians.TAG) || pokemon.persistentData.contains(BossFights.TAG) || isTrainerDitto(pokemon)
fun isTrainerDitto(pokemon: Pokemon): Boolean = pokemon.persistentData.contains(HMS_TRAINER)
@@ -774,6 +774,28 @@
}
]
},
{
"species": "cobblemon:articuno",
"drops": [
{
"item": "minecraft:feather",
"count": "2-4"
},
{
"item": "minecraft:chicken",
"count": "1"
},
{
"item": "cobblemon:never_melt_ice",
"count": "1"
}
],
"zones": [],
"summon": {
"zone": "frostpeak",
"level": 70
}
},
{
"species": "cobblemon:audino",
"drops": [
@@ -7994,6 +8016,24 @@
}
]
},
{
"species": "cobblemon:hooh",
"drops": [
{
"item": "minecraft:feather",
"count": "2-4"
},
{
"item": "cobblemon:sacred_ash",
"count": "1"
}
],
"zones": [],
"summon": {
"zone": "highlands",
"level": 80
}
},
{
"species": "cobblemon:hoothoot",
"drops": [
@@ -9231,6 +9271,34 @@
}
]
},
{
"species": "cobblemon:latias",
"drops": [
{
"item": "minecraft:feather",
"count": "2-4"
}
],
"zones": [],
"summon": {
"zone": "highlands",
"level": 70
}
},
{
"species": "cobblemon:latios",
"drops": [
{
"item": "minecraft:feather",
"count": "2-4"
}
],
"zones": [],
"summon": {
"zone": "highlands",
"level": 70
}
},
{
"species": "cobblemon:leafeon",
"drops": [
@@ -9732,6 +9800,20 @@
}
]
},
{
"species": "cobblemon:lugia",
"drops": [
{
"item": "minecraft:feather",
"count": "2-4"
}
],
"zones": [],
"summon": {
"zone": "deep_sea",
"level": 80
}
},
{
"species": "cobblemon:lumineon",
"drops": [
@@ -10792,6 +10874,20 @@
}
]
},
{
"species": "cobblemon:mewtwo",
"drops": [
{
"item": "cobblemon:twisted_spoon",
"count": "1"
}
],
"zones": [],
"summon": {
"zone": "deep_dark",
"level": 90
}
},
{
"species": "cobblemon:mienfoo",
"drops": [
@@ -11078,6 +11174,28 @@
}
]
},
{
"species": "cobblemon:moltres",
"drops": [
{
"item": "minecraft:feather",
"count": "2-4"
},
{
"item": "minecraft:cooked_chicken",
"count": "1"
},
{
"item": "cobblemon:charcoal_stick",
"count": "1"
}
],
"zones": [],
"summon": {
"zone": "basalt",
"level": 70
}
},
{
"species": "cobblemon:monferno",
"drops": [
@@ -11443,6 +11561,20 @@
}
]
},
{
"species": "cobblemon:naganadel",
"drops": [
{
"item": "minecraft:echo_shard",
"count": "1-3"
}
],
"zones": [],
"summon": {
"zone": "the_end",
"level": 70
}
},
{
"species": "cobblemon:natu",
"drops": [
@@ -12807,6 +12939,20 @@
}
]
},
{
"species": "cobblemon:poipole",
"drops": [
{
"item": "minecraft:echo_shard",
"count": "1-2"
}
],
"zones": [],
"summon": {
"zone": "the_end",
"level": 70
}
},
{
"species": "cobblemon:politoed",
"drops": [
@@ -13587,6 +13733,62 @@
}
]
},
{
"species": "cobblemon:rayquaza",
"drops": [
{
"item": "minecraft:dragon_breath",
"count": "3-5"
}
],
"zones": [],
"summon": {
"zone": "frostpeak",
"level": 90
}
},
{
"species": "cobblemon:regice",
"drops": [
{
"item": "cobblemon:never_melt_ice",
"count": "1"
}
],
"zones": [],
"summon": {
"zone": "frostpeak",
"level": 70
}
},
{
"species": "cobblemon:regirock",
"drops": [
{
"item": "cobblemon:hard_stone",
"count": "1"
}
],
"zones": [],
"summon": {
"zone": "dripstone",
"level": 70
}
},
{
"species": "cobblemon:registeel",
"drops": [
{
"item": "cobblemon:metal_coat",
"count": "1"
}
],
"zones": [],
"summon": {
"zone": "deep_dark",
"level": 70
}
},
{
"species": "cobblemon:relicanth",
"drops": [
@@ -19349,6 +19551,28 @@
}
]
},
{
"species": "cobblemon:zapdos",
"drops": [
{
"item": "minecraft:feather",
"count": "2-4"
},
{
"item": "minecraft:chicken",
"count": "1"
},
{
"item": "cobblemon:magnet",
"count": "1"
}
],
"zones": [],
"summon": {
"zone": "westmarch",
"level": 70
}
},
{
"species": "cobblemon:zebstrika",
"drops": [
@@ -19473,6 +19697,140 @@
]
}
],
"summons": [
{
"species": "cobblemon:zapdos",
"name": "Zapdos",
"zone": "westmarch",
"level": 70
},
{
"species": "cobblemon:regieleki",
"name": "Regieleki",
"zone": "westmarch",
"level": 70
},
{
"species": "cobblemon:ironleaves",
"name": "Iron Leaves",
"zone": "jungle",
"level": 70
},
{
"species": "cobblemon:regirock",
"name": "Regirock",
"zone": "dripstone",
"level": 70
},
{
"species": "cobblemon:latias",
"name": "Latias",
"zone": "highlands",
"level": 70
},
{
"species": "cobblemon:latios",
"name": "Latios",
"zone": "highlands",
"level": 70
},
{
"species": "cobblemon:walkingwake",
"name": "Walking Wake",
"zone": "mire",
"level": 70
},
{
"species": "cobblemon:regidrago",
"name": "Regidrago",
"zone": "sunscorch",
"level": 70
},
{
"species": "cobblemon:articuno",
"name": "Articuno",
"zone": "frostpeak",
"level": 70
},
{
"species": "cobblemon:regice",
"name": "Regice",
"zone": "frostpeak",
"level": 70
},
{
"species": "cobblemon:registeel",
"name": "Registeel",
"zone": "deep_dark",
"level": 70
},
{
"species": "cobblemon:moltres",
"name": "Moltres",
"zone": "basalt",
"level": 70
},
{
"species": "cobblemon:poipole",
"name": "Poipole",
"zone": "the_end",
"level": 70
},
{
"species": "cobblemon:xerneas",
"name": "Xerneas",
"zone": "jungle",
"level": 80
},
{
"species": "cobblemon:zarude",
"name": "Zarude",
"zone": "jungle",
"level": 80
},
{
"species": "cobblemon:mew",
"name": "Mew",
"zone": "jungle",
"level": 80
},
{
"species": "cobblemon:hooh",
"name": "Ho-Oh",
"zone": "highlands",
"level": 80
},
{
"species": "cobblemon:lugia",
"name": "Lugia",
"zone": "deep_sea",
"level": 80
},
{
"species": "cobblemon:regigigas",
"name": "Regigigas",
"zone": "deep_dark",
"level": 80
},
{
"species": "cobblemon:rayquaza",
"name": "Rayquaza",
"zone": "frostpeak",
"level": 90
},
{
"species": "cobblemon:mewtwo",
"name": "Mewtwo",
"zone": "deep_dark",
"level": 90
},
{
"species": "cobblemon:eternatus",
"name": "Eternatus",
"zone": "the_end",
"level": 90
}
],
"spawn_drops": [
{
"species": "cobblemon:beheeyem",
@@ -35,5 +35,6 @@
"tooltip.cobblesurvival.altar_core": "Sacrifice Altar: trades a Pokémon of Lv 20 or more for items; sneak-use twice. It's gone for good.",
"tooltip.cobblesurvival.altar_core.shell": "Shell: obsidian, crying obsidian, blackstone or polished blackstone bricks.",
"tooltip.cobblesurvival.rebirth_core": "Rebirth Chamber: a Lv 100 Pokémon becomes a shiny Lv 1 of its first stage after a long time.",
"tooltip.cobblesurvival.rebirth_core.shell": "Shell: amethyst, quartz or calcite blocks."
"tooltip.cobblesurvival.rebirth_core.shell": "Shell: amethyst, quartz or calcite blocks.",
"item.cobblesurvival.summon_sigil": "Blank Sigil"
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "cobblesurvival:item/summon_sigil"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B