Phase 3: the Mangrove Mire poisons anyone without Mire Waders
CI / build-and-smoke-test (push) Failing after 30m44s

- Companion mod zone hazards (zone/ZoneHazards.kt, data in
  data/cobblesurvival/zone_hazards/): in the Mire a player without an
  item from #cobblesurvival:mire_protection gets Poison every 5 seconds,
  with a one-time warning. Potions, milk and golden apples still help.
- Mire Waders: KubeJS leather boots, dropped by the Westmarch and Verdant
  Jungle guardians (the zones before the Mire), per Adrian's boss rule.
- Self-test mire_poisons_without_waders.
- Allow-list Cobblemon's misspelt jigsaw state in
  habitats/parched_peat_bog7.nbt, which test players now reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-30 01:46:16 +01:00
1 parent 73fe71918f
commit d105f6757d
19 files changed
+197 -15

No files matched your search

@@ -9,6 +9,7 @@ import dev.cobblesurvival.selftest.SelfTest
import dev.cobblesurvival.world.OnlyPokemon
import dev.cobblesurvival.zone.Guardians
import dev.cobblesurvival.zone.ZoneCondition
import dev.cobblesurvival.zone.ZoneHazards
import dev.cobblesurvival.zone.Zones
import net.minecraft.commands.Commands
import net.minecraft.core.BlockPos
@@ -57,6 +58,7 @@ class CobbleSurvival {
ZoneCondition.register()
NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners)
Guardians.register()
ZoneHazards.register()
SelfTest.register()
}
@@ -3,20 +3,22 @@ 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.gametest.framework.GameTestAssertException
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.effect.MobEffects
import net.minecraft.world.entity.EquipmentSlot
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
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.
* without gear by day and by night. The Mangrove Mire poisons anyone without Mire Waders.
*
* 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
@@ -42,6 +44,7 @@ object HazardTests {
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"),
SelfTestCase("mire_poisons_without_waders", 400, ::mire),
)
/** Frostpeak at noon, its warmest hour. */
@@ -120,6 +123,35 @@ object HazardTests {
}.thenSucceed()
}
/** The Mire's fumes (companion mod zone hazard) poison a player without Mire Waders, and nobody outside it. */
private fun mire(s: Scenario) {
val level = s.level
val mire = Zones.samples["mire"] ?: throw GameTestAssertException("no sample point for the Mire")
val elsewhere = Zones.samples["button_isle"] ?: throw GameTestAssertException("no sample point for Button Isle")
val waders = BuiltInRegistries.ITEM.get(ResourceLocation.fromNamespaceAndPath("cobblesurvival", "mire_waders"))
s.check(waders != Items.AIR) { "the Mire Waders item does not exist" }
val bare = s.player()
val wading = s.player().also { it.setItemSlot(EquipmentSlot.FEET, ItemStack(waders)) }
val outside = s.player()
fun surface(sample: ZoneSample): Double {
level.getChunk(sample.x shr 4, sample.z shr 4)
return level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z).toDouble()
}
s.helper.startSequence()
.thenExecute {
for (player in listOf(bare, wading)) s.moveTo(player, mire.x + 0.5, surface(mire), mire.z + 0.5)
s.moveTo(outside, elsewhere.x + 0.5, surface(elsewhere), elsewhere.z + 0.5)
}
.thenIdle(220)
.thenExecute {
val poison = MobEffects.POISON
s.check(bare.hasEffect(poison)) { "a player without waders in the Mire isn't poisoned" }
s.check(!wading.hasEffect(poison)) { "a player in Mire Waders got poisoned" }
s.check(!outside.hasEffect(poison)) { "a player on Button Isle got poisoned" }
}
.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)) {
@@ -0,0 +1,97 @@
package dev.cobblesurvival.zone
import com.google.gson.Gson
import com.google.gson.JsonElement
import dev.cobblesurvival.CobbleSurvival
import net.minecraft.ChatFormatting
import net.minecraft.core.Holder
import net.minecraft.core.registries.BuiltInRegistries
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.server.packs.resources.ResourceManager
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
import net.minecraft.tags.TagKey
import net.minecraft.util.profiling.ProfilerFiller
import net.minecraft.world.effect.MobEffect
import net.minecraft.world.effect.MobEffectInstance
import net.minecraft.world.entity.EquipmentSlot
import net.minecraft.world.item.Item
import net.neoforged.neoforge.common.NeoForge
import net.neoforged.neoforge.event.AddReloadListenerEvent
import net.neoforged.neoforge.event.tick.ServerTickEvent
import java.util.UUID
/**
* Zone hazards that Cold Sweat doesn't cover (roadmap Phase 3: "a poison swamp ... as area effects"): while a player
* is in the zone and wears nothing from the protection tag, they keep getting the effect. Potions, milk and golden
* apples still work against it. Definitions: `data/cobblesurvival/zone_hazards/<name>.json`.
*/
object ZoneHazards : SimpleJsonResourceReloadListener(Gson(), "zone_hazards") {
class Hazard(
val zone: String,
val effect: Holder<MobEffect>,
val amplifier: Int,
val durationTicks: Int,
val everyTicks: Int,
val protection: TagKey<Item>,
val warning: String?,
)
var hazards: List<Hazard> = emptyList()
private set
private val warned = HashMap<UUID, String>()
fun register() {
NeoForge.EVENT_BUS.addListener<AddReloadListenerEvent> { it.addListener(this) }
NeoForge.EVENT_BUS.addListener(::onServerTick)
}
override fun apply(files: Map<ResourceLocation, JsonElement>, resources: ResourceManager, profiler: ProfilerFiller) {
hazards = files.mapNotNull { (id, element) ->
val json = element.asJsonObject
val effectId = ResourceLocation.parse(json.get("effect").asString)
val effect = BuiltInRegistries.MOB_EFFECT.getHolder(effectId).orElse(null)
if (effect == null) {
CobbleSurvival.LOGGER.error("Zone hazard {}: unknown effect {}", id, effectId)
return@mapNotNull null
}
Hazard(
zone = json.get("zone").asString,
effect = effect,
amplifier = json.get("amplifier")?.asInt ?: 0,
durationTicks = json.get("duration_ticks").asInt,
everyTicks = json.get("every_ticks").asInt,
protection = TagKey.create(Registries.ITEM, ResourceLocation.parse(json.get("protection").asString)),
warning = json.get("warning")?.asString,
)
}
CobbleSurvival.LOGGER.info("Loaded {} zone hazards", hazards.size)
}
private fun onServerTick(event: ServerTickEvent.Post) {
if (hazards.isEmpty()) return
val tick = event.server.tickCount
for (player in event.server.playerList.players) {
if (player.isCreative || player.isSpectator) continue
val zone = Zones.at(player.serverLevel(), player.blockPosition())?.id
if (zone == null || hazards.none { it.zone == zone }) {
warned.remove(player.uuid)
continue
}
for (hazard in hazards) {
if (hazard.zone != zone || tick % hazard.everyTicks != 0 || isProtected(player, hazard)) continue
player.addEffect(MobEffectInstance(hazard.effect, hazard.durationTicks, hazard.amplifier))
if (hazard.warning != null && warned[player.uuid] != zone) {
warned[player.uuid] = zone
player.displayClientMessage(Component.translatable(hazard.warning).withStyle(ChatFormatting.DARK_GREEN), true)
}
}
}
}
fun isProtected(player: ServerPlayer, hazard: Hazard) =
listOf(EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET)
.any { player.getItemBySlot(it).`is`(hazard.protection) }
}