Phase 3: Alpha guardians, one per zone, back 5 hours after a defeat
CI / build-and-smoke-test (push) Failing after 7m6s

- Companion mod (zone/Guardians.kt): each zone's guardian stands at the
  zone's sample point, appears when a player is within 96 blocks, stays
  within 24 blocks, is uncatchable and half again as big. Beating it in a
  battle gives every player in the battle its drops; it returns after 5
  hours of game time (Adrian: fixed spots, 5 hours). State lives in the
  world save; /cobblesurvival guardians lists it.
- tools/zone_spawns.py writes data/cobblesurvival/guardians/<zone>.json:
  the strongest walking (or swimming) Pokemon of the zone at band top + 5,
  with the drops from world/zones.json. Boss drops follow Adrian's rule:
  Icy Rocks before Sunscorch, Heat Rocks before Frostpeak.
- Goat Fur and Hoglin Hide recipes make 8 per rock, so one guardian's 3
  rocks pay for one full insulating set.
- Self-test guardian_appears_and_returns; hazard tests cleaned up; the
  zone generator reads Cobblemon's own entries from the field guide's
  hidden_spawns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-29 19:01:21 +01:00
1 parent 398301f0be
commit 73fe71918f
25 files changed
+607 -23

No files matched your search

@@ -0,0 +1,75 @@
package dev.cobblesurvival.selftest
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.zone.Guardians
import dev.cobblesurvival.zone.Zones
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.world.level.levelgen.Heightmap
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
* state is shared.
*/
object GuardianTests {
private const val ZONE = "button_isle"
fun all() = listOf(SelfTestCase("guardian_appears_and_returns", 2400, ::guardian, batch = "guardian"))
private fun guardian(s: Scenario) {
val definition = Guardians.definitions[ZONE] ?: throw GameTestAssertException("no guardian defined for $ZONE")
val sample = Zones.samples[ZONE] ?: throw GameTestAssertException("no sample point for $ZONE")
val level = s.level
Guardians.resetForTest(level, ZONE)
s.afterTest { Guardians.resetForTest(level, ZONE) }
val player = s.player()
s.give(player, s.pokemon("mewtwo", 100, "earthquake")) // a spread move needs no target
s.answerBattles(player) { _, _ -> MoveActionResponse("earthquake") }
lateinit var guardian: PokemonEntity
val expectedLevel = Regex("level=(\\d+)").find(definition.pokemon)?.groupValues?.get(1)?.toInt()
s.helper.startSequence()
.thenExecute {
level.getChunk(sample.x shr 4, sample.z shr 4)
val y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z)
s.moveTo(player, sample.x + 6.5, y.toDouble(), sample.z + 0.5)
}
.thenWaitUntil {
guardian = Guardians.guardianOf(level, ZONE) ?: throw GameTestAssertException("no guardian appeared near the player")
}
.thenExecute {
val pokemon = guardian.pokemon
s.check(pokemon.isUncatchable()) { "the guardian can be caught" }
s.check(abs(pokemon.scaleModifier - definition.scale) < 0.01f) { "the guardian is size ${pokemon.scaleModifier}, not ${definition.scale}" }
s.check(expectedLevel == null || pokemon.level == expectedLevel) { "the guardian is level ${pokemon.level}, not $expectedLevel" }
s.moveTo(player, guardian.x + 3, guardian.y, guardian.z)
}
.thenWaitUntil { s.check(guardian.pokemon.entity === guardian) { "the guardian has not ticked yet" } }
.thenExecute { s.started(BattleBuilder.pve(player, guardian)) }
.thenWaitUntil {
s.check(!player.isInBattle() && Guardians.defeatedAt(level, ZONE) != null) { "the guardian isn't beaten yet: ${s.battleState(player)}" }
}
.thenExecute {
for ((item, count) in definition.drops) {
val have = player.inventory.countItem(BuiltInRegistries.ITEM.get(item))
s.check(have >= count) { "the player got $have of $item from the guardian, not $count" }
}
}
.thenIdle(100)
.thenExecute {
s.check(Guardians.guardianOf(level, ZONE) == null) { "the guardian came back right away" }
Guardians.expireDefeatForTest(level, ZONE)
}
.thenWaitUntil {
s.check(Guardians.guardianOf(level, ZONE) != null) { "the guardian didn't come back after its time" }
}
.thenSucceed()
}
}
@@ -48,7 +48,7 @@ object SelfTest {
private const val FORCED_CHUNKS = 9
private val cases: List<SelfTestCase> get() =
PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all()
PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all() + GuardianTests.all()
private var running: List<Scenario> = emptyList()
private var forced: List<ChunkPos> = emptyList()