Phase 6 done: see-through cores, faint on sacrifice and rebirth; Phase 7 design
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>
No files matched your search
@@ -54,6 +54,8 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
||||
internal var confirmBy: UUID? = null
|
||||
internal var confirmUntil = 0L
|
||||
private var display: PokemonEntity? = null
|
||||
/** No Pokémon shown before this game time (the old one is still fainting). */
|
||||
private var showFrom = 0L
|
||||
private var ticks = 0
|
||||
|
||||
val def: MachineDefs.Def? get() = MachineDefs.byCore[BuiltInRegistries.BLOCK.getKey(blockState.block)]
|
||||
@@ -68,7 +70,7 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
||||
giveBack(level, Component.translatableWithFallback("message.cobblesurvival.machine.shell_broke", "The shell of the %s broke.", machineName))
|
||||
return
|
||||
}
|
||||
if (display?.isAlive != true) display = MachineDisplay.spawn(level, pokemon, worldPosition, shell!!.chamber, facing())
|
||||
if (display?.isAlive != true && level.gameTime >= showFrom) display = MachineDisplay.spawn(level, pokemon, worldPosition, shell!!.chamber, facing())
|
||||
}
|
||||
MachineWork.tick(this, level, def)
|
||||
receivedThisTick = 0
|
||||
@@ -184,8 +186,18 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
||||
return true
|
||||
}
|
||||
|
||||
/** The Pokémon is gone for good (sacrificed). */
|
||||
internal fun consume() = clear()
|
||||
/** The Pokémon is gone for good (sacrificed): the one shown faints. */
|
||||
internal fun consume(level: ServerLevel) {
|
||||
faintDisplay(level)
|
||||
clear()
|
||||
}
|
||||
|
||||
/** The shown Pokémon faints and goes; the next one shows once it's gone (a rebirth's new form). */
|
||||
internal fun faintDisplay(level: ServerLevel) {
|
||||
display?.takeIf { it.isAlive }?.let { MachineDisplay.faint(level, it) }
|
||||
display = null
|
||||
showFrom = level.gameTime + MachineDisplay.FAINT_TICKS
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
pokemon = null
|
||||
@@ -198,8 +210,6 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
||||
setChanged()
|
||||
}
|
||||
|
||||
internal fun respawnDisplay() = removeDisplay()
|
||||
|
||||
private fun removeDisplay() {
|
||||
display?.discard()
|
||||
display = null
|
||||
|
||||
@@ -8,6 +8,10 @@ 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
|
||||
@@ -15,6 +19,7 @@ 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
|
||||
|
||||
/**
|
||||
@@ -28,6 +33,12 @@ object MachineDisplay {
|
||||
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)
|
||||
@@ -35,6 +46,23 @@ object MachineDisplay {
|
||||
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)
|
||||
|
||||
@@ -163,7 +163,7 @@ object MachineWork {
|
||||
val rules = def.altar!!
|
||||
val rewards = altarRewards(rules, pokemon)
|
||||
if (core.confirmBy == player.uuid && level.gameTime <= core.confirmUntil) {
|
||||
core.consume()
|
||||
core.consume(level)
|
||||
rewards.forEach { give(level, core, player, it.copy()) }
|
||||
CobbleSurvival.LOGGER.info("{} sacrificed {} (Lv {}) at {}", player.gameProfile.name, pokemon.species.name, pokemon.level, core.blockPos)
|
||||
MachineBlockEntity.tell(player, Component.translatableWithFallback("message.cobblesurvival.machine.sacrificed",
|
||||
@@ -180,10 +180,10 @@ object MachineWork {
|
||||
// --- Rebirth Chamber -------------------------------------------------------------------------------------------
|
||||
|
||||
private fun rebirth(core: MachineBlockEntity, level: ServerLevel, rules: MachineDefs.RebirthRules, pokemon: Pokemon) {
|
||||
core.faintDisplay(level)
|
||||
rebirth(pokemon, rules.perfectIvs)
|
||||
core.rebirthAt = 0
|
||||
core.reborn = true
|
||||
core.respawnDisplay()
|
||||
core.setChanged()
|
||||
core.owner?.let { level.server.playerList.getPlayer(it) }?.let {
|
||||
MachineBlockEntity.tell(it, Component.translatableWithFallback("message.cobblesurvival.machine.reborn",
|
||||
|
||||
@@ -39,7 +39,9 @@ object Machines {
|
||||
})
|
||||
|
||||
private fun core(name: String, color: MapColor, sound: SoundType): DeferredBlock<MachineBlock> = BLOCKS.registerBlock(
|
||||
name, ::MachineBlock, BlockBehaviour.Properties.of().mapColor(color).strength(3.5f).requiresCorrectToolForDrops().sound(sound),
|
||||
name, ::MachineBlock, BlockBehaviour.Properties.of().mapColor(color).strength(3.5f).requiresCorrectToolForDrops().sound(sound)
|
||||
// See-through, so the Pokémon in the chamber behind the core shows (Adrian, 2026-11-02).
|
||||
.noOcclusion().isViewBlocking { _, _, _ -> false }.isSuffocating { _, _, _ -> false },
|
||||
)
|
||||
|
||||
fun register(bus: IEventBus) {
|
||||
|
||||
@@ -58,8 +58,8 @@ object MachineTests {
|
||||
SelfTestCase("heat_core_cooks_with_hoppers", 800, ::heatCore),
|
||||
SelfTestCase("dynamo_core_powers_cables", 100, ::dynamo),
|
||||
SelfTestCase("synthesis_core_rolls_drops", 100, ::synthesis),
|
||||
SelfTestCase("altar_core_sacrifice_needs_confirmation", 60, ::altar),
|
||||
SelfTestCase("rebirth_core_makes_a_shiny_first_stage", 100, ::rebirth),
|
||||
SelfTestCase("altar_core_sacrifice_needs_confirmation", 120, ::altar),
|
||||
SelfTestCase("rebirth_core_makes_a_shiny_first_stage", 200, ::rebirth),
|
||||
SelfTestCase("machine_preview_shows_the_shell", 300, ::preview),
|
||||
SelfTestCase("machine_showcase_builds_every_machine", 100, ::showcase),
|
||||
)
|
||||
@@ -348,6 +348,13 @@ object MachineTests {
|
||||
s.helper.startSequence()
|
||||
.thenExecute { core.rebirthAt = level.gameTime }
|
||||
.thenWaitUntil { s.check(core.reborn) { "the rebirth didn't finish" } }
|
||||
.thenWaitUntil {
|
||||
// The old form faints first; then the chamber shows the new one.
|
||||
val shown = displays(level, core.blockPos)
|
||||
s.check(shown.size == 1 && shown.single().pokemon.species.name == "Charmander") {
|
||||
"after the rebirth the chamber shows ${shown.map { it.pokemon.species.name }}, not one Charmander"
|
||||
}
|
||||
}
|
||||
.thenExecute {
|
||||
core.use(player)
|
||||
val back = Cobblemon.storage.getParty(player).firstOrNull { it.uuid == pokemon.uuid }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/altar_core_top",
|
||||
"front": "cobblesurvival:block/altar_core_front",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/dynamo_core_top",
|
||||
"front": "cobblesurvival:block/dynamo_core_front",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/heat_core_top",
|
||||
"front": "cobblesurvival:block/heat_core_front",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/rebirth_core_top",
|
||||
"front": "cobblesurvival:block/rebirth_core_front",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/orientable",
|
||||
"render_type": "minecraft:translucent",
|
||||
"textures": {
|
||||
"top": "cobblesurvival:block/synthesis_core_top",
|
||||
"front": "cobblesurvival:block/synthesis_core_front",
|
||||
|
||||
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 322 B |
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 260 B After Width: | Height: | Size: 223 B |
|
Before Width: | Height: | Size: 205 B After Width: | Height: | Size: 276 B |
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 200 B |
|
Before Width: | Height: | Size: 249 B After Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 242 B After Width: | Height: | Size: 258 B |
|
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 204 B |
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 219 B |
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 194 B After Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 254 B After Width: | Height: | Size: 227 B |
|
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 261 B |
|
Before Width: | Height: | Size: 186 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 247 B After Width: | Height: | Size: 227 B |