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:
1 parent
5cae9252c3
commit
0877a6a179
75 files changed
+2321
-7
No files matched your search
@@ -0,0 +1,74 @@
|
||||
package dev.cobblesurvival.machine
|
||||
|
||||
import com.cobblemon.mod.common.api.Priority
|
||||
import com.cobblemon.mod.common.api.events.CobblemonEvents
|
||||
import com.cobblemon.mod.common.api.events.battles.BattleStartedEvent
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import com.cobblemon.mod.common.pokemon.properties.UncatchableProperty
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.nbt.CompoundTag
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.neoforged.neoforge.common.NeoForge
|
||||
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent
|
||||
import net.neoforged.neoforge.event.entity.player.AttackEntityEvent
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* The Pokémon a machine shows in its chamber (roadmap Phase 6: a real Cobblemon entity, so its own model and
|
||||
* animations are used). It's a copy of the Pokémon kept in the core, with its own id, scaled to fit the chamber,
|
||||
* without AI, invulnerable, silent, uncatchable, and it can't be used, hit or battled. It's never meant to be in the
|
||||
* world save: one that comes back from disk is dropped, and the core puts a fresh one there.
|
||||
*/
|
||||
object MachineDisplay {
|
||||
/** Key in the copy's persistent data; its value is the core's position. */
|
||||
const val TAG = "cobblesurvival_machine"
|
||||
/** The chamber is one block; leave a little room. */
|
||||
private const val FIT = 0.9f
|
||||
|
||||
fun register() {
|
||||
NeoForge.EVENT_BUS.addListener(::onJoin)
|
||||
NeoForge.EVENT_BUS.addListener<PlayerInteractEvent.EntityInteract> { if (isDisplay(it.target)) it.isCanceled = true }
|
||||
NeoForge.EVENT_BUS.addListener<PlayerInteractEvent.EntityInteractSpecific> { if (isDisplay(it.target)) it.isCanceled = true }
|
||||
NeoForge.EVENT_BUS.addListener<AttackEntityEvent> { if (isDisplay(it.target)) it.isCanceled = true }
|
||||
CobblemonEvents.BATTLE_STARTED_PRE.subscribe(Priority.HIGHEST, ::onBattle)
|
||||
}
|
||||
|
||||
fun isDisplay(entity: Entity?): Boolean = entity is PokemonEntity && entity.pokemon.persistentData.contains(TAG)
|
||||
|
||||
fun spawn(level: ServerLevel, pokemon: Pokemon, core: BlockPos, chamber: BlockPos, facing: Direction): PokemonEntity {
|
||||
val copy = Pokemon().loadFromNBT(level.registryAccess(), pokemon.saveToNBT(level.registryAccess(), CompoundTag()))
|
||||
copy.uuid = UUID.randomUUID()
|
||||
copy.persistentData.putLong(TAG, core.asLong())
|
||||
UncatchableProperty.uncatchable().apply(copy)
|
||||
val entity = PokemonEntity(level, copy)
|
||||
val size = maxOf(entity.bbWidth, entity.bbHeight)
|
||||
if (size > FIT) {
|
||||
copy.scaleModifier = copy.scaleModifier * FIT / size
|
||||
entity.refreshDimensions()
|
||||
}
|
||||
entity.setNoAi(true)
|
||||
entity.setNoGravity(true)
|
||||
entity.isInvulnerable = true
|
||||
entity.isSilent = true
|
||||
entity.setPersistenceRequired()
|
||||
entity.busyLocks.add(this)
|
||||
entity.moveTo(chamber.x + 0.5, chamber.y.toDouble(), chamber.z + 0.5, facing.toYRot(), 0f)
|
||||
entity.yHeadRot = facing.toYRot()
|
||||
entity.yBodyRot = facing.toYRot()
|
||||
level.addFreshEntity(entity)
|
||||
return entity
|
||||
}
|
||||
|
||||
/** A copy saved with its chunk comes back without its core knowing it: drop it. */
|
||||
private fun onJoin(event: EntityJoinLevelEvent) {
|
||||
if (event.loadedFromDisk() && isDisplay(event.entity)) event.isCanceled = true
|
||||
}
|
||||
|
||||
private fun onBattle(event: BattleStartedEvent.Pre) {
|
||||
if (event.battle.actors.any { actor -> actor.pokemonList.any { isDisplay(it.entity) } }) event.cancel()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user