Phase 6: Pokémon machines (five cores, shells as data)

Adrian (2026-10-31): a Large Furnace where a Fire Pokémon heats real
furnaces (hoppers in and out), our own Electric generator, no upkeep;
then a drop synthesizer (FE into full drop rolls, looting tiers), a
sacrifice altar (level tiers, rare candies only from Lv 80) and a
rebirth chamber (Lv 100 -> shiny Lv 1 first stage, three perfect IVs,
moves kept, Ability Patch so the hidden ability is optional).

- machine/: data-driven 3x3x3 shells (window spots for glass, free
  spot under the core), checked in four rotations; the Pokémon walks in
  from the party and lives in the core; breaking core or shell sends it
  home (party, else PC, offline too); a real Cobblemon entity copy is
  shown in the chamber (no AI, invulnerable, uncatchable, never saved).
- Heat Core keeps vanilla furnaces lit and faster; Dynamo Core makes
  and pushes FE; Synthesis Core takes FE and rolls drops into a hopper-
  extractable buffer; Altar Core sacrifices on a confirmed sneak-use;
  Rebirth Core runs on server time.
- Recipes, casing tags, block loot, textures (tools/machine_assets.py),
  models, blockstates, names.
- Self-tests: in/out, refusal, shell and core breaks (PC when full),
  chunk unload, server restart, and each machine's work.
- Docs: design, balance table, Phase 6 checklist; Phase 5 approved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-11-02 15:18:03 +01:00
1 parent 5cae9252c3
commit 0877a6a179
75 files changed
+2321 -7

No files matched your search

@@ -0,0 +1,73 @@
package dev.cobblesurvival.machine
import net.minecraft.core.BlockPos
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Rotation
import net.minecraft.world.level.block.state.BlockState
/**
* A machine's shell: what must stand at each offset from the core, written with the core in the south wall, and the
* chamber where the Pokémon stands. [match] tries the four horizontal rotations around the core.
*/
class Shell(private val parts: Map<BlockPos, BlockRule>, private val chamber: BlockPos) {
class BlockRule(val text: String, val test: (BlockState) -> Boolean)
/** The shell found around a core: where the chamber is and the shell's blocks (the core not included). */
class Match(val chamber: BlockPos, val blocks: Set<BlockPos>)
fun match(level: Level, core: BlockPos): Match? = ROTATIONS.firstNotNullOfOrNull { rotation ->
if (parts.all { (offset, rule) -> rule.test(level.getBlockState(core.offset(offset.rotate(rotation)))) }) {
Match(core.offset(chamber.rotate(rotation)), parts.keys.filter { it != chamber }.mapTo(HashSet()) { core.offset(it.rotate(rotation)) })
} else null
}
/** For a message: the first wrong block in the rotation that comes closest, and what belongs there. */
fun firstMismatch(level: Level, core: BlockPos): Pair<BlockPos, String>? = ROTATIONS
.map { rotation ->
parts.entries.filterNot { (offset, rule) -> rule.test(level.getBlockState(core.offset(offset.rotate(rotation)))) }
.map { (offset, rule) -> core.offset(offset.rotate(rotation)) to rule.text }
}
.minByOrNull { it.size }?.firstOrNull()
/** Every block position of the shell in the orientation where the core is in the south wall (for building it). */
fun layout(): Map<BlockPos, String> = parts.mapValues { it.value.text }
companion object {
private val ROTATIONS = listOf(Rotation.NONE, Rotation.CLOCKWISE_90, Rotation.CLOCKWISE_180, Rotation.COUNTERCLOCKWISE_90)
private const val CORE = 'C'
private const val CHAMBER = '.'
const val AIR = "air"
/** [layers] bottom to top, each a list of rows north to south, each row west to east; ' ' = anything. A key
* lists the blocks (ids or #tags) allowed for its character. */
fun parse(layers: List<List<String>>, keys: Map<Char, List<String>>): Shell {
val cells = layers.flatMapIndexed { y, rows -> rows.flatMapIndexed { z, row -> row.mapIndexed { x, c -> BlockPos(x, y, z) to c } } }
val core = cells.single { it.second == CORE }.first
val chamber = cells.single { it.second == CHAMBER }.first.subtract(core)
val parts = cells.filter { it.second != CORE && it.second != ' ' }.associate { (pos, c) ->
pos.subtract(core) to when (c) {
CHAMBER -> BlockRule(AIR) { it.isAir }
else -> anyOf(keys[c] ?: error("no key for '$c'"))
}
}
return Shell(parts, chamber)
}
private fun anyOf(specs: List<String>): BlockRule {
val rules = specs.map(::rule)
return BlockRule(specs.joinToString(" or ")) { state -> rules.any { it.test(state) } }
}
private fun rule(spec: String): BlockRule = if (spec.startsWith("#")) {
val tag = TagKey.create(Registries.BLOCK, ResourceLocation.parse(spec.substring(1)))
BlockRule(spec) { it.`is`(tag) }
} else {
val id = ResourceLocation.parse(spec)
BlockRule(spec) { BuiltInRegistries.BLOCK.getKey(it.block) == id }
}
}
}