The End gets Pokémon: a dimension zone with Cobblemon's End lines

Adrian, 2026-11-02: the End looked barren. The End is now a zone of its own
(Lv 50-70, world/zones.json -> dimensions; Zones.at returns it anywhere in the
End). tools/zone_spawns.py copies every line Cobblemon spawns in End biomes
into spawn_pool_world/the_end.json, on top of its island home: Beheeyem,
Reuniclus, Metagross; Gothitelle and Sigilyph in End cities; Malamar and
Unown with a Poke Snack. Cobblemon rolls the bucket before the Pokemon and
has these all uncommon or rare, so they move to the common bucket with even
line weights (Metagross a fifth).

Self-test end_has_spawns: the main island is in the_end and offers only End
spawns, some in the common bucket. 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-02 17:13:12 +01:00
1 parent 4e7b16abae
commit 4f0bc03919
11 files changed
+810 -9

No files matched your search

@@ -19,6 +19,8 @@ import net.minecraft.core.BlockPos
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.Mth
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.levelgen.Heightmap
import kotlin.math.roundToInt
@@ -36,6 +38,7 @@ object ZoneTests {
SelfTestCase("spawn_pools_are_zoned", 20, ::spawnPoolsAreZoned),
SelfTestCase("spawn_survey", 2400, ::spawnSurvey, islandOnly = true),
SelfTestCase("level_pressure_cuts_xp", 100, ::levelPressure),
SelfTestCase("end_has_spawns", 200, ::endSpawns),
)
private fun zoneOf(detail: SpawnDetail): Zone? = Zones.byId[detail.id.substringBefore('-')]
@@ -108,6 +111,44 @@ object ZoneTests {
}.thenSucceed()
}
/**
* The End is a zone of its own (Adrian, 2026-11-02: it was barren). On the main island Cobblemon offers End
* spawns only, and some in the common bucket, which it rolls most of the time.
*/
private fun endSpawns(s: Scenario) {
val end = s.level.server.getLevel(Level.END) ?: throw GameTestAssertException("no End")
val chunks = (0..2).flatMap { x -> (0..2).map { z -> ChunkPos(x, z) } }
chunks.forEach { end.setChunkForced(it.x, it.z, true) }
s.afterTest { chunks.forEach { end.setChunkForced(it.x, it.z, false) } }
s.helper.startSequence()
.thenWaitUntil { s.check(chunks.all { end.areEntitiesLoaded(it.toLong()) }) { "the End's main island isn't loaded yet" } }
.thenExecute {
val stand = ground(end, 20, 20)
s.check(Zones.at(end, stand)?.id == "the_end") { "the End's main island is in zone ${Zones.at(end, stand)?.id}, not the_end" }
val (offered, common) = offeredAt(s.player(), end, stand)
val strangers = offered.filter { zoneOf(it)?.id != "the_end" }
s.check(strangers.isEmpty()) { "the End offers other zones' spawns: ${strangers.take(5).map { it.id }}" }
s.check(common.isNotEmpty()) { "nothing in the common bucket can spawn on the End's main island (${offered.map { it.id }})" }
}
.thenSucceed()
}
/** What Cobblemon could spawn around [pos] in [level] right now (the way `/checkspawn` does): all, and the common bucket's. */
private fun offeredAt(player: ServerPlayer, level: net.minecraft.server.level.ServerLevel, pos: BlockPos): Pair<Set<SpawnDetail>, Set<SpawnDetail>> {
val spawner = player.spawner
val diameter = Cobblemon.config.spawningZoneDiameter
val height = Cobblemon.config.spawningZoneHeight
val input = SpawningZoneInput(
SpawnCause(spawner, player), level,
pos.x - diameter / 2, pos.y - height / 2, pos.z - diameter / 2, diameter, height, diameter,
)
val positions = Cobblemon.areaSpawnablePositionResolver.resolve(
spawner, SpawnablePositionCalculator.prioritizedAreaCalculators, Cobblemon.spawningZoneGenerator.generate(spawner, input),
)
fun inBucket(bucket: String) = spawner.selector.getProbabilities(spawner, bucket, positions).filterValues { it > 0f }.keys
return CobblemonSpawnPools.WORLD_SPAWN_POOL.buckets.flatMap(::inBucket).toSet() to inBucket("common")
}
/**
* The ground at [x] [z], below any tree: standing on a mangrove's roots or logs, the survey once found no natural
* ground around it, so nothing could spawn (2026-11-02).
@@ -37,6 +37,8 @@ object Zones : SimpleJsonResourceReloadListener(Gson(), "zones") {
var byId: Map<String, Zone> = emptyMap()
private set
private var caves: Map<ResourceLocation, Zone> = emptyMap()
/** Zones that are a whole dimension (the End; Adrian, 2026-11-02: it was barren). */
private var dimensions: Map<ResourceLocation, Zone> = emptyMap()
var samples: Map<String, ZoneSample> = emptyMap()
private set
var levelPressure = LevelPressureRules()
@@ -65,7 +67,9 @@ object Zones : SimpleJsonResourceReloadListener(Gson(), "zones") {
byIndex = json.getAsJsonArray("zones").map { zone(it.asJsonObject) }
val caveZones = json.getAsJsonArray("caves").map { it.asJsonObject }
caves = caveZones.associate { ResourceLocation.parse(it.get("biome").asString) to zone(it) }
byId = (byIndex + caves.values).associateBy { it.id }
dimensions = json.getAsJsonArray("dimensions")?.map { it.asJsonObject }
?.associate { ResourceLocation.parse(it.get("dimension").asString) to zone(it) }.orEmpty()
byId = (byIndex + caves.values + dimensions.values).associateBy { it.id }
levelPressure = json.getAsJsonObject("level_pressure")?.let { rules ->
LevelPressureRules(
graceLevels = rules.get("grace_levels")?.asInt ?: 5,
@@ -87,7 +91,7 @@ object Zones : SimpleJsonResourceReloadListener(Gson(), "zones") {
}
check(cells.size == size * size) { "zone grid has ${cells.size} cells, expected ${size * size}" }
grid = cells.toIntArray()
CobbleSurvival.LOGGER.info("Loaded {} zones ({} surface, {} cave)", byId.size, byIndex.size, caves.size)
CobbleSurvival.LOGGER.info("Loaded {} zones ({} surface, {} cave, {} dimension)", byId.size, byIndex.size, caves.size, dimensions.size)
}
private fun samplesOf(json: JsonObject, key: String): Map<String, ZoneSample> =
@@ -101,8 +105,9 @@ object Zones : SimpleJsonResourceReloadListener(Gson(), "zones") {
return Zone(json.get("id").asString, json.get("name").asString, levels[0].asInt, levels[1].asInt)
}
/** The zone at [pos], or null outside the island's zones or outside the overworld. */
/** The zone at [pos]: a whole other dimension's zone, or in the overworld the island's; null outside them. */
fun at(level: Level, pos: BlockPos, biome: Holder<Biome> = level.getBiome(pos)): Zone? {
dimensions[level.dimension().location()]?.let { return it }
if (level.dimension() != Level.OVERWORLD || size == 0) return null
biome.unwrapKey().orElse(null)?.let { caves[it.location()] }?.let { return it }
// Grid cells are centred on -radius + i * step.