Phase 4 play-test fixes: deep caves, fuller cave spawns, strays, shared loot

From Adrian's play-test (2026-10-30):
- The Deep Dark no longer fills the deepslate layer: world/biomes.json
  splits it by temperature (Deep Dark under cold and temperate land,
  basalt deltas under the hottest, ordinary caves between); at y -52 it
  drops from 30 % to 14 % of the land, basalt 7 %.
- Cave zones fill up to their targets with lines whose primary type fits
  the cave; top stages spawn up to the band's top (Deep Dark Lv 60, Basalt
  70); no time-of-day conditions underground.
- Wild Pokemon more than 10 levels above the band of the zone they
  wander into despawn (zone/Wanderers.kt).
- Loot is shared: all players in one scoreboard team, Lootr team_loot.
- Basalt ore nodes reach the deep basalt.
Self-tests strays_despawn and loot_is_shared; the catching rules from the
previous commit are now tested too. Template rebuilt (world check passed),
island quick check passed (36 self-tests and the restart test).
Shoulder Pokemon noted for Phase 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-31 02:01:41 +01:00
1 parent 6361636da7
commit d49684a2eb
40 files changed
+8212 -9412

No files matched your search

@@ -3,6 +3,7 @@ package dev.cobblesurvival.selftest
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.catching.Catching
import dev.cobblesurvival.zone.Wanderers
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.core.BlockPos
@@ -53,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()
CatchingTests.all() + WorldRulesTests.all()
private var running: List<Scenario> = emptyList()
private var forced: List<ChunkPos> = emptyList()
@@ -102,6 +103,7 @@ object SelfTest {
}
running = scenarios
Catching.hostilityPaused = true
Wanderers.paused = true
GameTestTicker.SINGLETON.clear()
GameTestRunner.Builder.fromInfo(infos, level)
.newStructureSpawner(StructureGridSpawner(origin, TESTS_PER_ROW, false))
@@ -132,5 +134,6 @@ object SelfTest {
forced.forEach { level.setChunkForced(it.x, it.z, false) }
running = emptyList()
Catching.hostilityPaused = false
Wanderers.paused = false
}
}
@@ -0,0 +1,81 @@
package dev.cobblesurvival.selftest
import dev.cobblesurvival.world.SharedLoot
import dev.cobblesurvival.zone.Wanderers
import dev.cobblesurvival.zone.Zones
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.InteractionHand
import net.minecraft.world.RandomizableContainer
import net.minecraft.world.inventory.ChestMenu
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.storage.loot.BuiltInLootTables
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.Vec3
/**
* World rules from Adrian's play-test (2026-10-30): a wild Pokémon far above the band of the zone it wandered into
* despawns (one from the Basalt Furnace turned up in the Lush Caves), and loot containers are shared by all players.
* The stray case switches the stray rule on in a batch of its own.
*/
object WorldRulesTests {
private val LOOTR_CHEST = ResourceLocation.parse("lootr:lootr_chest")
fun all() = listOf(
SelfTestCase("strays_despawn", 400, ::straysDespawn, batch = "wanderers"),
SelfTestCase("loot_is_shared", 40, ::lootIsShared),
)
private fun straysDespawn(s: Scenario) {
Wanderers.paused = false
s.afterTest { Wanderers.paused = true }
val level = s.level
s.player() // the rule only looks near players
val zone = Zones.at(level, s.helper.absolutePos(BlockPos(7, 1, 7)))
?: throw GameTestAssertException("the test platform lies in no zone")
val grace = Zones.wanderGrace ?: throw GameTestAssertException("no wanderers grace in the zone layout")
val stray = s.wild(s.pokemon("geodude", zone.maxLevel + grace + 5), s.at(4.5, 1.0, 10.5))
val local = s.wild(s.pokemon("geodude", zone.maxLevel), s.at(10.5, 1.0, 10.5))
s.helper.startSequence()
.thenWaitUntil { s.check(stray.isRemoved) { "a Lv ${stray.pokemon.level} wild Pokémon stays in ${zone.name} (Lv ${zone.minLevel}–${zone.maxLevel})" } }
.thenExecute { s.check(!local.isRemoved) { "a Lv ${local.pokemon.level} wild Pokémon was removed from ${zone.name}, its own band" } }
.thenSucceed()
}
/** Player A empties a fresh Lootr chest; player B then finds it empty too. */
private fun lootIsShared(s: Scenario) {
val level = s.level
val chest = BuiltInRegistries.BLOCK.get(LOOTR_CHEST)
s.check(chest != Blocks.AIR) { "Lootr isn't installed" }
val a = s.player()
val b = s.player()
val team = level.scoreboard.getPlayerTeam(SharedLoot.TEAM)
s.check(team != null && a.team == team && b.team == team) { "the players aren't both in the team ${SharedLoot.TEAM}" }
val pos = s.helper.absolutePos(BlockPos(7, 2, 7))
level.setBlockAndUpdate(pos, chest.defaultBlockState())
val container = level.getBlockEntity(pos) as? RandomizableContainer
?: throw GameTestAssertException("the Lootr chest has no loot table to set")
container.setLootTable(BuiltInLootTables.SIMPLE_DUNGEON, 1234L)
val found = open(s, a, pos)
s.check(found > 0) { "the chest had no loot for the first player" }
(a.containerMenu as ChestMenu).container.clearContent()
a.closeContainer()
val left = open(s, b, pos)
b.closeContainer()
s.check(left == 0) { "the second player found $left stacks in a chest the first one emptied: loot is per player" }
s.helper.succeed()
}
private fun open(s: Scenario, player: ServerPlayer, pos: BlockPos): Int {
val hit = BlockHitResult(Vec3.atCenterOf(pos), Direction.UP, pos, false)
player.gameMode.useItemOn(player, s.level, ItemStack.EMPTY, InteractionHand.MAIN_HAND, hit)
val menu = player.containerMenu as? ChestMenu ?: throw GameTestAssertException("the Lootr chest didn't open for ${player.gameProfile.name}")
return (0 until menu.container.containerSize).count { !menu.container.getItem(it).isEmpty }
}
}