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>
74 lines
3.8 KiB
Kotlin
74 lines
3.8 KiB
Kotlin
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 }
|
|
}
|
|
}
|
|
}
|