Files
cobblemon-survival-pack/mod/src/main/kotlin/dev/cobblesurvival/selftest/MachineTests.kt
T
claudeandClaude Opus 5.5 c5d1cf93f4 Phase 8: the quest book, generated from docs/quests.yaml
- tools/quests.py writes pack/config/ftbquests/quests/ in FTB Quests 2101's
  format (data.snbt, chapters/*.snbt, texts in lang/en_us.snbt) with ids
  derived from the YAML names, so progress survives regeneration. Flexible
  progression, no lock icons, no rewards (Adrian, 2026-11-03). tools/build.py
  runs it.
- docs/quests.yaml: 18 chapters, 74 quests. Getting Started; one chapter per
  zone and the End from a zone template (reach it, catch three of its
  Pokemon, mine its ores, beat its guardian) plus chapter extras (Mire
  Waders, Eyes of Ender, Eternatus); Pokemon Machines; Base and Jobs.
- Node positions: --pull reads them from an FTB Quests folder into the
  layout block (only moved nodes); tools/prism_instance.py pulls from the
  instance before each sync and rebuilds if something moved.
- The pack's own events as hidden advancements (cobblesurvival:events/...),
  granted by QuestEvents: zone reached, guardian beaten, machine filled, End
  boss beaten.
- Self-tests quest_book_loads and quest_events_mark_zones; the guardian and
  furnace tests check their events. Self-tests and island check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:57:35 +01:00

428 lines
24 KiB
Kotlin

package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokemon.stats.Stats
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.pokemon.Pokemon
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.machine.MachineBlock
import dev.cobblesurvival.machine.MachineBlockEntity
import dev.cobblesurvival.machine.MachineDefs
import dev.cobblesurvival.machine.MachineDisplay
import dev.cobblesurvival.machine.MachineGhosts
import dev.cobblesurvival.machine.MachineShowcase
import dev.cobblesurvival.machine.MachineWork
import dev.cobblesurvival.machine.Shell
import dev.cobblesurvival.progress.QuestEvents
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.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.HopperBlock
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity
import net.minecraft.world.level.block.entity.HopperBlockEntity
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import net.neoforged.neoforge.capabilities.Capabilities
/**
* Pokémon machines (roadmap Phase 6). The roadmap's done-when: putting a Pokémon in and taking it out, a broken shell or
* core, chunk unload and server restart, all without losing or duplicating the Pokémon; and each machine's work.
* Machines are built with the core in the south wall, the chamber north of it.
*/
object MachineTests {
/** Core position in a test's area; the shell spans x 6-8, y 0-2, z 5-7. */
private val CORE = BlockPos(7, 1, 7)
/** Blocks south (unload) and north (restart) of spawn: far beyond view distance, clear of the vein tests (east, west). */
private const val UNLOAD_OFFSET = 352
private const val RESTART_OFFSET = -352
/** The showcase test: south of the unload spot. */
private const val SHOWCASE_OFFSET = 448
fun all() = listOf(
SelfTestCase("machine_pokemon_goes_in_and_out", 60, ::inAndOut),
SelfTestCase("machine_refuses_what_doesnt_fit", 20, ::refuses),
SelfTestCase("machine_shell_break_returns_pokemon", 100, ::shellBreak),
SelfTestCase("machine_core_break_returns_pokemon_to_pc", 60, ::coreBreakToPc),
SelfTestCase("machine_survives_unload", 600, ::survivesUnload),
SelfTestCase("machine_filled_before_restart", 100, ::beforeRestart),
SelfTestCase("machine_survives_restart", 200, ::afterRestart, afterRestart = true),
SelfTestCase("heat_core_cooks_with_hoppers", 800, ::heatCore),
SelfTestCase("dynamo_core_powers_cables", 100, ::dynamo),
SelfTestCase("synthesis_core_rolls_drops", 100, ::synthesis),
SelfTestCase("altar_core_sacrifice_needs_confirmation", 120, ::altar),
SelfTestCase("rebirth_core_makes_a_shiny_first_stage", 200, ::rebirth),
SelfTestCase("machine_preview_shows_the_shell", 300, ::preview),
SelfTestCase("machine_showcase_builds_every_machine", 100, ::showcase),
)
private fun id(name: String) = MachineShowcase.id(name)
fun build(level: ServerLevel, core: BlockPos, name: String, casing: Block): MachineBlockEntity = MachineShowcase.build(level, core, name, casing)
private fun build(s: Scenario, name: String, casing: Block) = build(s.level, s.helper.absolutePos(CORE), name, casing)
/** Sends [pokemon] (in [player]'s party) out two blocks in front of the core. */
private fun sendOut(s: Scenario, core: BlockPos, pokemon: Pokemon): PokemonEntity {
val spot = core.south(2)
if (s.level.getBlockState(spot.below()).isAir) s.level.setBlockAndUpdate(spot.below(), Blocks.STONE.defaultBlockState())
return s.spawn(PokemonEntity(s.level, pokemon), Vec3.atBottomCenterOf(spot))
}
private fun insert(s: Scenario, core: MachineBlockEntity, player: ServerPlayer, pokemon: Pokemon) {
sendOut(s, core.blockPos, pokemon)
core.use(player)
s.check(core.pokemon?.uuid == pokemon.uuid) { "${pokemon.species.name} didn't go into the ${core.machineName.string}" }
}
private fun displays(level: ServerLevel, core: BlockPos) =
level.getEntitiesOfClass(PokemonEntity::class.java, AABB(core).inflate(3.0)) { MachineDisplay.isDisplay(it) }
private fun inParty(player: ServerPlayer, pokemon: Pokemon) = Cobblemon.storage.getParty(player).any { it.uuid == pokemon.uuid }
private fun inPc(player: ServerPlayer, pokemon: Pokemon) = Cobblemon.storage.getPC(player).any { it.uuid == pokemon.uuid }
private fun inAndOut(s: Scenario) {
val player = s.player()
val core = build(s, "heat_core", Blocks.BRICKS)
val pokemon = s.give(player, s.pokemon("charmander", 20))
s.helper.startSequence()
.thenExecute {
insert(s, core, player, pokemon)
s.check(!inParty(player, pokemon)) { "Charmander is in the machine and still in the party" }
}
.thenWaitUntil { s.check(displays(s.level, core.blockPos).size == 1) { "${displays(s.level, core.blockPos).size} Pokémon shown in the chamber, not 1" } }
.thenExecute {
val shown = displays(s.level, core.blockPos).single()
s.check(shown.pokemon.uuid != pokemon.uuid) { "the shown Pokémon is the real one, not a copy" }
s.check(shown.isNoAi && shown.isInvulnerable && shown.pokemon.isUncatchable()) { "the shown Pokémon can move, be hurt or be caught" }
core.use(player)
s.check(core.pokemon == null) { "Charmander is still in the machine after taking it out" }
s.check(inParty(player, pokemon)) { "Charmander didn't come back to the party" }
}
.thenWaitUntil { s.check(displays(s.level, core.blockPos).isEmpty()) { "the chamber still shows a Pokémon" } }
.thenSucceed()
}
private fun refuses(s: Scenario) {
val player = s.player()
val core = build(s, "heat_core", Blocks.BRICKS)
val pikachu = s.give(player, s.pokemon("pikachu", 20))
sendOut(s, core.blockPos, pikachu)
core.use(player)
s.check(core.pokemon == null && inParty(player, pikachu)) { "a Pikachu went into the Heat Core (Fire types only)" }
val charmander = s.give(player, s.pokemon("charmander", 20))
sendOut(s, core.blockPos, charmander)
s.level.setBlockAndUpdate(core.blockPos.offset(-1, 1, -2), Blocks.AIR.defaultBlockState())
core.use(player)
s.check(core.pokemon == null && inParty(player, charmander)) { "Charmander went into a Heat Core with a hole in its shell" }
s.helper.succeed()
}
private fun shellBreak(s: Scenario) {
val player = s.player()
val core = build(s, "heat_core", Blocks.BRICKS)
val pokemon = s.give(player, s.pokemon("charmander", 20))
s.helper.startSequence()
.thenExecute {
insert(s, core, player, pokemon)
s.level.setBlockAndUpdate(core.blockPos.offset(1, 1, -2), Blocks.AIR.defaultBlockState())
}
.thenWaitUntil { s.check(core.pokemon == null) { "Charmander stayed in a machine with a broken shell" } }
.thenExecute { s.check(inParty(player, pokemon) && !inPc(player, pokemon)) { "Charmander didn't come back to the party (only)" } }
.thenWaitUntil { s.check(displays(s.level, core.blockPos).isEmpty()) { "the broken machine still shows a Pokémon" } }
.thenSucceed()
}
private fun coreBreakToPc(s: Scenario) {
val player = s.player()
val core = build(s, "heat_core", Blocks.BRICKS)
val pokemon = s.give(player, s.pokemon("charmander", 20))
insert(s, core, player, pokemon)
repeat(6) { s.give(player, s.pokemon("rattata", 5)) }
s.level.destroyBlock(core.blockPos, true)
s.check(!inParty(player, pokemon) && inPc(player, pokemon)) { "with a full party, Charmander didn't go to the PC when the core broke" }
s.helper.startSequence()
.thenWaitUntil { s.check(displays(s.level, core.blockPos).isEmpty()) { "the broken machine still shows a Pokémon" } }
.thenSucceed()
}
private fun spot(level: ServerLevel, offset: Int) = level.sharedSpawnPos.let { BlockPos(it.x, SelfTest.PLATFORM_Y, it.z + offset) }
private fun chunks(core: BlockPos) = listOf(core.offset(-2, 0, -3), core.offset(2, 0, 3), core.offset(-2, 0, 3), core.offset(2, 0, -3))
.map(::ChunkPos).distinct()
private fun force(level: ServerLevel, core: BlockPos, on: Boolean) = chunks(core).forEach { level.setChunkForced(it.x, it.z, on) }
private fun clearMachine(level: ServerLevel, core: BlockPos) {
BlockPos.betweenClosed(core.offset(-1, -1, -2), core.offset(1, 1, 2)).forEach { level.setBlockAndUpdate(it, Blocks.AIR.defaultBlockState()) }
level.setBlockAndUpdate(core.south(2).below(), Blocks.AIR.defaultBlockState())
}
private fun survivesUnload(s: Scenario) {
val level = s.level
val pos = spot(level, UNLOAD_OFFSET)
val chunk = ChunkPos(pos)
val player = s.player()
val pokemon = s.give(player, s.pokemon("charmander", 20))
s.afterTest {
force(level, pos, true)
clearMachine(level, pos)
force(level, pos, false)
}
s.helper.startSequence()
.thenExecute { force(level, pos, true) }
.thenWaitUntil { s.check(level.isPositionEntityTicking(pos.south(2))) { "the machine's spot doesn't tick entities yet" } }
.thenExecute {
insert(s, build(level, pos, "heat_core", Blocks.BRICKS), player, pokemon)
force(level, pos, false)
}
.thenWaitUntil { s.check(!level.hasChunk(chunk.x, chunk.z)) { "the machine's chunk is still loaded" } }
.thenExecute { force(level, pos, true) }
.thenWaitUntil {
val back = level.getBlockEntity(pos) as? MachineBlockEntity
s.check(back?.pokemon?.uuid == pokemon.uuid) { "after unload the core holds ${back?.pokemon?.species?.name}, not Charmander" }
s.check(displays(level, pos).size == 1) { "after unload ${displays(level, pos).size} Pokémon are shown, not 1" }
}
.thenExecute {
val back = level.getBlockEntity(pos) as MachineBlockEntity
back.use(player)
s.check(inParty(player, pokemon) && back.pokemon == null) { "Charmander didn't come out after the unload" }
}
.thenSucceed()
}
private fun beforeRestart(s: Scenario) {
val level = s.level
val pos = spot(level, RESTART_OFFSET)
val player = s.player()
val pokemon = s.give(player, s.pokemon("charmander", 20))
s.helper.startSequence()
.thenExecute { force(level, pos, true) }
.thenWaitUntil { s.check(level.isPositionEntityTicking(pos.south(2))) { "the machine's spot doesn't tick entities yet" } }
.thenExecute {
insert(s, build(level, pos, "heat_core", Blocks.BRICKS), player, pokemon)
force(level, pos, false)
}
.thenSucceed()
}
private fun afterRestart(s: Scenario) {
val level = s.level
val pos = spot(level, RESTART_OFFSET)
s.afterTest {
clearMachine(level, pos)
force(level, pos, false)
}
s.helper.startSequence()
.thenExecute { force(level, pos, true) }
.thenWaitUntil {
val core = level.getBlockEntity(pos) as? MachineBlockEntity
s.check(core?.pokemon?.species?.name == "Charmander") { "after the restart the core holds ${core?.pokemon?.species?.name}, not Charmander" }
s.check(displays(level, pos).size == 1) { "after the restart ${displays(level, pos).size} Pokémon are shown, not 1" }
}
.thenExecute {
val core = level.getBlockEntity(pos) as MachineBlockEntity
s.check(core.giveBack(level, null)) { "Charmander couldn't go back to its (offline) owner after the restart" }
}
.thenSucceed()
}
private fun heatCore(s: Scenario) {
val level = s.level
val player = s.player()
val core = build(s, "heat_core", Blocks.BRICKS)
insert(s, core, player, s.give(player, s.pokemon("charmander", 50)))
s.check(QuestEvents.done(player, "machine/heat_core")) { "a Large Furnace built didn't mark machine/heat_core for the quest book" }
// A furnace against the east wall, a hopper with raw iron above it and an empty one below; no fuel anywhere.
val furnace = core.blockPos.offset(2, 0, -1)
level.setBlockAndUpdate(furnace, Blocks.FURNACE.defaultBlockState())
level.setBlockAndUpdate(furnace.above(), Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING, Direction.DOWN))
level.setBlockAndUpdate(furnace.below(), Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING, Direction.DOWN))
(level.getBlockEntity(furnace.above()) as HopperBlockEntity).setItem(0, ItemStack(Items.RAW_IRON, 4))
val output = level.getBlockEntity(furnace.below()) as HopperBlockEntity
val active = { core.blockState.getValue(MachineBlock.ACTIVE) }
s.helper.startSequence()
// Adrian, 2026-11-02: a working machine shows it (the core glows and animates, particles show).
.thenWaitUntil { s.check(active()) { "the Large Furnace's core doesn't show that it works" } }
.thenExecute {
// The Pokémon inside flinches as if hit, but takes no damage.
val shown = displays(level, core.blockPos).firstOrNull() ?: throw GameTestAssertException("no Pokémon shown in the chamber")
MachineDisplay.flinch(level, shown)
val inside = core.pokemon!!
s.check(shown.isAlive && inside.currentHealth == inside.maxHealth) { "a flinch hurt the Pokémon (${inside.currentHealth}/${inside.maxHealth})" }
}
.thenWaitUntil {
val ingots = (0 until output.containerSize).sumOf { output.getItem(it).takeIf { s -> s.`is`(Items.IRON_INGOT) }?.count ?: 0 }
s.check(ingots >= 4) { "the hopper under the furnace has $ingots iron ingots, not 4" }
}
.thenExecute {
val fuel = (level.getBlockEntity(furnace) as AbstractFurnaceBlockEntity).getItem(1)
s.check(fuel.isEmpty) { "the furnace got fuel: $fuel" }
}
.thenWaitUntil { s.check(!active()) { "the Large Furnace's core still shows work with nothing left to cook" } }
.thenSucceed()
}
private fun dynamo(s: Scenario) {
val level = s.level
val player = s.player()
val core = build(s, "dynamo_core", Blocks.COPPER_BLOCK)
insert(s, core, player, s.give(player, s.pokemon("pikachu", 30)))
val cable = BuiltInRegistries.BLOCK.get(ResourceLocation.parse("poke_power:power_cable"))
s.check(cable != Blocks.AIR) { "Poke Power's cable isn't installed" }
val cablePos = core.blockPos.south()
level.setBlockAndUpdate(cablePos, cable.defaultBlockState())
s.helper.startSequence()
.thenWaitUntil {
val stored = level.getCapability(Capabilities.EnergyStorage.BLOCK, cablePos, Direction.NORTH)?.energyStored ?: 0
s.check(stored > 0) { "the cable in front of the Dynamo Core got no energy (core: ${core.energy} FE)" }
}
.thenSucceed()
}
private fun synthesis(s: Scenario) {
val level = s.level
val player = s.player()
val core = build(s, "synthesis_core", Blocks.GOLD_BLOCK)
insert(s, core, player, s.give(player, s.pokemon("magikarp", 10)))
val rules = MachineDefs.byCore[id("synthesis_core")]!!.synthesis!!
val energy = level.getCapability(Capabilities.EnergyStorage.BLOCK, core.blockPos, Direction.SOUTH)
?: throw GameTestAssertException("the Synthesis Core takes no energy")
val taken = energy.receiveEnergy(Int.MAX_VALUE, false)
s.check(taken == rules.maxInput) { "the Synthesis Core took $taken FE in one tick, not its cap ${rules.maxInput}" }
level.setBlockAndUpdate(core.blockPos.below(), Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING, Direction.DOWN))
val hopper = level.getBlockEntity(core.blockPos.below()) as HopperBlockEntity
s.helper.startSequence()
.thenWaitUntil { s.check(core.tier == 1) { "a gold shell is tier ${core.tier}, not 1 (looting I)" } }
.thenExecute { core.energy = rules.fePerRoll }
.thenWaitUntil {
val salmon = (0 until hopper.containerSize).sumOf { hopper.getItem(it).takeIf { s -> s.`is`(Items.SALMON) }?.count ?: 0 }
s.check(salmon > 0) { "no salmon from Magikarp's drops reached the hopper under the core" }
}
.thenSucceed()
}
private fun count(player: ServerPlayer, item: Item) = player.inventory.items.sumOf { if (it.`is`(item)) it.count else 0 }
private fun altar(s: Scenario) {
val level = s.level
val player = s.player()
val rules = MachineDefs.byCore[id("altar_core")]!!.altar!!
val rareCandy = BuiltInRegistries.ITEM.get(ResourceLocation.parse("cobblemon:rare_candy"))
s.check(MachineWork.altarRewards(rules, s.pokemon("geodude", 79)).none { it.`is`(rareCandy) }) { "a Lv 79 Pokémon gives rare candies" }
val core = build(s, "altar_core", Blocks.OBSIDIAN)
val pokemon = s.give(player, s.pokemon("dragonite", 100))
insert(s, core, player, pokemon)
player.isShiftKeyDown = true
core.use(player)
s.check(core.pokemon != null) { "Dragonite was sacrificed without confirming" }
core.use(player)
player.isShiftKeyDown = false
s.check(core.pokemon == null && !inParty(player, pokemon) && !inPc(player, pokemon)) { "Dragonite wasn't sacrificed" }
s.check(count(player, rareCandy) >= 8) { "no rare candies for a Lv 100 sacrifice" }
s.check(count(player, Items.DRAGON_EGG) == 1) { "no dragon egg for a Lv 100 Dragon type" }
s.check(count(player, Items.NETHER_STAR) == 1) { "no nether star for a Lv 100 sacrifice" }
s.helper.startSequence()
.thenWaitUntil { s.check(displays(level, core.blockPos).isEmpty()) { "the altar still shows the sacrificed Pokémon" } }
.thenSucceed()
}
private fun rebirth(s: Scenario) {
val level = s.level
// The rebirth itself, on its own.
val test = s.pokemon("charizard", 100)
Stats.PERMANENT.forEach { test.setIV(it, 10) }
test.setIV(Stats.SPEED, 31)
val moves = test.moveSet.getMoves().map { it.name }
val ability = test.ability.name
MachineWork.rebirth(test, 3)
s.check(test.species.name == "Charmander" && test.level == 1 && test.shiny) { "rebirth gave ${test.species.name} Lv ${test.level} shiny=${test.shiny}" }
val perfect = Stats.PERMANENT.count { test.ivs[it] == 31 }
s.check(perfect == 4 && Stats.PERMANENT.all { (test.ivs[it] ?: 0) >= 10 }) { "after rebirth $perfect IVs are 31 (wanted 4) or one went down: ${test.ivs}" }
s.check(test.moveSet.getMoves().map { it.name } == moves) { "rebirth changed the moves: $moves -> ${test.moveSet.getMoves().map { it.name }}" }
s.check(test.ability.name == ability) { "rebirth changed the ability: $ability -> ${test.ability.name}" }
// And in the machine: the time runs out, the owner takes it out with an Ability Patch.
val player = s.player()
val core = build(s, "rebirth_core", Blocks.AMETHYST_BLOCK)
val pokemon = s.give(player, s.pokemon("charizard", 100))
insert(s, core, player, pokemon)
s.check(core.rebirthAt > level.gameTime) { "the rebirth didn't start" }
val patch = BuiltInRegistries.ITEM.get(ResourceLocation.parse("cobblemon:ability_patch"))
s.helper.startSequence()
.thenExecute { core.rebirthAt = level.gameTime }
.thenWaitUntil { s.check(core.reborn) { "the rebirth didn't finish" } }
.thenWaitUntil {
// The old form faints first; then the chamber shows the new one.
val shown = displays(level, core.blockPos)
s.check(shown.size == 1 && shown.single().pokemon.species.name == "Charmander") {
"after the rebirth the chamber shows ${shown.map { it.pokemon.species.name }}, not one Charmander"
}
}
.thenExecute {
core.use(player)
val back = Cobblemon.storage.getParty(player).firstOrNull { it.uuid == pokemon.uuid }
s.check(back?.species?.name == "Charmander" && back.level == 1 && back.shiny) { "${back?.species?.name} came out, not a shiny Lv 1 Charmander" }
s.check(count(player, patch) == 1) { "no Ability Patch came with the reborn Pokémon" }
}
.thenSucceed()
}
private fun ghosts(level: ServerLevel, core: BlockPos) =
level.getEntitiesOfClass(net.minecraft.world.entity.Display.BlockDisplay::class.java, AABB(core).inflate(3.0)) { it.tags.contains(MachineGhosts.TAG) }
/** Using a core with a broken shell shows the wrong spots as ghost blocks; they go away again. */
private fun preview(s: Scenario) {
val level = s.level
val player = s.player()
val core = build(s, "heat_core", Blocks.BRICKS)
val pos = core.blockPos
level.setBlockAndUpdate(pos.offset(-1, 1, -2), Blocks.AIR.defaultBlockState())
level.setBlockAndUpdate(pos.offset(1, -1, -1), Blocks.DIRT.defaultBlockState())
level.setBlockAndUpdate(pos.offset(0, 0, -1), Blocks.STONE.defaultBlockState())
core.use(player)
s.check(ghosts(level, pos).size == 3) { "${ghosts(level, pos).size} ghost blocks for 3 wrong spots" }
s.helper.startSequence()
.thenWaitUntil { s.check(ghosts(level, pos).isEmpty()) { "the ghost blocks didn't go away" } }
.thenSucceed()
}
/** The test world's showcase: every machine works (Pokémon inside, complete shell), every bare core is there. */
private fun showcase(s: Scenario) {
val level = s.level
val origin = spot(level, SHOWCASE_OFFSET)
val area = BlockPos.betweenClosed(origin.offset(-3, -3, -3), origin.offset(5 * 11 + 3, 4, 2 * 12 + 6))
val chunkSet = area.map { ChunkPos(it) }.distinct()
chunkSet.forEach { level.setChunkForced(it.x, it.z, true) }
s.afterTest {
BlockPos.betweenClosed(origin.offset(-3, -3, -3), origin.offset(5 * 11 + 3, 4, 2 * 12 + 6)).forEach {
level.setBlockAndUpdate(it, Blocks.AIR.defaultBlockState())
}
chunkSet.forEach { level.setChunkForced(it.x, it.z, false) }
}
val lines = MachineShowcase.showcase(level, origin)
s.check(lines.size == MachineDefs.byCore.size) { "the showcase built ${lines.size} machines, not ${MachineDefs.byCore.size}" }
val cores = BlockPos.betweenClosed(origin, origin.offset(5 * 11, 2, 2 * 12 + 3))
.mapNotNull { level.getBlockEntity(it) as? MachineBlockEntity }
s.check(cores.size == 3 * lines.size) { "${cores.size} cores in the showcase, not ${3 * lines.size}" }
s.helper.startSequence()
.thenWaitUntil {
val working = cores.filter { it.pokemon != null }
s.check(working.size == lines.size && working.all { it.def!!.shell.match(level, it.blockPos) != null }) {
"${working.size} working machines with complete shells, not ${lines.size}"
}
s.check(working.sumOf { displays(level, it.blockPos).size } == lines.size) { "not every working machine shows its Pokémon" }
}
.thenSucceed()
}
}