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:
1 parent
1a1fb93da8
commit
362d06aa4b
22 files changed
+618
-20
No files matched your search
@@ -6,6 +6,8 @@ import com.mojang.logging.LogUtils
|
||||
import dev.cobblesurvival.dev.BiomePreview
|
||||
import dev.cobblesurvival.dev.OreVeins
|
||||
import dev.cobblesurvival.permadeath.Permadeath
|
||||
import dev.cobblesurvival.progress.Gates
|
||||
import dev.cobblesurvival.progress.Progress
|
||||
import dev.cobblesurvival.selftest.SelfTest
|
||||
import dev.cobblesurvival.world.OnlyPokemon
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
@@ -60,6 +62,7 @@ class CobbleSurvival {
|
||||
ZoneCondition.register()
|
||||
NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners)
|
||||
Guardians.register()
|
||||
Gates.register()
|
||||
ZoneHazards.register()
|
||||
LevelPressure.register()
|
||||
SelfTest.register()
|
||||
@@ -94,6 +97,7 @@ class CobbleSurvival {
|
||||
})
|
||||
.then(BiomePreview.command())
|
||||
.then(OreVeins.command())
|
||||
.then(Progress.command())
|
||||
.then(Commands.literal("zone").executes { context ->
|
||||
val source = context.source
|
||||
val zone = Zones.at(source.level, BlockPos.containing(source.position))
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package dev.cobblesurvival.progress
|
||||
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import net.minecraft.world.level.Level
|
||||
import net.neoforged.fml.ModList
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Proxy
|
||||
|
||||
/**
|
||||
* Puts [Gates] in front of Cobblebase's skills, by reflection (Cobblebase is not a compile dependency): every skill
|
||||
* executor in Cobblebase's `ExecutorRegistry` is replaced by a proxy that runs it only when the pastured Pokémon's owner
|
||||
* has unlocked that skill. Cobblebase looks the executor up on every tick, so the proxies take effect at once. One
|
||||
* executor can serve several skills (all the finders), so the check is per skill, not per executor.
|
||||
*/
|
||||
object CobblebaseGate {
|
||||
private const val CORE = "notlown.cobblebase.core"
|
||||
|
||||
val installed: Boolean get() = ModList.get().isLoaded("cobblebase")
|
||||
|
||||
internal val executorType by lazy { Class.forName("$CORE.SkillExecutor") }
|
||||
internal val skillEntry by lazy { Class.forName("$CORE.SkillEntry") }
|
||||
private val executors by lazy { Class.forName("$CORE.ExecutorRegistry").let { it to it.getField("INSTANCE").get(null) } }
|
||||
private val skillRegistry by lazy { Class.forName("$CORE.SkillRegistry").let { it to it.getField("INSTANCE").get(null) } }
|
||||
internal val skillDef by lazy { Class.forName("$CORE.SkillDef") }
|
||||
private val skillId by lazy { skillDef.getMethod("getId") }
|
||||
|
||||
/** Skill id → its definition (Cobblebase's SkillDef). */
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun skills(): Map<String, Any> {
|
||||
val (type, instance) = skillRegistry
|
||||
val all = type.getMethod("getAll").invoke(instance) as Map<String, Any>
|
||||
return all.values.associateBy { skillId.invoke(it) as String }
|
||||
}
|
||||
|
||||
fun skillName(id: String): String? = skills()[id]?.let { skillDef.getMethod("getName").invoke(it) as String }
|
||||
|
||||
fun executor(name: String): Any? {
|
||||
val (type, instance) = executors
|
||||
return type.getMethod("get", String::class.java).invoke(instance, name)
|
||||
}
|
||||
|
||||
/** The executor names the skills use (several skills can share one). */
|
||||
fun executorNames(): Set<String> = skills().values.map { skillDef.getMethod("getExecutor").invoke(it) as String }.toSortedSet()
|
||||
|
||||
fun install() {
|
||||
if (!installed) return
|
||||
val (type, instance) = executors
|
||||
val register = type.getMethod("register", String::class.java, executorType)
|
||||
var wrapped = 0
|
||||
for (name in executorNames()) {
|
||||
val original = executor(name) ?: continue
|
||||
if (isGated(original)) continue
|
||||
register.invoke(instance, name, wrap(original))
|
||||
wrapped++
|
||||
}
|
||||
CobbleSurvival.LOGGER.info("Progression gates in front of {} Cobblebase skill executors", wrapped)
|
||||
}
|
||||
|
||||
fun isGated(executor: Any): Boolean =
|
||||
Proxy.isProxyClass(executor.javaClass) && Proxy.getInvocationHandler(executor) is Gate
|
||||
|
||||
/** [original] behind the gates: `tick(level, pos, pokemonEntity, skillDef, skillEntry)` only runs if allowed. */
|
||||
fun wrap(original: Any): Any = Proxy.newProxyInstance(executorType.classLoader, arrayOf(executorType), Gate(original))
|
||||
|
||||
private class Gate(private val original: Any) : InvocationHandler {
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||
if (method.name == "tick" && args != null && args.size == 5) {
|
||||
val level = args[0] as Level
|
||||
val owner = (args[2] as PokemonEntity).pokemon.getOwnerUUID()
|
||||
val skill = skillId.invoke(args[3]) as String
|
||||
val server = level.server ?: return null
|
||||
if (!Gates.skillOpen(Progress.of(server), owner, skill)) return null
|
||||
}
|
||||
return try {
|
||||
method.invoke(original, *(args ?: emptyArray()))
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw e.targetException
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package dev.cobblesurvival.progress
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonElement
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import dev.cobblesurvival.zone.Zones
|
||||
import net.minecraft.ChatFormatting
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.server.packs.resources.ResourceManager
|
||||
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
|
||||
import net.minecraft.util.profiling.ProfilerFiller
|
||||
import net.neoforged.neoforge.common.NeoForge
|
||||
import net.neoforged.neoforge.event.AddReloadListenerEvent
|
||||
import net.neoforged.neoforge.event.level.BlockEvent
|
||||
import net.neoforged.neoforge.event.server.ServerStartedEvent
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Progression gates (Adrian, 2026-10-30: Cobblebase's skills and the power source stay in, "gated behind
|
||||
* progression"): a Cobblebase skill works for a pastured Pokémon, and a gated block can be placed, only once its owner
|
||||
* has beaten a given zone's Alpha guardian ([Progress]).
|
||||
*
|
||||
* The table is data, `data/cobblesurvival/progression_gates/<name>.json`:
|
||||
* `{"cobblebase_skills": {"<skill id>": "<zone>" or null}, "blocks": {"<block id>": "<zone>"}}`; null means from the
|
||||
* start, later files override earlier ones. A Cobblebase skill missing from the table stays off, so an update can't
|
||||
* slip new skills past the gates; the self-test `gates_cover_every_cobblebase_skill` names any that are missing.
|
||||
*/
|
||||
object Gates : SimpleJsonResourceReloadListener(Gson(), "progression_gates") {
|
||||
var skills: Map<String, String?> = emptyMap()
|
||||
private set
|
||||
var blocks: Map<ResourceLocation, String> = emptyMap()
|
||||
private set
|
||||
|
||||
fun register() {
|
||||
NeoForge.EVENT_BUS.addListener<AddReloadListenerEvent> { it.addListener(this) }
|
||||
NeoForge.EVENT_BUS.addListener(::onPlace)
|
||||
NeoForge.EVENT_BUS.addListener<ServerStartedEvent> { CobblebaseGate.install() }
|
||||
}
|
||||
|
||||
override fun apply(files: Map<ResourceLocation, JsonElement>, resources: ResourceManager, profiler: ProfilerFiller) {
|
||||
val skillGates = LinkedHashMap<String, String?>()
|
||||
val blockGates = LinkedHashMap<ResourceLocation, String>()
|
||||
for ((_, element) in files.toSortedMap()) {
|
||||
val json = element.asJsonObject
|
||||
json.getAsJsonObject("cobblebase_skills")?.entrySet()?.forEach { (skill, zone) ->
|
||||
skillGates[skill] = if (zone.isJsonNull) null else zone.asString
|
||||
}
|
||||
json.getAsJsonObject("blocks")?.entrySet()?.forEach { (block, zone) ->
|
||||
blockGates[ResourceLocation.parse(block)] = zone.asString
|
||||
}
|
||||
}
|
||||
skills = skillGates
|
||||
blocks = blockGates
|
||||
CobbleSurvival.LOGGER.info("Loaded progression gates: {} Cobblebase skills, {} blocks", skills.size, blocks.size)
|
||||
}
|
||||
|
||||
fun skillOpen(progress: Progress, owner: UUID?, skill: String): Boolean {
|
||||
if (!skills.containsKey(skill)) return false
|
||||
val zone = skills[skill] ?: return true
|
||||
return owner != null && zone in progress.beaten(owner)
|
||||
}
|
||||
|
||||
private fun onPlace(event: BlockEvent.EntityPlaceEvent) {
|
||||
val player = event.entity as? ServerPlayer ?: return
|
||||
if (player.isCreative) return
|
||||
val zone = blocks[BuiltInRegistries.BLOCK.getKey(event.placedBlock.block)] ?: return
|
||||
if (zone in Progress.of(player.server).beaten(player.uuid)) return
|
||||
event.isCanceled = true
|
||||
player.displayClientMessage(
|
||||
Component.translatableWithFallback(
|
||||
"message.cobblesurvival.gate.block", "Beat the guardian of %s to use this.", zoneName(zone),
|
||||
).withStyle(ChatFormatting.RED), true
|
||||
)
|
||||
}
|
||||
|
||||
/** Tells [player] what beating [zone]'s guardian unlocked for them. */
|
||||
fun announceUnlocks(player: ServerPlayer, zone: String) {
|
||||
val unlocked = skills.filterValues { it == zone }.keys.map { CobblebaseGate.skillName(it) ?: it } +
|
||||
blocks.filterValues { it == zone }.keys.map { BuiltInRegistries.BLOCK.get(it).name.string }
|
||||
if (unlocked.isEmpty()) return
|
||||
player.sendSystemMessage(
|
||||
Component.translatableWithFallback(
|
||||
"message.cobblesurvival.gate.unlocked", "Unlocked at your base: %s", unlocked.joinToString(),
|
||||
).withStyle(ChatFormatting.GREEN)
|
||||
)
|
||||
}
|
||||
|
||||
private fun zoneName(zone: String) = Zones.byId[zone]?.name ?: zone
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package dev.cobblesurvival.progress
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
import net.minecraft.commands.CommandSourceStack
|
||||
import net.minecraft.commands.Commands
|
||||
import net.minecraft.commands.SharedSuggestionProvider
|
||||
import net.minecraft.commands.arguments.EntityArgument
|
||||
import net.minecraft.core.HolderLookup
|
||||
import net.minecraft.nbt.CompoundTag
|
||||
import net.minecraft.nbt.ListTag
|
||||
import net.minecraft.nbt.StringTag
|
||||
import net.minecraft.nbt.Tag
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.world.level.saveddata.SavedData
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* A player's progression: the zones whose Alpha guardian they have helped beat. [Guardians] records a win for every
|
||||
* player in the battle; [Gates] unlocks base skills and blocks from it. Saved with the overworld.
|
||||
*
|
||||
* Operators: `/cobblesurvival progress <player>` lists it, `... grant|revoke <player> <zone>` changes it.
|
||||
*/
|
||||
class Progress : SavedData() {
|
||||
private val beaten = HashMap<UUID, MutableSet<String>>()
|
||||
|
||||
fun beaten(player: UUID): Set<String> = beaten[player].orEmpty()
|
||||
|
||||
/** Records that [player] beat [zone]'s guardian; false if they had already. */
|
||||
fun grant(player: UUID, zone: String): Boolean =
|
||||
beaten.getOrPut(player) { HashSet() }.add(zone).also { if (it) setDirty() }
|
||||
|
||||
fun revoke(player: UUID, zone: String): Boolean = (beaten[player]?.remove(zone) == true).also { if (it) setDirty() }
|
||||
|
||||
override fun save(tag: CompoundTag, registries: HolderLookup.Provider): CompoundTag {
|
||||
for ((player, zones) in beaten) {
|
||||
tag.put(player.toString(), ListTag().apply { zones.sorted().forEach { add(StringTag.valueOf(it)) } })
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FACTORY = Factory(::Progress, ::load)
|
||||
|
||||
fun of(server: MinecraftServer): Progress = server.overworld().dataStorage.computeIfAbsent(FACTORY, "cobblesurvival_progress")
|
||||
|
||||
private fun load(tag: CompoundTag, registries: HolderLookup.Provider): Progress {
|
||||
val progress = Progress()
|
||||
for (key in tag.allKeys) {
|
||||
val zones = tag.getList(key, Tag.TAG_STRING.toInt()).map { it.asString }
|
||||
progress.beaten[UUID.fromString(key)] = zones.toHashSet()
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
fun command(): LiteralArgumentBuilder<CommandSourceStack> {
|
||||
val zone = Commands.argument("zone", StringArgumentType.word())
|
||||
.suggests { _, builder -> SharedSuggestionProvider.suggest(Guardians.definitions.keys, builder) }
|
||||
fun change(grant: Boolean) = Commands.literal(if (grant) "grant" else "revoke")
|
||||
.then(Commands.argument("player", EntityArgument.player()).then(zone.executes { context ->
|
||||
val player = EntityArgument.getPlayer(context, "player")
|
||||
val id = StringArgumentType.getString(context, "zone")
|
||||
val progress = of(context.source.server)
|
||||
val changed = if (grant) progress.grant(player.uuid, id) else progress.revoke(player.uuid, id)
|
||||
context.source.sendSuccess({ Component.literal(
|
||||
"${player.gameProfile.name}: $id ${if (grant) "granted" else "revoked"}${if (changed) "" else " (no change)"}") }, true)
|
||||
1
|
||||
}))
|
||||
return Commands.literal("progress")
|
||||
.then(Commands.argument("player", EntityArgument.player()).executes { context ->
|
||||
val player = EntityArgument.getPlayer(context, "player")
|
||||
val zones = of(context.source.server).beaten(player.uuid).sorted()
|
||||
context.source.sendSuccess({ Component.literal(
|
||||
"${player.gameProfile.name} beat: ${zones.joinToString().ifEmpty { "no guardian yet" }}") }, false)
|
||||
1
|
||||
})
|
||||
.then(change(grant = true))
|
||||
.then(change(grant = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -8,6 +8,8 @@ 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.progress.Gates
|
||||
import dev.cobblesurvival.progress.Progress
|
||||
import net.minecraft.ChatFormatting
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.HolderLookup
|
||||
@@ -38,6 +40,7 @@ import kotlin.math.abs
|
||||
* `respawn_hours` of game time later (Adrian: fixed spots, 5 hours). It only appears while a player is near, and
|
||||
* wanders no further than [LEASH] blocks from its spot. Definitions: `data/cobblesurvival/guardians/<zone>.json`,
|
||||
* written by tools/zone_spawns.py; state (which entity, when beaten, where it stands) lives in the world save.
|
||||
* Every player in the winning battle gets the drops and the zone in their [Progress], which unlocks base skills.
|
||||
*/
|
||||
object Guardians : SimpleJsonResourceReloadListener(Gson(), "guardians") {
|
||||
/** Key in the guardian Pokémon's persistent data; its value is the zone id. */
|
||||
@@ -178,6 +181,7 @@ object Guardians : SimpleJsonResourceReloadListener(Gson(), "guardians") {
|
||||
"The guardian of %s is beaten! It returns in %s hours.", zoneName, hours,
|
||||
).withStyle(ChatFormatting.GOLD)
|
||||
)
|
||||
if (Progress.of(player.server).grant(player.uuid, zone)) Gates.announceUnlocks(player, zone)
|
||||
}
|
||||
CobbleSurvival.LOGGER.info("Guardian of {} beaten by {}", zone, players.joinToString { it.gameProfile.name })
|
||||
}
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
"message.cobblesurvival.permadeath.lost": "%s fainted and is gone for good.",
|
||||
"message.cobblesurvival.permadeath.saved": "%s's Revival Charm saved it and crumbled to dust.",
|
||||
"message.cobblesurvival.guardian.beaten": "The guardian of %s is beaten! It returns in %s hours.",
|
||||
"message.cobblesurvival.hazard.mire": "The Mire's fumes are poisoning you. Mire Waders would keep them off."
|
||||
"message.cobblesurvival.hazard.mire": "The Mire's fumes are poisoning you. Mire Waders would keep them off.",
|
||||
"message.cobblesurvival.gate.block": "Beat the guardian of %s to use this.",
|
||||
"message.cobblesurvival.gate.unlocked": "Unlocked at your base: %s"
|
||||
}
|
||||
Reference in new issue
Block a user