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,170 @@
package dev.cobblesurvival.selftest
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.zone.ZoneSample
import dev.cobblesurvival.zone.Zones
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.EquipmentSlot
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.levelgen.Heightmap
/**
* Roadmap Phase 3, hazards as gates, on the island itself (real biomes, real altitude): in Frostpeak and Sunscorch
* a player without the right gear freezes or overheats within a minute or two, while the fur (cold) or hide (heat)
* set holds them off for many minutes; leather armour alone doesn't help. The zones without a hazard are livable
* without gear by day and by night.
*
* Cold Sweat's insulation doesn't change what counts as too cold or too hot; it slows how fast the body drifts
* there. So the tests reset each player's core temperature to 0, wait, and turn the drift into minutes until the
* body reaches ±100, where Cold Sweat starts to hurt. Each case changes the time of day, so it runs in a batch of
* its own.
*/
object HazardTests {
private val FUR = listOf("goat_fur_helmet", "goat_fur_chestplate", "goat_fur_leggings", "goat_fur_boots")
private val HIDE = listOf("hoglin_helmet", "hoglin_chestplate", "hoglin_leggings", "hoglin_boots")
private val LEATHER = listOf("leather_helmet", "leather_chestplate", "leather_leggings", "leather_boots")
private const val NOON = 6000L
private const val MIDNIGHT = 18000L
private const val MEASURE_TICKS = 60
/** Cold Sweat eases the felt temperature towards a new place; let it arrive before measuring. */
private const val SETTLE_TICKS = 120
/** Without gear the hazard must hurt within this many minutes... */
private const val UNPROTECTED_MAX_MINUTES = 3.0
/** ...and with the right set not before this many. */
private const val PROTECTED_MIN_MINUTES = 10.0
fun all() = listOf(
SelfTestCase("cold_needs_fur", 600, ::cold, islandOnly = true, batch = "hazard_cold"),
SelfTestCase("heat_needs_hide", 600, ::heat, islandOnly = true, batch = "hazard_heat"),
SelfTestCase("lowlands_are_mild", 4000, ::lowlands, islandOnly = true, batch = "hazard_lowlands"),
)
/** Frostpeak at noon, its warmest hour. */
private fun cold(s: Scenario) {
val bare = s.player()
val leather = s.player().also { dress(it, "minecraft", LEATHER) }
val fur = s.player().also { dress(it, "cold_sweat", FUR) }
val players = listOf(bare, leather, fur)
s.helper.startSequence()
.thenExecute { exposeAt(s, Zones.biomeSamples["frostpeak"], NOON, players) }
.thenIdle(SETTLE_TICKS)
.thenExecute { players.forEach { ColdSweat.setCore(it, 0.0) } }
.thenIdle(MEASURE_TICKS)
.thenExecute {
val (bareMin, leatherMin, furMin) = players.map { ColdSweat.minutesToLimit(it) }
report(s, "Frostpeak at noon (%s), minutes until the body freezes: none %.1f, leather %.1f, fur %.1f".format(ColdSweat.describe(bare), bareMin, leatherMin, furMin))
s.check(bareMin <= UNPROTECTED_MAX_MINUTES) { "without gear Frostpeak takes %.1f min to freeze".format(bareMin) }
s.check(leatherMin <= UNPROTECTED_MAX_MINUTES) { "in leather Frostpeak takes %.1f min to freeze".format(leatherMin) }
s.check(furMin >= PROTECTED_MIN_MINUTES) { "in the fur set Frostpeak freezes in %.1f min".format(furMin) }
// And it really hurts: a frozen body without the new-player grace takes damage.
ColdSweat.endGrace(bare)
ColdSweat.setCore(bare, -150.0)
}
.thenExecuteAfter(60) {
s.check(bare.health < bare.maxHealth) { "a frozen player took no damage (health ${bare.health})" }
}
.thenSucceed()
}
/** Sunscorch at noon, its hottest hour. */
private fun heat(s: Scenario) {
val bare = s.player()
val hide = s.player().also { dress(it, "cold_sweat", HIDE) }
val players = listOf(bare, hide)
s.helper.startSequence()
.thenExecute { exposeAt(s, Zones.biomeSamples["sunscorch"], NOON, players) }
.thenIdle(SETTLE_TICKS)
.thenExecute { players.forEach { ColdSweat.setCore(it, 0.0) } }
.thenIdle(MEASURE_TICKS)
.thenExecute {
val (bareMin, hideMin) = players.map { ColdSweat.minutesToLimit(it) }
report(s, "Sunscorch at noon (%s), minutes until the body overheats: none %.1f, hide %.1f".format(ColdSweat.describe(bare), bareMin, hideMin))
s.check(bareMin <= UNPROTECTED_MAX_MINUTES) { "without gear Sunscorch takes %.1f min to overheat".format(bareMin) }
s.check(hideMin >= PROTECTED_MIN_MINUTES) { "in the hide set Sunscorch overheats in %.1f min".format(hideMin) }
}
.thenSucceed()
}
/** Every zone without a hazard, at noon and at midnight, without gear. */
private fun lowlands(s: Scenario) {
val player = s.player()
val problems = mutableListOf<String>()
val readings = mutableListOf<String>()
var sequence = s.helper.startSequence()
// Nights are the cold risk everywhere; the warm zones are checked at noon too.
val checks = listOf("button_isle", "southwold", "westmarch", "jungle", "highlands", "mire").map { it to MIDNIGHT } +
listOf("jungle" to NOON, "mire" to NOON)
for ((zone, time) in checks) {
run {
val label = "$zone at ${if (time == NOON) "noon" else "midnight"}"
sequence = sequence
.thenExecute { exposeAt(s, Zones.samples[zone], time, listOf(player)) }
.thenIdle(SETTLE_TICKS)
.thenExecute { ColdSweat.setCore(player, 0.0) }
.thenIdle(MEASURE_TICKS)
.thenExecute {
val minutes = ColdSweat.minutesToLimit(player)
readings += "$label %.0f (world %.2f)".format(minutes, ColdSweat.get(player, "WORLD"))
if (minutes < PROTECTED_MIN_MINUTES) problems += "$label: limit in %.1f min (%s)".format(minutes, ColdSweat.describe(player))
}
}
}
sequence.thenExecute {
report(s, "minutes until too hot or cold without gear: ${readings.joinToString()}")
s.check(problems.isEmpty()) { problems.joinToString("; ") }
}.thenSucceed()
}
private fun dress(player: ServerPlayer, namespace: String, pieces: List<String>) {
val slots = listOf(EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET)
for ((slot, piece) in slots.zip(pieces)) {
player.setItemSlot(slot, ItemStack(BuiltInRegistries.ITEM.get(ResourceLocation.fromNamespaceAndPath(namespace, piece))))
}
}
/** Stands the players on the surface at the sample point at that time of day. */
private fun exposeAt(s: Scenario, sample: ZoneSample?, time: Long, players: List<ServerPlayer>) {
if (sample == null) throw GameTestAssertException("no sample point (run tools/zone_layout.py)")
val level = s.level
for (dx in -2..2) for (dz in -2..2) level.getChunk((sample.x shr 4) + dx, (sample.z shr 4) + dz)
val y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z)
level.dayTime = time
for (player in players) {
s.moveTo(player, sample.x + 0.5, y.toDouble(), sample.z + 0.5)
}
}
private fun report(s: Scenario, text: String) = CobbleSurvival.LOGGER.info("selftest {}: {}", s.case.name, text)
/** Cold Sweat's temperature API, by reflection: the companion mod doesn't compile against Cold Sweat. */
private object ColdSweat {
private val api = Class.forName("com.momosoftworks.coldsweat.api.util.Temperature")
private val traits = Class.forName("com.momosoftworks.coldsweat.api.util.Temperature\$Trait")
private val get = api.getMethod("get", LivingEntity::class.java, traits)
private val set = api.getMethod("set", LivingEntity::class.java, traits, Double::class.javaPrimitiveType)
private fun trait(name: String) = traits.getField(name).get(null)
fun get(entity: LivingEntity, name: String) = get.invoke(null, entity, trait(name)) as Double
fun setCore(player: ServerPlayer, value: Double) { set.invoke(null, player, trait("CORE"), value) }
/** Minutes from now until the core reaches ±100 at the drift since it was reset to 0. */
fun minutesToLimit(player: ServerPlayer): Double {
val perTick = kotlin.math.abs(get(player, "CORE")) / MEASURE_TICKS
return if (perTick == 0.0) Double.POSITIVE_INFINITY else 100.0 / perTick / 20.0 / 60.0
}
fun endGrace(player: ServerPlayer) {
BuiltInRegistries.MOB_EFFECT.getHolder(ResourceLocation.fromNamespaceAndPath("cold_sweat", "grace"))
.ifPresent { player.removeEffect(it) }
}
fun describe(player: ServerPlayer) = "world %.2f, comfortable %.2f..%.2f, core %.1f".format(
get(player, "WORLD"), get(player, "FREEZING_POINT"), get(player, "BURNING_POINT"), get(player, "CORE"),
)
}
}