WIP: ore nodes, catching rules, bee nest fix, smaller Deep Dark

Done and checked: surface and cave-floor ore nodes (companion mod
ore/OreNodes.kt), bigger and rarer underground veins; template with nodes
passed the world check and the island quick check (node self-tests).

Built, not yet tested: catching rules (catch rate falls with the level
gap to the trainer's strongest Pokemon, failed catch enrages, strong wild
Pokemon hunt weaker trainers; catching/, Fight or Flight config), bee
nests emptied when their bee is turned away.

In progress: the Deep Dark no longer fills the deepslate layer (split by
temperature: cold = Deep Dark, hot = basalt deltas, warm = ordinary caves;
world/biomes.json), zone layout regenerated; cave zone types added to
world/zones.json but tools/zone_spawns.py doesn't use them yet; template
not rebuilt for the new caves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-30 15:44:17 +01:00
1 parent 362d06aa4b
commit 6361636da7
29 files changed
+1733 -357

No files matched your search

@@ -0,0 +1,90 @@
package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.api.events.CobblemonEvents
import com.cobblemon.mod.common.api.events.pokeball.PokeBallCaptureCalculatedEvent
import com.cobblemon.mod.common.api.events.pokeball.PokemonCatchRateEvent
import com.cobblemon.mod.common.api.pokeball.catching.CaptureContext
import com.cobblemon.mod.common.entity.pokeball.EmptyPokeBallEntity
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import dev.cobblesurvival.catching.Catching
import dev.cobblesurvival.catching.FightOrFlight
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import kotlin.math.abs
/**
* Catching (Adrian, 2026-10-30), against the trainer's strongest party Pokémon: the catch rate falls with the level
* gap (20+ levels above: very improbable), a failed catch makes the Pokémon attack the thrower, and a wild Pokémon
* above a nearby trainer's strongest is hostile to them. The hostility cases run in batches of their own, with the
* scan switched on only there (other tests put weak trainers next to wild Pokémon).
*/
object CatchingTests {
fun all() = listOf(
SelfTestCase("catch_rate_falls_with_level_gap", 20, ::catchRate, batch = "catching"),
SelfTestCase("failed_catch_enrages", 400, ::failedCatch, batch = "catching"),
SelfTestCase("strong_wild_pokemon_hunt_weak_trainers", 400, { s -> hostility(s, strongest = 5, hostile = true) },
batch = "catching_weak"),
SelfTestCase("wild_pokemon_ignore_stronger_trainers", 120, { s -> hostility(s, strongest = 50, hostile = false) },
batch = "catching_strong"),
)
private const val WILD_LEVEL = 30
private fun catchRate(s: Scenario) {
val player = s.player()
s.give(player, s.pokemon("pidgey", 10))
s.check(Catching.catchFactor(20) <= 0.05) { "20 levels above still keeps ${Catching.catchFactor(20)} of the catch rate" }
for (level in listOf(10, 15, 20, 30, 45)) {
val wild = PokemonEntity(s.level, s.pokemon("rattata", level))
val event = PokemonCatchRateEvent(player, EmptyPokeBallEntity(s.level), wild, 100f)
CobblemonEvents.POKEMON_CATCH_RATE.post(event)
val expected = 100.0 * Catching.catchFactor(level - 10)
s.check(abs(event.catchRate - expected) < 0.01) { "Lv $level against Lv 10: catch rate ${event.catchRate}, expected $expected" }
}
s.helper.succeed()
}
private fun failedCatch(s: Scenario) {
val player = s.player()
s.give(player, s.pokemon("pidgeot", 60)) // stronger than the wild one: only the failed catch angers it
val wild = s.wild(s.pokemon("bidoof", 20), s.at(7.5, 1.0, 7.5))
s.helper.startSequence()
.thenWaitUntil { s.check(wild.pokemon.entity === wild) { "the wild Pokémon hasn't ticked yet" } }
.thenExecute {
CobblemonEvents.POKE_BALL_CAPTURE_CALCULATED.post(
PokeBallCaptureCalculatedEvent(player, wild, EmptyPokeBallEntity(s.level), CaptureContext(1, false, false))
)
s.check(Catching.ENRAGED in wild.pokemon.aspects) { "a failed catch didn't anger the Pokémon" }
val aggression = FightOrFlight.aggression(wild)
s.check(aggression >= FightOrFlight.neutralThreshold()) { "the angry Pokémon would flee (aggression $aggression)" }
FightOrFlight.setTarget(wild, player) // what Fight or Flight does when the ball breaks open
}
.thenWaitUntil { checkTarget(s, wild, player) }
.thenSucceed()
}
private fun hostility(s: Scenario, strongest: Int, hostile: Boolean) {
Catching.hostilityPaused = false
s.afterTest { Catching.hostilityPaused = true }
val player = s.player()
s.give(player, s.pokemon("pidgey", strongest))
val wild = s.wild(s.pokemon("machop", WILD_LEVEL), s.at(9.5, 1.0, 9.5))
val sequence = s.helper.startSequence()
.thenWaitUntil { s.check(wild.pokemon.entity === wild) { "the wild Pokémon hasn't ticked yet" } }
if (hostile) {
sequence.thenWaitUntil {
s.check(Catching.ENRAGED in wild.pokemon.aspects) { "a Lv $WILD_LEVEL wild Pokémon isn't angry at a trainer whose best is Lv $strongest" }
checkTarget(s, wild, player)
}.thenSucceed()
} else {
sequence.thenIdle(80).thenExecute {
s.check(Catching.ENRAGED !in wild.pokemon.aspects) { "a Lv $WILD_LEVEL wild Pokémon got angry at a trainer with a Lv $strongest" }
}.thenSucceed()
}
}
private fun checkTarget(s: Scenario, wild: PokemonEntity, player: ServerPlayer) {
val target = wild.brain.getMemory(MemoryModuleType.ATTACK_TARGET).orElse(null)
s.check(target === player) { "the angry Pokémon targets ${target?.name?.string ?: "nobody"}, not the player" }
}
}
@@ -0,0 +1,54 @@
package dev.cobblesurvival.selftest
import dev.cobblesurvival.dev.OreVeins
import dev.cobblesurvival.ore.OreNodes
import dev.cobblesurvival.zone.Zones
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.world.level.ChunkPos
/**
* Ore nodes on the island (Adrian, 2026-10-30): a surface node is a small lump of ore in the ground of its zone with
* open air above it; a cave node lies on the floor of its cave biome, touching the cave's air, not buried in a wall.
* Each test places one node on purpose near its zone's sample point and checks it as Regenerating Ore Veins tracks it.
*/
object OreNodeTests {
fun all() = listOf(
SelfTestCase("surface_nodes_lie_in_the_ground", 20, { s -> nodeTest(s, "button_isle", cave = false) }, islandOnly = true),
SelfTestCase("cave_nodes_lie_on_the_cave_floor", 20, { s -> nodeTest(s, "deep_dark", cave = true) }, islandOnly = true),
)
private fun nodeTest(s: Scenario, zone: String, cave: Boolean) {
val level = s.level
val node = OreNodes.nodes.firstOrNull { it.zone == zone && it.cave == cave }
?: throw GameTestAssertException("no ${if (cave) "cave" else "surface"} node kind in $zone")
val sample = Zones.samples[zone] ?: throw GameTestAssertException("no sample point for $zone")
val home = ChunkPos(BlockPos(sample.x, 0, sample.z))
// Next to the sample chunk first (the guardian stands on the sample point), then around it.
val chunks = listOf(ChunkPos(home.x + 2, home.z)) + (-1..1).flatMap { dx -> (-1..1).map { dz -> ChunkPos(home.x + dx, home.z + dz) } }
val grounds = chunks.asSequence()
.onEach { level.getChunk(it.x, it.z) }
.map { OreNodes.fill(level, it, only = node, force = true) }
.firstOrNull { it.isNotEmpty() }
?: throw GameTestAssertException("no spot for a ${node.vein} near $zone's sample point")
for (ground in grounds) {
val blocks = BlockPos.betweenClosed(ground.offset(-node.radius - 1, -node.radius - 1, -node.radius - 1),
ground.offset(node.radius + 1, node.radius + 1, node.radius + 1))
.map { it.immutable() }
.filter { OreVeins.veinAt(level, it) == node.vein }
s.check(blocks.isNotEmpty()) { "the ${node.vein} at ${ground.toShortString()} isn't tracked by Regenerating Ore Veins" }
val exposed = blocks.any { block -> Direction.entries.any { level.getBlockState(block.relative(it)).isAir } }
s.check(exposed) { "the ${node.vein} at ${ground.toShortString()} touches no air: it's buried" }
if (cave) {
s.check(level.getBiome(ground.above()).`is`(node.biome!!)) { "the ${node.vein} at ${ground.toShortString()} isn't in ${node.biome}" }
} else {
val top = blocks.maxBy { it.y }
s.check(level.canSeeSky(top.above())) { "the ${node.vein} at ${ground.toShortString()} is under cover" }
s.check(blocks.all { Zones.at(level, it)?.id == zone }) { "the ${node.vein} at ${ground.toShortString()} reaches out of $zone" }
}
}
s.helper.succeed()
}
}
@@ -2,6 +2,7 @@ package dev.cobblesurvival.selftest
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.catching.Catching
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.core.BlockPos
@@ -51,7 +52,8 @@ object SelfTest {
private val cases: List<SelfTestCase> get() =
PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all() + GuardianTests.all() +
VeinTests.all() + DropTests.all() + ProgressionTests.all()
VeinTests.all() + OreNodeTests.all() + DropTests.all() + ProgressionTests.all() +
CatchingTests.all()
private var running: List<Scenario> = emptyList()
private var forced: List<ChunkPos> = emptyList()
@@ -99,6 +101,7 @@ object SelfTest {
GameTestInfo(function, Rotation.NONE, level, RetryOptions.noRetries()).also { it.addListener(scenario) }
}
running = scenarios
Catching.hostilityPaused = true
GameTestTicker.SINGLETON.clear()
GameTestRunner.Builder.fromInfo(infos, level)
.newStructureSpawner(StructureGridSpawner(origin, TESTS_PER_ROW, false))
@@ -128,5 +131,6 @@ object SelfTest {
val level = event.server.overworld()
forced.forEach { level.setChunkForced(it.x, it.z, false) }
running = emptyList()
Catching.hostilityPaused = false
}
}