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() } }