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.core.Holder import net.minecraft.resources.ResourceLocation import net.minecraft.sounds.SoundEvent import net.minecraft.sounds.SoundSource 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 net.neoforged.neoforge.event.tick.ServerTickEvent 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 /** A fainting Pokémon's cry is played lower, as in the games. */ private const val FAINT_PITCH = 0.6f const val FAINT_TICKS = 40L private class Fading(val entity: PokemonEntity, val until: Long) private val fading = ArrayList() fun register() { NeoForge.EVENT_BUS.addListener(::onJoin) NeoForge.EVENT_BUS.addListener { if (isDisplay(it.target)) it.isCanceled = true } NeoForge.EVENT_BUS.addListener { if (isDisplay(it.target)) it.isCanceled = true } NeoForge.EVENT_BUS.addListener { if (isDisplay(it.target)) it.isCanceled = true } CobblemonEvents.BATTLE_STARTED_PRE.subscribe(Priority.HIGHEST, ::onBattle) NeoForge.EVENT_BUS.addListener { event -> if (fading.isEmpty()) return@addListener val now = event.server.overworld().gameTime fading.removeIf { f -> (f.until <= now || f.entity.isRemoved).also { if (it) f.entity.discard() } } } } /** * The shown Pokémon dies (a sacrifice, or the old form at the end of a rebirth; Adrian, 2026-11-02): its cry, * pitched down as for a fainting Pokémon, and Cobblemon's faint animation; then it's gone after [FAINT_TICKS]. */ fun faint(level: ServerLevel, entity: PokemonEntity) { val species = entity.pokemon.species.resourceIdentifier val cry = SoundEvent.createVariableRangeEvent(ResourceLocation.fromNamespaceAndPath(species.namespace, "pokemon.${species.path}.cry")) level.playSeededSound(null, entity.x, entity.y, entity.z, Holder.direct(cry), SoundSource.NEUTRAL, 1f, FAINT_PITCH, level.random.nextLong()) entity.playAnimation("faint") fading += Fading(entity, level.gameTime + FAINT_TICKS) } 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() } }