Phase 9: safe start island, better quest book, every useful item obtainable
- Button Isle is peaceful: wild Pokémon only fight a player who hit them first (tuning/server.json peaceful_zones); its apricorns and coal make Poké Balls (4 apricorns + coal or charcoal), and the world report checks it has enough. - Quest book: no boilerplate; each zone page says what is particular to it, and every zone that needs gear or a field move to be safe has its own quest. New page on the bigger mods (Cold Sweat, Fight or Flight, Cobblebase, breeding, veins, shared loot, maps and info). - The Mire's poison stops 10 blocks below the surface. - Player battles never cost a Pokémon (CobbleHardcoreMon config, mixins, and the companion mod's own permadeath). - tools/item_audit.py: every item with a use has a source on the island; new sources for the gaps it found (species additions, three recipes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ace26e012
commit
d29e177f9d
65 files changed
+1920
-364
No files matched your search
@@ -1,5 +1,15 @@
|
||||
package dev.cobblesurvival.selftest
|
||||
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import net.minecraft.world.level.levelgen.Heightmap
|
||||
import net.minecraft.gametest.framework.GameTestAssertException
|
||||
import net.minecraft.world.item.crafting.RecipeType
|
||||
import net.minecraft.world.item.crafting.CraftingInput
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import dev.cobblesurvival.zone.Zones
|
||||
import dev.cobblesurvival.zone.PeacefulZones
|
||||
import com.cobblemon.mod.common.battles.BattleRegistry
|
||||
import com.cobblemon.mod.common.api.events.CobblemonEvents
|
||||
import com.cobblemon.mod.common.api.events.pokeball.PokeBallCaptureCalculatedEvent
|
||||
@@ -26,6 +36,8 @@ object CatchingTests {
|
||||
fun all() = listOf(
|
||||
SelfTestCase("catch_rate_falls_with_level_gap", 20, ::catchRate, batch = "catching"),
|
||||
SelfTestCase("failed_catch_enrages", 400, ::failedCatch, batch = "catching"),
|
||||
SelfTestCase("button_isle_pokemon_leave_players_alone", 200, ::peaceful, islandOnly = true, batch = "peaceful"),
|
||||
SelfTestCase("start_isle_materials_make_a_poke_ball", 20, ::startBall),
|
||||
SelfTestCase("wild_attack_starts_a_battle", 300, ::attackBattle, batch = "attack_battle"),
|
||||
SelfTestCase("wild_attack_hurts_without_a_party", 120, ::attackHurts, batch = "attack_battle"),
|
||||
SelfTestCase("strong_wild_pokemon_hunt_weak_trainers", 400, { s -> hostility(s, strongest = 5, hostile = true) },
|
||||
@@ -50,6 +62,57 @@ object CatchingTests {
|
||||
s.helper.succeed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adrian, 2026-11-10: on Button Isle a wild Pokémon leaves a player alone, even
|
||||
* one with no Pokémon whom it would otherwise hunt, unless the player hits it first.
|
||||
*/
|
||||
private fun peaceful(s: Scenario) {
|
||||
PeacefulZones.paused = false
|
||||
Catching.hostilityPaused = false
|
||||
s.afterTest {
|
||||
PeacefulZones.paused = true
|
||||
Catching.hostilityPaused = true
|
||||
}
|
||||
// On the island's surface: the platform's height counts as a cave biome.
|
||||
val sample = Zones.samples["button_isle"] ?: throw GameTestAssertException("no sample point for Button Isle")
|
||||
s.level.getChunk(sample.x shr 4, sample.z shr 4)
|
||||
val y = s.level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z).toDouble()
|
||||
val player = s.player()
|
||||
s.moveTo(player, sample.x + 0.5, y, sample.z + 0.5)
|
||||
val wild = s.wild(s.pokemon("primeape", 40, "karatechop"), Vec3(sample.x + 3.5, y, sample.z + 0.5))
|
||||
s.check(Zones.at(s.level, player.blockPosition())?.id == "button_isle") { "the player isn't on Button Isle" }
|
||||
val health = player.health
|
||||
s.helper.startSequence()
|
||||
.thenIdle(SPAWN_PROTECTION_TICKS)
|
||||
.thenExecute {
|
||||
s.check(Catching.ENRAGED !in wild.pokemon.forcedAspects) { "a stronger Pokémon was angered on Button Isle" }
|
||||
Catching.enrage(wild, player) // as if something else had set it on the player
|
||||
}
|
||||
.thenIdle(25)
|
||||
.thenExecute {
|
||||
s.check(!PeacefulZones.targets(wild, player)) { "the wild Pokémon still goes for the player" }
|
||||
s.check(!player.hurt(wild.damageSources().mobAttack(wild), 6f)) { "an unprovoked hit went through on Button Isle" }
|
||||
s.check(player.health == health) { "the player lost health" }
|
||||
wild.hurt(player.damageSources().playerAttack(player), 1f)
|
||||
s.check(player.hurt(wild.damageSources().mobAttack(wild), 6f)) { "a Pokémon the player hit couldn't hit back" }
|
||||
}
|
||||
.thenSucceed()
|
||||
}
|
||||
|
||||
/** Adrian, 2026-11-10: the start island can make a Poké Ball: four apricorns of any colour and coal or charcoal. */
|
||||
private fun startBall(s: Scenario) {
|
||||
val level = s.level
|
||||
fun item(id: String) = ItemStack(BuiltInRegistries.ITEM.get(ResourceLocation.parse(id)))
|
||||
for (fuel in listOf("minecraft:coal", "minecraft:charcoal")) {
|
||||
val grid = listOf(ItemStack.EMPTY, item("cobblemon:white_apricorn"), ItemStack.EMPTY, item("cobblemon:red_apricorn"), item(fuel),
|
||||
item("cobblemon:pink_apricorn"), ItemStack.EMPTY, item("cobblemon:green_apricorn"), ItemStack.EMPTY)
|
||||
val input = CraftingInput.of(3, 3, grid)
|
||||
val made = level.recipeManager.getRecipeFor(RecipeType.CRAFTING, input, level).map { it.value.assemble(input, level.registryAccess()) }.orElse(ItemStack.EMPTY)
|
||||
s.check(made.`is`(item("cobblemon:poke_ball").item)) { "four apricorns and $fuel make ${made.hoverName.string}, not a Poké Ball" }
|
||||
}
|
||||
s.helper.succeed()
|
||||
}
|
||||
|
||||
private fun attackBattle(s: Scenario) {
|
||||
val player = s.player()
|
||||
s.give(player, s.pokemon("charizard", 50, "ember"))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dev.cobblesurvival.selftest
|
||||
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.core.BlockPos
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import dev.cobblesurvival.zone.ZoneSample
|
||||
import dev.cobblesurvival.zone.Zones
|
||||
@@ -133,6 +135,7 @@ object HazardTests {
|
||||
val bare = s.player()
|
||||
val wading = s.player().also { it.setItemSlot(EquipmentSlot.FEET, ItemStack(waders)) }
|
||||
val outside = s.player()
|
||||
val below = s.player()
|
||||
fun surface(sample: ZoneSample): Double {
|
||||
level.getChunk(sample.x shr 4, sample.z shr 4)
|
||||
return level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z).toDouble()
|
||||
@@ -141,6 +144,10 @@ object HazardTests {
|
||||
.thenExecute {
|
||||
for (player in listOf(bare, wading)) s.moveTo(player, mire.x + 0.5, surface(mire), mire.z + 0.5)
|
||||
s.moveTo(outside, elsewhere.x + 0.5, surface(elsewhere), elsewhere.z + 0.5)
|
||||
// Deep in the rock under the Mire (a pocket cut for it), where the swamp's poison doesn't reach.
|
||||
val deep = BlockPos(mire.x + 2, surface(mire).toInt() - 30, mire.z)
|
||||
for (dy in 0..1) level.setBlockAndUpdate(deep.above(dy), Blocks.AIR.defaultBlockState())
|
||||
s.moveTo(below, deep.x + 0.5, deep.y.toDouble(), deep.z + 0.5)
|
||||
}
|
||||
.thenIdle(220)
|
||||
.thenExecute {
|
||||
@@ -148,6 +155,7 @@ object HazardTests {
|
||||
s.check(bare.hasEffect(poison)) { "a player without waders in the Mire isn't poisoned" }
|
||||
s.check(!wading.hasEffect(poison)) { "a player in Mire Waders got poisoned" }
|
||||
s.check(!outside.hasEffect(poison)) { "a player on Button Isle got poisoned" }
|
||||
s.check(!below.hasEffect(poison)) { "a player deep under the Mire got poisoned" }
|
||||
}
|
||||
.thenSucceed()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.cobblesurvival.selftest
|
||||
|
||||
import dev.cobblesurvival.permadeath.Permadeath
|
||||
import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.api.npc.NPCClasses
|
||||
import com.cobblemon.mod.common.api.pokemon.status.Statuses
|
||||
@@ -37,6 +38,7 @@ object PermadeathTests {
|
||||
SelfTestCase("wild_battle_charm", BATTLE_TICKS, { wildBattle(it, charm = true) }),
|
||||
SelfTestCase("revived_during_battle", BATTLE_TICKS, ::revivedDuringBattle),
|
||||
SelfTestCase("trainer_battle", BATTLE_TICKS, ::trainerBattle),
|
||||
SelfTestCase("player_battle_only_faints", BATTLE_TICKS, ::playerBattle),
|
||||
)
|
||||
|
||||
/** Cobblemon heals the party while the player sleeps and wakes fainted Pokémon after a while; both are off. */
|
||||
@@ -160,6 +162,32 @@ object PermadeathTests {
|
||||
.thenSucceed()
|
||||
}
|
||||
|
||||
/** Adrian, 2026-11-10: a Pokémon that faints in a battle between players stays, fainted, until healed. */
|
||||
private fun playerBattle(s: Scenario) {
|
||||
val loser = s.player()
|
||||
val winner = s.player()
|
||||
val pokemon = s.give(loser, s.pokemon("bidoof", 5, "explosion"))
|
||||
s.give(winner, s.pokemon("bidoof", 50, "splash"))
|
||||
s.answerBattles(loser) { _, _ -> MoveActionResponse("explosion") }
|
||||
s.answerBattles(winner) { _, _ -> MoveActionResponse("splash") }
|
||||
var ended = -1L
|
||||
s.helper.startSequence()
|
||||
.thenExecute { s.started(BattleBuilder.pvp1v1(loser, winner)) }
|
||||
.thenWaitUntil {
|
||||
s.check(!loser.isInBattle()) { "the battle is still running: ${s.battleState(loser)}" }
|
||||
if (ended < 0) ended = s.level.gameTime
|
||||
s.check(s.level.gameTime - ended > 60) { "waiting for the permadeath sweeps" }
|
||||
}
|
||||
.thenExecute {
|
||||
s.check(Cobblemon.storage.getParty(loser).any { it.uuid == pokemon.uuid }) { "the Pokémon that fainted in a player battle was removed" }
|
||||
s.check(pokemon.isFainted()) { "it isn't fainted" }
|
||||
pokemon.heal()
|
||||
}
|
||||
.thenIdle(30)
|
||||
.thenExecute { s.check(!pokemon.persistentData.contains(Permadeath.PVP_FAINT)) { "healed, it still carries the player-battle mark" } }
|
||||
.thenSucceed()
|
||||
}
|
||||
|
||||
private fun wildBattle(s: Scenario, charm: Boolean) {
|
||||
val player = s.player()
|
||||
val pokemon = s.give(player, s.pokemon("bidoof", 5, "explosion"))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.cobblesurvival.selftest
|
||||
|
||||
import dev.cobblesurvival.zone.PeacefulZones
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import dev.cobblesurvival.catching.Catching
|
||||
@@ -104,6 +105,7 @@ object SelfTest {
|
||||
running = scenarios
|
||||
Catching.hostilityPaused = true
|
||||
Wanderers.paused = true
|
||||
PeacefulZones.paused = true
|
||||
GameTestTicker.SINGLETON.clear()
|
||||
GameTestRunner.Builder.fromInfo(infos, level)
|
||||
.newStructureSpawner(StructureGridSpawner(origin, TESTS_PER_ROW, false))
|
||||
@@ -135,5 +137,6 @@ object SelfTest {
|
||||
running = emptyList()
|
||||
Catching.hostilityPaused = false
|
||||
Wanderers.paused = false
|
||||
PeacefulZones.paused = false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user