Adrian (2026-10-31): HMs abilities stay open; the ones that get around zone hazards get soft gates (materials or a high-level Pokemon, never flags; gifted Pokemon and materials count the same); party passives from shoulder Pokemon; Cobblebase skills and the Power Block move to soft gates too. - Gates: a zone gate passes at the top of the zone's band or while the Pokemon holds the zone's material (or a harder zone's). Cobblebase skills check the pastured Pokemon and tell the owner once what it needs. - Progress record, guardian unlock messages, the Power Block placement gate and /cobblesurvival progress removed; the Power Block's recipe already needs Jungle redstone. - HmsGate: switches off gated HMs auras (Flash, Dive, Mist, Rock Climb, Magnet Rise, Lucky Chant, Lava Plume) whose Pokemon doesn't pass. - ShoulderPassives: type-based effects from shoulder Pokemon, hazard ones (warmth, frigidness, water breathing, night vision) gated. - Self-tests for all of it; docs, balance table, Phase 5 checklist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
4.0 KiB
Kotlin
85 lines
4.0 KiB
Kotlin
package dev.cobblesurvival.progress
|
|
|
|
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
|
import dev.cobblesurvival.CobbleSurvival
|
|
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 passes
|
|
* that skill's soft gate (level or held material); otherwise its owner is told what it needs. 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 entity = args[2] as PokemonEntity
|
|
val skill = skillId.invoke(args[3]) as String
|
|
if (!Gates.skillOpen(entity.pokemon, skill)) {
|
|
Gates.noticeIdle(entity, skill)
|
|
return null
|
|
}
|
|
}
|
|
return try {
|
|
method.invoke(original, *(args ?: emptyArray()))
|
|
} catch (e: InvocationTargetException) {
|
|
throw e.targetException
|
|
}
|
|
}
|
|
}
|
|
}
|