Phase 5: field moves from Cobblemon HMs

Adrian (2026-10-31): use Cobblemon HMs; mining ore is the one hard
requirement, furnaces get lit by a Pokemon inside a Phase 6 machine.

- Cobblemon HMs 4.1.0 in the pack (Modrinth), default config.
- Self-test rock_smash_mines_veins: HMs' Rock Smash breaks a
  regenerating vein, the ore drops and the vein becomes a regenerator.
- HMs' Trainer Ditto (a wild Ditto underneath) is exempt from the
  hostility scan and the stray despawn (world/SpecialPokemon.kt).
- Docs: licenses, addon status, design, balance, roadmap note,
  Phase 5 checklist with the open questions for Adrian.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-31 03:00:07 +01:00
1 parent ea21bb725a
commit 56ffdd9f2e
14 files changed
+145 -6

No files matched your search

@@ -9,6 +9,7 @@ import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.google.gson.Gson
import com.google.gson.JsonElement
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.world.SpecialPokemon
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.resources.ResourceManager
@@ -23,7 +24,8 @@ import net.neoforged.neoforge.event.tick.ServerTickEvent
* strongest party Pokémon:
* - the catch rate falls with the level gap, down to a small share at 20 levels above ("very improbable");
* - a Pokémon that breaks out of a ball attacks the thrower instead of fleeing;
* - a wild Pokémon above a nearby player's strongest Pokémon is hostile to that player.
* - a wild Pokémon above a nearby player's strongest Pokémon is hostile to that player (not guardians and other
* special Pokémon, see [SpecialPokemon]).
*
* The attacking is Fight or Flight Reborn's: an angry Pokémon carries the aspect [ENRAGED], which the pack's Fight or
* Flight config lists in `always_aggro_aspects` (so it fights rather than flees), and its target is set through Fight
@@ -96,7 +98,7 @@ object Catching : SimpleJsonResourceReloadListener(Gson(), "catching") {
val strongest = strongest(player)
val near = player.boundingBox.inflate(rules.hostileRange)
player.serverLevel().getEntitiesOfClass(PokemonEntity::class.java, near) {
it.pokemon.isWild() && !it.isBattling && it.pokemon.level > strongest
it.pokemon.isWild() && !it.isBattling && it.pokemon.level > strongest && !SpecialPokemon.isExempt(it.pokemon)
}.forEach { enrage(it, player) }
}
}
@@ -0,0 +1,62 @@
package dev.cobblesurvival.selftest
import dev.cobblesurvival.dev.OreVeins
import net.minecraft.commands.arguments.EntityAnchorArgument
import net.minecraft.core.BlockPos
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.RandomSource
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.item.Items
import net.minecraft.world.level.GameType
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
/**
* Phase 5 field moves come from Cobblemon HMs (Adrian, 2026-10-31); mining ore is the one hard requirement. Rock Smash
* must mine a regenerating vein the normal way: the ore drops and Regenerating Ore Veins turns the block into a
* regenerator, so the vein grows back. The test fires HMs' own Rock Smash handler (by reflection) at a one-block
* coal vein, away from spawn, since HMs respects spawn protection.
*/
object FieldMoveTests {
private const val VEIN = "selftest"
private const val OFFSET = 64
private val REGENERATOR = ResourceLocation.parse("regenerating_ore_veins:regenerator_block")
fun all() = listOf(SelfTestCase("rock_smash_mines_veins", 100, ::rockSmashMinesVeins))
private fun rockSmashMinesVeins(s: Scenario) {
val level = s.level
val pos = level.sharedSpawnPos.let { BlockPos(it.x + OFFSET, SelfTest.PLATFORM_Y, it.z) }
val player = s.player()
val start = player.position()
s.helper.startSequence()
.thenExecute {
s.moveTo(player, pos.x + 0.5, pos.y - 1.0, pos.z + 3.5) // forces the chunks around the vein
s.check(OreVeins.place(level, pos, VEIN, RandomSource.create(1L)) > 0) { "the test vein was not placed" }
s.check(level.getBlockState(pos).`is`(Blocks.COAL_ORE)) { "the test vein is ${level.getBlockState(pos)}" }
player.lookAt(EntityAnchorArgument.Anchor.EYES, Vec3.atCenterOf(pos))
s.check(rockSmash(player)) { "Rock Smash refused to break the ore" }
player.teleportTo(start.x, start.y, start.z)
}
.thenWaitUntil {
val block = BuiltInRegistries.BLOCK.getKey(level.getBlockState(pos).block)
s.check(block == REGENERATOR) { "the mined vein block is $block, not a regenerator: it won't grow back" }
val drops = level.getEntitiesOfClass(ItemEntity::class.java, AABB(pos).inflate(4.0)) { it.item.`is`(Items.COAL) }
s.check(drops.isNotEmpty()) { "Rock Smash dropped no coal" }
}
.thenExecute {
level.getEntitiesOfClass(ItemEntity::class.java, AABB(pos).inflate(4.0)).forEach { it.discard() }
player.setGameMode(GameType.CREATIVE) // breaking a regenerator in creative removes the vein for good
player.gameMode.destroyBlock(pos)
}
.thenSucceed()
}
private fun rockSmash(player: ServerPlayer): Boolean {
val handler = Class.forName("com.shier.cobblemonhms.ability.handlers.RockSmashHandler")
return handler.getMethod("execute", ServerPlayer::class.java).invoke(handler.getField("INSTANCE").get(null), player) as Boolean
}
}
@@ -54,7 +54,7 @@ object SelfTest {
private val cases: List<SelfTestCase> get() =
PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all() + GuardianTests.all() +
VeinTests.all() + OreNodeTests.all() + DropTests.all() + ProgressionTests.all() +
CatchingTests.all() + WorldRulesTests.all()
CatchingTests.all() + WorldRulesTests.all() + FieldMoveTests.all()
private var running: List<Scenario> = emptyList()
private var forced: List<ChunkPos> = emptyList()
@@ -0,0 +1,17 @@
package dev.cobblesurvival.world
import com.cobblemon.mod.common.pokemon.Pokemon
import dev.cobblesurvival.zone.Guardians
/**
* Wild Pokémon that the island's rules for wild Pokémon (strays despawning, hunting weaker trainers) leave alone:
* guardians, anything uncatchable, and other mods' characters that are Pokémon underneath, such as Cobblemon HMs'
* visiting Trainer Ditto, which sells TMs.
*/
object SpecialPokemon {
/** Cobblemon HMs marks its Trainer Ditto with this in the Pokémon's persistent data. */
private const val HMS_TRAINER = "dittohms_trainer"
fun isExempt(pokemon: Pokemon): Boolean =
pokemon.isUncatchable() || pokemon.persistentData.contains(Guardians.TAG) || pokemon.persistentData.contains(HMS_TRAINER)
}
@@ -1,6 +1,7 @@
package dev.cobblesurvival.zone
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import dev.cobblesurvival.world.SpecialPokemon
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.entity.ai.memory.MemoryModuleType
import net.neoforged.neoforge.common.NeoForge
@@ -11,7 +12,8 @@ import java.util.UUID
* Wild Pokémon keep to their zone's levels (Adrian, 2026-10-30: a Lv 61 Pokémon from the Basalt Furnace turned up in the
* Lush Caves, Lv 12–28). Spawns follow the zones, but wild Pokémon wander and cave zones touch, so every few seconds a
* wild Pokémon near a player that stands in a zone whose band it tops by more than [Zones.wanderGrace] levels
* despawns, unless it is in a battle, angry at someone (it is chasing them) or a guardian. Grace: world/zones.json →
* despawns, unless it is in a battle, angry at someone (it is chasing them), a guardian or another special Pokémon
* ([SpecialPokemon]). Grace: world/zones.json →
* `wanderers`.
*/
object Wanderers {
@@ -38,7 +40,7 @@ object Wanderers {
}
fun strayed(level: ServerLevel, wild: PokemonEntity, grace: Int): Boolean {
if (wild.isBattling || wild.pokemon.persistentData.contains(Guardians.TAG)) return false
if (wild.isBattling || SpecialPokemon.isExempt(wild.pokemon)) return false
if (wild.brain.hasMemoryValue(MemoryModuleType.ATTACK_TARGET)) return false
val zone = Zones.at(level, wild.blockPosition()) ?: return false
return wild.pokemon.level > zone.maxLevel + grace