Cobblemon Survival Pack: phases 0-2 and Phase 3 zones and hazards

Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus
the Kotlin companion mod (cobblesurvival) and its built-in data pack.

- Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and
  export tooling, Prism instance sync, test server with smoke checks.
- Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26),
  Adrian's biome list, no natural ore, structures without ore or vanilla
  mobs, no Nether portals, world template builder and world report.
- Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap
  fixes), Revival Charm, expensive Healing Machine, Lootr refreshing
  loot, in-game self-tests on Minecraft's GameTest framework.
- Phase 3 so far: 14 zones with their own spawn pools and level bands
  (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition),
  Cold Sweat heat and cold as gates with boss-drop insulation recipes,
  spawn survey and hazard self-tests in tools/quick_check.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-29 19:01:21 +01:00
commit 398301f0be
232 files changed
+72665

No files matched your search

@@ -0,0 +1,117 @@
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.
*/
object Permadeath {
val REVIVAL_CHARMS: TagKey<Item> =
TagKey.create(Registries.ITEM, ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "revival_charms"))
private const val SWEEP_TICKS = 20
private val fainted = ConcurrentLinkedQueue<Pokemon>()
private val faintedInBattle = HashMap<UUID, MutableSet<Pokemon>>()
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)
}
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() || (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)
}
}
}