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