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:
1 parent
e206664b3d
commit
f7ea371862
92 files changed
+4081
-21
No files matched your search
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user