Adrian (2026-11-02): the cores should be transparent so the Pokémon shows; the Pokémon's death sound when a sacrifice is made or a rebirth ends; then start the next phase (Phase 6 approved). - Cores drawn as a solid frame around tinted glass, rendered translucent, no occlusion (tools/machine_assets.py). - The shown Pokémon plays Cobblemon's faint animation and its cry pitched down (Cobblemon has no death sounds), vanishes after 2 s; a reborn Pokémon then shows in its new form. Tests check the new form. - Phase 7 decisions (every guardian gets area attacks, Groudon for the Basalt Furnace, Giratina replaces the Ender Dragon, dying or leaving the arena loses) and design in docs/design.md; README status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
5.1 KiB
Kotlin
103 lines
5.1 KiB
Kotlin
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<Fading>()
|
|
|
|
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)
|
|
NeoForge.EVENT_BUS.addListener<ServerTickEvent.Post> { 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()
|
|
}
|
|
}
|