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:
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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user