Phase 5: Cobblebase jobs take back gated work, dig only zone ores
From Adrian's Mining test (2026-10-31): - A gated job assigned to a pastured Pokemon that doesn't pass its gate is taken back at once (BaseManager assignment cleared, Cobblebase's assignment sync sent, owner told what the Pokemon needs), so it no longer sticks or shows under Buffs -> Active. - Mining, Ore Finder and Evolution-item Finder only give the ores and evolution stones of the pasture's zone: new loot condition cobblesurvival:in_zone, the pack's versions of the 12 Cobblebase tables (tools/cobblebase_loot.py), Cobblebase's built-in copies removed at server start (ore/BaseLoot.kt). Mining adds quartz and the evolution stones, drops the type gems. - Mining's loot reaching the chest needs an Item Gatherer in the same pasture (Cobblebase's design); documented, not changed. - Self-tests gated_jobs_are_taken_back, base_jobs_dig_their_zones_ores; docs, balance table, licenses, Phase 5 checklist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
70cc2cad50
commit
11a583db01
27 files changed
+1458
-28
No files matched your search
@@ -0,0 +1,58 @@
|
||||
package dev.cobblesurvival.selftest
|
||||
|
||||
import dev.cobblesurvival.ore.BaseLoot
|
||||
import dev.cobblesurvival.zone.InZoneCondition
|
||||
import dev.cobblesurvival.zone.Zones
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.gametest.framework.GameTestAssertException
|
||||
import net.minecraft.world.level.storage.loot.LootContext
|
||||
import net.minecraft.world.level.storage.loot.LootParams
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Cobblebase's base jobs only dig up the pasture zone's ores (Adrian, 2026-10-31): Cobblebase's built-in copies of the
|
||||
* mining and finder tables are gone, `cobblesurvival:in_zone` holds only inside its zones, and Mining's common table
|
||||
* gives Button Isle coal but no iron and Westmarch iron but no coal (world/zones.json: coal is Button Isle's only ore,
|
||||
* raw iron comes from Westmarch).
|
||||
*/
|
||||
object BaseLootTests {
|
||||
private const val ROLLS = 200
|
||||
|
||||
fun all() = listOf(SelfTestCase("base_jobs_dig_their_zones_ores", 40, ::baseJobsDigTheirZonesOres))
|
||||
|
||||
/** A spot in [zone], high above the ground so no cave zone lies there. */
|
||||
private fun spotIn(s: Scenario, zone: String): BlockPos {
|
||||
val sample = Zones.samples[zone] ?: throw GameTestAssertException("no sample point for $zone")
|
||||
val pos = BlockPos(sample.x, 250, sample.z)
|
||||
s.check(Zones.at(s.level, pos)?.id == zone) { "the sample point of $zone lies in ${Zones.at(s.level, pos)?.id}" }
|
||||
return pos
|
||||
}
|
||||
|
||||
private fun baseJobsDigTheirZonesOres(s: Scenario) {
|
||||
val level = s.level
|
||||
s.check(BaseLoot.builtInLeft().isEmpty()) { "Cobblebase still rolls its own copies of ${BaseLoot.builtInLeft()}" }
|
||||
val isle = spotIn(s, "button_isle")
|
||||
val westmarch = spotIn(s, "westmarch")
|
||||
|
||||
val params = LootParams.Builder(level).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(isle)).create(LootContextParamSets.CHEST)
|
||||
val context = LootContext.Builder(params).create(Optional.empty())
|
||||
s.check(InZoneCondition(listOf("button_isle")).test(context)) { "in_zone [button_isle] is false on Button Isle" }
|
||||
s.check(!InZoneCondition(listOf("westmarch")).test(context)) { "in_zone [westmarch] is true on Button Isle" }
|
||||
|
||||
fun dug(pos: BlockPos, table: String) = (1..ROLLS).flatMap { BaseLoot.roll(table, level, pos) }
|
||||
.map { BuiltInRegistries.ITEM.getKey(it.item).path }.toSet()
|
||||
val onIsle = dug(isle, "cobblebase:mining_common")
|
||||
s.check("coal" in onIsle && "raw_iron" !in onIsle && "raw_copper" !in onIsle) { "Mining on Button Isle dug up $onIsle" }
|
||||
val inWestmarch = dug(westmarch, "cobblebase:mining_common")
|
||||
s.check("raw_iron" in inWestmarch && "coal" !in inWestmarch) { "Mining in Westmarch dug up $inWestmarch" }
|
||||
val gems = (dug(isle, "cobblebase:mining_uncommon") + dug(isle, "cobblebase:mining_ultra_rare")).filter { it.endsWith("_gem") }
|
||||
s.check(gems.isEmpty()) { "Mining still digs up type gems: $gems" }
|
||||
val stones = dug(isle, "cobblebase:finder_evo_common")
|
||||
s.check(stones.isEmpty()) { "the Evolution-item Finder found $stones on Button Isle, which has no evolution stones" }
|
||||
s.helper.succeed()
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import java.lang.reflect.Proxy
|
||||
|
||||
/**
|
||||
* Soft gates (Adrian, 2026-10-31): Cobblebase's base skills open up through the pastured Pokémon's level or a material it
|
||||
* holds, never through flags (`data/cobblesurvival/progression_gates/`). Poke Power's Power Block has no gate of its
|
||||
* holds, never through flags (`data/cobblesurvival/progression_gates/`); a gated job doesn't stay assigned. Poke Power's Power Block has no gate of its
|
||||
* own: its recipe already takes the Verdant Jungle's redstone.
|
||||
*/
|
||||
object ProgressionTests {
|
||||
@@ -29,6 +29,7 @@ object ProgressionTests {
|
||||
fun all() = listOf(
|
||||
SelfTestCase("gates_cover_every_cobblebase_skill", 20, ::gatesCoverEverySkill),
|
||||
SelfTestCase("cobblebase_skills_need_level_or_material", 20, ::skillsNeedLevelOrMaterial),
|
||||
SelfTestCase("gated_jobs_are_taken_back", 20, ::gatedJobsAreTakenBack),
|
||||
SelfTestCase("power_block_gated_by_its_recipe", 20, ::powerBlockGatedByRecipe),
|
||||
)
|
||||
|
||||
@@ -50,6 +51,26 @@ object ProgressionTests {
|
||||
}
|
||||
|
||||
/** A probe behind the same gate as Cobblebase's executors, called the way Cobblebase calls them. */
|
||||
private class Probe(private val s: Scenario, private val entity: PokemonEntity) {
|
||||
private var calls = 0
|
||||
private val type = CobblebaseGate.executorType
|
||||
private val wrapped = CobblebaseGate.wrap(
|
||||
Proxy.newProxyInstance(type.classLoader, arrayOf(type)) { _, method, _ -> if (method.name == "tick") calls++; null }
|
||||
)
|
||||
private val tick = type.getMethod("tick", Level::class.java, BlockPos::class.java, PokemonEntity::class.java,
|
||||
CobblebaseGate.skillDef, CobblebaseGate.skillEntry)
|
||||
private val entry = CobblebaseGate.skillEntry.getConstructor(String::class.java, Int::class.javaPrimitiveType)
|
||||
|
||||
/** Ticks [skill] for the Pokémon, holding [holding]; whether the skill ran. */
|
||||
fun runs(skill: String, holding: Item? = null): Boolean {
|
||||
entity.pokemon.swapHeldItem(holding?.let(::ItemStack) ?: ItemStack.EMPTY, false)
|
||||
val def = CobblebaseGate.skills()[skill] ?: throw GameTestAssertException("Cobblebase has no skill $skill")
|
||||
val before = calls
|
||||
tick.invoke(wrapped, s.level, entity.blockPosition(), entity, def, entry.newInstance(skill, 1))
|
||||
return calls > before
|
||||
}
|
||||
}
|
||||
|
||||
private fun skillsNeedLevelOrMaterial(s: Scenario) {
|
||||
val level = s.level
|
||||
val player = s.player()
|
||||
@@ -62,32 +83,37 @@ object ProgressionTests {
|
||||
val easier = Gates.zones.values.filter { it.level < gate.level && it.items.isNotEmpty() }.minByOrNull { it.level }
|
||||
val harder = Gates.zones.values.filter { it.level > gate.level && it.items.isNotEmpty() }.maxByOrNull { it.level }
|
||||
|
||||
var calls = 0
|
||||
val type = CobblebaseGate.executorType
|
||||
val probe = Proxy.newProxyInstance(type.classLoader, arrayOf(type)) { _, method, _ -> if (method.name == "tick") calls++; null }
|
||||
val wrapped = 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 runs(skill: String, holding: Item? = null): Boolean {
|
||||
pokemon.swapHeldItem(holding?.let(::ItemStack) ?: ItemStack.EMPTY, false)
|
||||
val def = CobblebaseGate.skills()[skill] ?: throw GameTestAssertException("Cobblebase has no skill $skill")
|
||||
val before = calls
|
||||
tick.invoke(wrapped, level, entity.blockPosition(), entity, def, entry.newInstance(skill, 1))
|
||||
return calls > before
|
||||
}
|
||||
val probe = Probe(s, entity)
|
||||
|
||||
s.check(runs(openSkill)) { "$openSkill is open from the start but didn't run" }
|
||||
s.check(!runs(lockedSkill)) { "$lockedSkill (gate: ${gate.zone}) ran for a Lv 1 Pokémon holding nothing" }
|
||||
s.check(runs(lockedSkill, gate.items.first())) { "$lockedSkill didn't run for a Pokémon holding ${gate.items.first()} (${gate.zone})" }
|
||||
s.check(probe.runs(openSkill)) { "$openSkill is open from the start but didn't run" }
|
||||
s.check(!probe.runs(lockedSkill)) { "$lockedSkill (gate: ${gate.zone}) ran for a Lv 1 Pokémon holding nothing" }
|
||||
s.check(probe.runs(lockedSkill, gate.items.first())) { "$lockedSkill didn't run for a Pokémon holding ${gate.items.first()} (${gate.zone})" }
|
||||
if (harder != null) {
|
||||
s.check(runs(lockedSkill, harder.items.first())) { "$lockedSkill didn't run with ${harder.items.first()} from the harder ${harder.zone}" }
|
||||
s.check(probe.runs(lockedSkill, harder.items.first())) { "$lockedSkill didn't run with ${harder.items.first()} from the harder ${harder.zone}" }
|
||||
}
|
||||
if (easier != null) {
|
||||
s.check(!runs(lockedSkill, easier.items.first())) { "$lockedSkill ran with ${easier.items.first()} from the easier ${easier.zone}" }
|
||||
s.check(!probe.runs(lockedSkill, easier.items.first())) { "$lockedSkill ran with ${easier.items.first()} from the easier ${easier.zone}" }
|
||||
}
|
||||
pokemon.level = gate.level
|
||||
s.check(runs(lockedSkill)) { "$lockedSkill didn't run for a Lv ${gate.level} Pokémon (${gate.zone})" }
|
||||
s.check(probe.runs(lockedSkill)) { "$lockedSkill didn't run for a Lv ${gate.level} Pokémon (${gate.zone})" }
|
||||
s.helper.succeed()
|
||||
}
|
||||
|
||||
/** A gated job assigned to a Pokémon that doesn't pass is taken back at once; one it passes for stays. */
|
||||
private fun gatedJobsAreTakenBack(s: Scenario) {
|
||||
val player = s.player()
|
||||
val pokemon = s.give(player, s.pokemon("pikachu", 1))
|
||||
val probe = Probe(s, PokemonEntity(s.level, pokemon))
|
||||
val (job, zone) = Gates.skills.entries.firstOrNull { it.value != null } ?: throw GameTestAssertException("no skill is gated")
|
||||
val material = Gates.zones.getValue(zone!!).items.first()
|
||||
s.afterTest { CobblebaseGate.assign(pokemon.uuid, null) }
|
||||
|
||||
CobblebaseGate.assign(pokemon.uuid, job)
|
||||
probe.runs(job)
|
||||
s.check(CobblebaseGate.assignment(pokemon.uuid) == null) { "$job stayed assigned to a Lv 1 Pokémon holding nothing (gate: $zone)" }
|
||||
CobblebaseGate.assign(pokemon.uuid, job)
|
||||
s.check(probe.runs(job, material)) { "$job didn't run for a Pokémon holding $material" }
|
||||
s.check(CobblebaseGate.assignment(pokemon.uuid) == job) { "$job was taken back from a Pokémon holding $material" }
|
||||
s.helper.succeed()
|
||||
}
|
||||
|
||||
|
||||
@@ -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() + FieldMoveTests.all() + ShoulderTests.all()
|
||||
CatchingTests.all() + WorldRulesTests.all() + FieldMoveTests.all() + ShoulderTests.all() + BaseLootTests.all()
|
||||
|
||||
private var running: List<Scenario> = emptyList()
|
||||
private var forced: List<ChunkPos> = emptyList()
|
||||
|
||||
Reference in new issue
Block a user