- 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>
428 lines
24 KiB
Kotlin
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()
|
|
}
|
|
}
|