Phase 3: soft level pressure, less experience far above a zone's band

- Companion mod (zone/LevelPressure.kt): a Pokemon more than 5 levels
  above the band of the zone its trainer is in gets 10% less experience
  per extra level, down to 10%. Exp. Candy and commands are exempt; no
  hard level caps (roadmap).
- Rules in world/zones.json -> level_pressure, shipped in the zone layout.
- Self-test level_pressure_cuts_xp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-30 01:50:56 +01:00
1 parent d105f6757d
commit 173203eeb5
10 files changed
+91 -3

No files matched your search

@@ -1,6 +1,8 @@
package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.Cobblemon
import com.cobblemon.mod.common.api.pokemon.experience.CandyExperienceSource
import com.cobblemon.mod.common.api.pokemon.experience.SidemodExperienceSource
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.SpawnCause
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
@@ -9,6 +11,7 @@ import com.cobblemon.mod.common.api.spawning.position.calculators.SpawnablePosit
import com.cobblemon.mod.common.api.spawning.spawner.SpawningZoneInput
import com.cobblemon.mod.common.util.spawner
import dev.cobblesurvival.CobbleSurvival
import dev.cobblesurvival.zone.LevelPressure
import dev.cobblesurvival.zone.Zone
import dev.cobblesurvival.zone.ZoneSample
import dev.cobblesurvival.zone.Zones
@@ -16,7 +19,9 @@ import net.minecraft.core.BlockPos
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.server.level.ServerPlayer
import net.minecraft.util.Mth
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.levelgen.Heightmap
import kotlin.math.roundToInt
/**
* Roadmap Phase 3: each zone has its own Pokémon and levels. The spawn pools come from tools/zone_spawns.py;
@@ -30,10 +35,36 @@ object ZoneTests {
fun all() = listOf(
SelfTestCase("spawn_pools_are_zoned", 20, ::spawnPoolsAreZoned),
SelfTestCase("spawn_survey", 2400, ::spawnSurvey, islandOnly = true),
SelfTestCase("level_pressure_cuts_xp", 100, ::levelPressure),
)
private fun zoneOf(detail: SpawnDetail): Zone? = Zones.byId[detail.id.substringBefore('-')]
/** On Button Isle (band up to 10, 5 levels' grace): a Lv 10 Pokémon keeps its experience, a Lv 25 one 10 %. */
private fun levelPressure(s: Scenario) {
val zone = Zones.byId["button_isle"] ?: throw GameTestAssertException("no Button Isle zone")
val sample = Zones.samples["button_isle"] ?: throw GameTestAssertException("no sample point for Button Isle")
val player = s.player()
val inBand = s.give(player, s.pokemon("bidoof", zone.maxLevel))
val farAbove = s.give(player, s.pokemon("bidoof", zone.maxLevel + 15))
val expected = (1000 * LevelPressure.factor(farAbove.level, zone)).roundToInt()
s.helper.startSequence()
.thenExecute {
s.level.getChunk(sample.x shr 4, sample.z shr 4)
s.moveTo(player, sample.x + 0.5, s.level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z).toDouble(), sample.z + 0.5)
}
.thenExecute {
val battle = SidemodExperienceSource(CobbleSurvival.MOD_ID)
val kept = inBand.addExperienceWithPlayer(player, battle, 1000).experienceAdded
val cut = farAbove.addExperienceWithPlayer(player, battle, 1000).experienceAdded
val candy = farAbove.addExperienceWithPlayer(player, CandyExperienceSource(player, ItemStack.EMPTY), 1000).experienceAdded
s.check(kept == 1000) { "a Pokémon at the band's top got $kept of 1000 experience" }
s.check(cut == expected) { "a Pokémon 15 levels above the band got $cut of 1000 experience, not $expected" }
s.check(candy == 1000) { "Exp. Candy was cut too: $candy of 1000" }
}
.thenSucceed()
}
/** Every loaded spawn entry belongs to a zone and spawns within its level band; none of Cobblemon's is left. */
private fun spawnPoolsAreZoned(s: Scenario) {
val details = CobblemonSpawnPools.WORLD_SPAWN_POOL.details