Phase 4: Cobblebase and Poke Power, gated behind progression

Adrian (2026-10-30): Cobblebase's skills stay in but gated behind
progression, power from an existing mod (Poke Power) also gated,
evolution stones as ores fine for now, balance fine and configurable.

Progression is the zones whose Alpha guardian a player helped beat
(Progress saved data; a guardian win records it and lists the unlocks).
The gate table is data (progression_gates/base.json): each of
Cobblebase's 44 skills needs a zone (or none), the Power Block needs the
Verdant Jungle. The companion mod wraps every Cobblebase skill executor
in a gate by reflection; a skill missing from the table stays off.
Operator command /cobblesurvival progress. Cobblebase's and Poke Power's
configs ship in the pack. Self-tests: gate coverage, a locked skill only
runs after its guardian, the Power Block waits for the Jungle guardian,
a guardian win counts as progress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-30 14:00:50 +01:00
1 parent 1a1fb93da8
commit 362d06aa4b
22 files changed
+618 -20

No files matched your search

@@ -4,6 +4,7 @@ import com.cobblemon.mod.common.battles.BattleBuilder
import com.cobblemon.mod.common.battles.MoveActionResponse
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isInBattle
import dev.cobblesurvival.progress.Progress
import dev.cobblesurvival.zone.Guardians
import dev.cobblesurvival.zone.Zones
import net.minecraft.core.registries.BuiltInRegistries
@@ -14,7 +15,7 @@ import kotlin.math.abs
/**
* Roadmap Phase 3, Alpha guardians (Adrian: fixed spots, back 5 hours after being beaten). On Button Isle's spot:
* a player arriving makes the guardian appear, big and uncatchable at its level; beating it in a battle gives the
* player its drops; it stays away, and returns once its time is up. Runs in a batch of its own, since the guardian
* player its drops and the zone as progress (which unlocks base skills); it stays away, and returns once its time is up. Runs in a batch of its own, since the guardian
* state is shared.
*/
object GuardianTests {
@@ -61,6 +62,7 @@ object GuardianTests {
val have = player.inventory.countItem(BuiltInRegistries.ITEM.get(item))
s.check(have >= count) { "the player got $have of $item from the guardian, not $count" }
}
s.check(ZONE in Progress.of(level.server).beaten(player.uuid)) { "beating the guardian didn't count as progress" }
}
.thenIdle(100)
.thenExecute {
@@ -0,0 +1,111 @@
package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import dev.cobblesurvival.progress.CobblebaseGate
import dev.cobblesurvival.progress.Gates
import dev.cobblesurvival.progress.Progress
import dev.cobblesurvival.zone.Guardians
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.InteractionHand
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.Vec3
import java.lang.reflect.Proxy
/**
* Progression gates (Adrian, 2026-10-30): Cobblebase's base skills and Poke Power's Power Block open up as a player
* beats the zones' Alpha guardians (`data/cobblesurvival/progression_gates/`).
*/
object ProgressionTests {
private val POWER_BLOCK = ResourceLocation.parse("poke_power:power_block")
fun all() = listOf(
SelfTestCase("gates_cover_every_cobblebase_skill", 20, ::gatesCoverEverySkill),
SelfTestCase("cobblebase_skills_wait_for_guardians", 20, ::skillsWaitForGuardians),
SelfTestCase("power_block_waits_for_guardian", 20, ::powerBlockWaits),
)
private fun gatesCoverEverySkill(s: Scenario) {
s.check(CobblebaseGate.installed) { "Cobblebase isn't installed" }
val skills = CobblebaseGate.skills().keys
val ungated = skills - Gates.skills.keys
s.check(ungated.isEmpty()) { "Cobblebase skills without a gate (they stay off): ${ungated.sorted()}" }
val stale = Gates.skills.keys - skills
s.check(stale.isEmpty()) { "gates for skills Cobblebase doesn't have: ${stale.sorted()}" }
val zones = (Gates.skills.values.filterNotNull() + Gates.blocks.values).toSet()
val noGuardian = zones - Guardians.definitions.keys
s.check(noGuardian.isEmpty()) { "gates name zones without a guardian: ${noGuardian.sorted()}" }
val noBlock = Gates.blocks.keys.filterNot { BuiltInRegistries.BLOCK.containsKey(it) }
s.check(noBlock.isEmpty()) { "gates for blocks that don't exist: $noBlock" }
val open = CobblebaseGate.executorNames().filterNot { name -> CobblebaseGate.executor(name)?.let(CobblebaseGate::isGated) == true }
s.check(open.isEmpty()) { "Cobblebase executors not behind the gates: $open" }
s.helper.succeed()
}
/** A probe behind the same gate as Cobblebase's executors, called the way Cobblebase calls them. */
private fun skillsWaitForGuardians(s: Scenario) {
val level = s.level
val player = s.player()
val pokemon = s.give(player, s.pokemon("pikachu", 10))
s.check(pokemon.getOwnerUUID() == player.uuid) { "the test Pokémon has no owner" }
val entity = PokemonEntity(level, pokemon)
val (openSkill, _) = Gates.skills.entries.firstOrNull { it.value == null } ?: throw GameTestAssertException("no skill is open from the start")
val (lockedSkill, zone) = Gates.skills.entries.firstOrNull { it.value != null } ?: throw GameTestAssertException("no skill is gated")
val progress = Progress.of(level.server)
s.afterTest { progress.revoke(player.uuid, zone!!) }
var calls = 0
val type = CobblebaseGate.executorType
val probe = Proxy.newProxyInstance(type.classLoader, arrayOf(type)) { _, method, _ -> if (method.name == "tick") calls++; null }
val gated = CobblebaseGate.wrap(probe)
val tick = type.getMethod("tick", Level::class.java, BlockPos::class.java, PokemonEntity::class.java,
CobblebaseGate.skillDef, CobblebaseGate.skillEntry)
val entry = CobblebaseGate.skillEntry.getConstructor(String::class.java, Int::class.javaPrimitiveType)
fun run(skill: String) {
val def = CobblebaseGate.skills()[skill] ?: throw GameTestAssertException("Cobblebase has no skill $skill")
tick.invoke(gated, level, entity.blockPosition(), entity, def, entry.newInstance(skill, 1))
}
run(openSkill)
s.check(calls == 1) { "$openSkill is open from the start but didn't run" }
run(lockedSkill)
s.check(calls == 1) { "$lockedSkill ran before the guardian of $zone was beaten" }
progress.grant(player.uuid, zone!!)
run(lockedSkill)
s.check(calls == 2) { "$lockedSkill didn't run after the guardian of $zone was beaten" }
s.helper.succeed()
}
private fun powerBlockWaits(s: Scenario) {
val level = s.level
val zone = Gates.blocks[POWER_BLOCK] ?: throw GameTestAssertException("the Power Block has no gate")
val item = BuiltInRegistries.ITEM.get(POWER_BLOCK)
s.check(item != Items.AIR) { "Poke Power isn't installed" }
val player = s.player()
val progress = Progress.of(level.server)
s.afterTest { progress.revoke(player.uuid, zone) }
val floor = s.helper.absolutePos(BlockPos(7, 2, 7))
level.setBlockAndUpdate(floor, Blocks.STONE.defaultBlockState())
val target = floor.above()
fun place() {
player.setItemInHand(InteractionHand.MAIN_HAND, ItemStack(item))
val hit = BlockHitResult(Vec3.atCenterOf(floor).add(0.0, 0.5, 0.0), Direction.UP, floor, false)
player.gameMode.useItemOn(player, level, player.mainHandItem, InteractionHand.MAIN_HAND, hit)
}
fun placed() = BuiltInRegistries.BLOCK.getKey(level.getBlockState(target).block) == POWER_BLOCK
place()
s.check(!placed()) { "a player who hasn't beaten the guardian of $zone placed a Power Block" }
progress.grant(player.uuid, zone)
place()
s.check(placed()) { "the Power Block couldn't be placed after beating the guardian of $zone: ${level.getBlockState(target)}" }
s.helper.succeed()
}
}
@@ -51,7 +51,7 @@ object SelfTest {
private val cases: List<SelfTestCase> get() =
PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all() + GuardianTests.all() +
VeinTests.all() + DropTests.all()
VeinTests.all() + DropTests.all() + ProgressionTests.all()
private var running: List<Scenario> = emptyList()
private var forced: List<ChunkPos> = emptyList()