Files
cobblemon-survival-pack/mod/src/main/kotlin/dev/cobblesurvival/progress/CobblebaseGate.kt
T
claudeandClaude Opus 5.5 70cc2cad50 Phase 5: soft gates instead of guardian flags, shoulder passives
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>
2026-10-31 13:41:04 +01:00

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
}
}
}
}