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