package dev.cobblesurvival.permadeath import com.cobblemon.mod.common.Cobblemon import com.cobblemon.mod.common.api.Priority import com.cobblemon.mod.common.api.events.CobblemonEvents import com.cobblemon.mod.common.api.events.battles.BattleEndedEvent import com.cobblemon.mod.common.api.events.battles.BattleFaintedEvent import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore import com.cobblemon.mod.common.api.storage.pc.PCStore import com.cobblemon.mod.common.pokemon.Pokemon import com.cobblemon.mod.common.util.isInBattle import dev.cobblesurvival.CobbleSurvival import net.minecraft.ChatFormatting import net.minecraft.core.registries.Registries import net.minecraft.network.chat.Component import net.minecraft.resources.ResourceLocation import net.minecraft.server.level.ServerPlayer import net.minecraft.sounds.SoundEvents import net.minecraft.sounds.SoundSource import net.minecraft.tags.TagKey import net.minecraft.world.item.Item import net.neoforged.neoforge.common.NeoForge import net.neoforged.neoforge.event.tick.ServerTickEvent import java.util.UUID import java.util.concurrent.ConcurrentLinkedQueue /** * A fainted Pokémon is gone for good. CobbleHardcoreMon settles faints in battles; this closes the gaps around it: * - a Pokémon that faints outside a battle (falling, lava, poison, ...) is settled at the end of the same server * tick. CobbleHardcoreMon only looks once a second, and waits a full minute for a Pokémon whose charm already * saved it once, which left time to heal it or move it to the PC. * - a Pokémon that fainted in a battle and was revived before the battle ended (a Revive, Revival Blessing, ...) * is fainted again when the battle ends, just before CobbleHardcoreMon settles the battle. * - once a second, every online player's party and PC are swept for fainted Pokémon (one whose trainer was offline * or in a battle when it fainted). * Settling: a Pokémon holding an item from `#cobblesurvival:revival_charms` is healed and the charm is used up; * any other is removed from its party or PC. The one exception is the player's starter (Adrian, 2026-11-09): it only * faints, and can't be released or sacrificed either ([isStarter]; CobbleHardcoreMon's side is mixin/HardcorePartyMixin). */ object Permadeath { val REVIVAL_CHARMS: TagKey = TagKey.create(Registries.ITEM, ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "revival_charms")) private const val SWEEP_TICKS = 20 private val fainted = ConcurrentLinkedQueue() private val faintedInBattle = HashMap>() fun register() { CobblemonEvents.POKEMON_FAINTED.subscribe(Priority.NORMAL) { fainted += it.pokemon } CobblemonEvents.BATTLE_FAINTED.subscribe(Priority.NORMAL, ::onBattleFainted) // CobbleHardcoreMon settles the battle at NORMAL priority; it has to see the revived Pokémon fainted. CobblemonEvents.BATTLE_ENDED.subscribe(Priority.HIGHEST, ::onBattleEnded) NeoForge.EVENT_BUS.addListener(::onServerTick) CobblemonEvents.POKEMON_RELEASED_EVENT_PRE.subscribe(Priority.NORMAL) { event -> if (!isStarter(event.pokemon)) return@subscribe event.cancel() event.player.sendSystemMessage(Component.translatableWithFallback("message.cobblesurvival.permadeath.starter_release", "%s is your first partner: it can't be released.", event.pokemon.getDisplayName(false)).withStyle(ChatFormatting.YELLOW)) } } /** The Pokémon this player chose as a starter, which Cobblemon remembers for each player. */ fun isStarter(pokemon: Pokemon): Boolean { val owner = pokemon.getOwnerUUID() ?: return false return Cobblemon.playerDataManager.getGenericData(owner).starterUUID == pokemon.uuid } private fun onBattleFainted(event: BattleFaintedEvent) { val killed = event.killed // In a battle on copies (exhibition rules) the real Pokémon is not hurt, so it does not count. if (killed.effectedPokemon !== killed.originalPokemon || !killed.originalPokemon.isPlayerOwned()) return faintedInBattle.getOrPut(event.battle.battleId) { HashSet() } += killed.originalPokemon } private fun onBattleEnded(event: BattleEndedEvent) { val faintedHere = faintedInBattle.remove(event.battle.battleId) ?: return for (pokemon in faintedHere) { if (!pokemon.isFainted() && pokemon.storeCoordinates.get() != null) { pokemon.currentHealth = 0 } } } private fun onServerTick(event: ServerTickEvent.Post) { while (true) { val pokemon = fainted.poll() ?: break val owner = pokemon.getOwnerPlayer() ?: continue settle(owner, pokemon) } if (event.server.tickCount % SWEEP_TICKS == 0) { for (player in event.server.playerList.players) { val party = Cobblemon.storage.getParty(player) val pc = Cobblemon.storage.getPC(player) (party.filter { it.isFainted() } + pc.filter { it.isFainted() }).forEach { settle(player, it) } } } } private fun settle(player: ServerPlayer, pokemon: Pokemon) { val store = pokemon.storeCoordinates.get()?.store if (!pokemon.isFainted() || isStarter(pokemon) || (store !is PlayerPartyStore && store !is PCStore)) return // Faints during a battle are settled when it ends (above, and by CobbleHardcoreMon). if (player.isInBattle()) return try { val name = pokemon.getDisplayName(false) if (pokemon.heldItem().`is`(REVIVAL_CHARMS)) { pokemon.heal() pokemon.removeHeldItem() player.sendSystemMessage( Component.translatableWithFallback( "message.cobblesurvival.permadeath.saved", "%s's Revival Charm saved it and crumbled to dust.", name, ).withStyle(ChatFormatting.GOLD) ) player.playNotifySound(SoundEvents.RESPAWN_ANCHOR_DEPLETE.value(), SoundSource.PLAYERS, 1f, 1f) CobbleSurvival.LOGGER.info("Permadeath: a Revival Charm saved {} of {}", name.string, player.gameProfile.name) } else if (store?.remove(pokemon) == true) { player.sendSystemMessage( Component.translatableWithFallback( "message.cobblesurvival.permadeath.lost", "%s fainted and is gone for good.", name, ).withStyle(ChatFormatting.RED) ) CobbleSurvival.LOGGER.info("Permadeath: {} of {} fainted and was removed", name.string, player.gameProfile.name) } } catch (e: Exception) { CobbleSurvival.LOGGER.error("Permadeath: could not settle a fainted Pokémon of {}", player.gameProfile.name, e) } } }