Phase 6: shell preview, core tooltips, machine test world, faster furnace

Adrian (2026-11-02): double the furnace speed; there must be a way in
game to see how a machine is built; a test world with the machines.

- Large Furnace speed doubled (2 + 0.08 x level + 0.02 x SpA).
- Ghost preview: placing a core, using it with an incomplete shell, or
  sneak-using it empty shows every wrong spot as a small glowing block
  display for 10 s (never saved), in the core's orientation, with a
  chat line counting what's missing.
- Core tooltips: what the machine does, what its shell is made of.
- /cobblesurvival machines showcase builds every machine three times
  (working with a Showcase-owned Pokemon, empty, bare core) with signs,
  hoppers, chests and cables; tools/make_machine_world.py makes a
  superflat creative world from it (level.dat via tools/nbtfile.py)
  and --prism installs it as "Machine Test (Phase 6)".
- Self-tests machine_preview_shows_the_shell and
  machine_showcase_builds_every_machine; docs and checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-11-02 15:42:14 +01:00
1 parent 0877a6a179
commit c2a87f178e
17 files changed
+558 -27

No files matched your search

@@ -9,6 +9,8 @@ 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 net.minecraft.core.BlockPos
@@ -42,6 +44,8 @@ object MachineTests {
/** 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),
@@ -56,18 +60,13 @@ object MachineTests {
SelfTestCase("synthesis_core_rolls_drops", 100, ::synthesis),
SelfTestCase("altar_core_sacrifice_needs_confirmation", 60, ::altar),
SelfTestCase("rebirth_core_makes_a_shiny_first_stage", 100, ::rebirth),
SelfTestCase("machine_preview_shows_the_shell", 300, ::preview),
SelfTestCase("machine_showcase_builds_every_machine", 100, ::showcase),
)
private fun id(name: String) = ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, name)
private fun id(name: String) = MachineShowcase.id(name)
fun build(level: ServerLevel, core: BlockPos, name: String, casing: Block): MachineBlockEntity {
val def = MachineDefs.byCore[id(name)] ?: throw GameTestAssertException("no machine data for ${id(name)}")
for ((offset, text) in def.shell.layout()) {
level.setBlockAndUpdate(core.offset(offset), if (text == Shell.AIR) Blocks.AIR.defaultBlockState() else casing.defaultBlockState())
}
level.setBlockAndUpdate(core, BuiltInRegistries.BLOCK.get(id(name)).defaultBlockState().setValue(MachineBlock.FACING, Direction.SOUTH))
return level.getBlockEntity(core) as? MachineBlockEntity ?: throw GameTestAssertException("no machine at $core")
}
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)
@@ -357,4 +356,52 @@ object MachineTests {
}
.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()
}
}