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
@@ -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 |
Reference in new issue
Block a user