Big, tough bosses; working machines show it; JEI drop pages on the roadmap
Bosses (Adrian, 2026-11-02: Eternatus too small and too easy; guardians way bigger and tougher; only defensive stats up; no attack takes more than 25 %): - Guardians scale to about 8 blocks tall (1.5x-12x), Eternatus to 15 blocks (2.5x, was 0.2x). Cave guardians look for a spot with room above them. Bosses already in a world grow on the next check outside battle. - Perfect IVs and 170 EVs each in HP, Defense and Sp. Def, nothing offensive. - The Guardian's Ward, a Showdown-only held item (held_items/guardiansward.js) handed to every boss by a HeldItemProvider: Defense and Sp. Def x2 while defending, and one attack (all hits together) takes at most 25 % of max HP. - Area attacks that start at the boss reach half its width further. Machines (Adrian, 2026-11-02): a working core is active: it glows, its front animates (flickering fire on the furnace core, pulsing emblems elsewhere), and particles show in the chamber and at the core (client side). The Pokemon inside flinches about every 15 s (red flash, Cobblemon's hit sound, no damage). Roadmap Phase 8: every item that can drop gets a JEI page naming its sources. Self-tests: practice_bosses_spawn checks size and training, boss_takes_a_quarter_per_attack runs a real battle (each Lv 100 Bullet Seed takes exactly a quarter), heat_core_cooks_with_hoppers checks the active state and the flinch. Self-tests and the island check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4f0bc03919
commit
120c664cb0
58 files changed
+681
-86
No files matched your search
@@ -2,10 +2,14 @@ package dev.cobblesurvival.selftest
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.pokemon.stats.Stats
|
||||
import com.cobblemon.mod.common.battles.MoveActionResponse
|
||||
import com.cobblemon.mod.common.battles.BattleRegistry
|
||||
import com.cobblemon.mod.common.util.getBattleState
|
||||
import dev.cobblesurvival.boss.Area
|
||||
import dev.cobblesurvival.boss.BossDefs
|
||||
import dev.cobblesurvival.boss.BossFights
|
||||
import dev.cobblesurvival.boss.BossToughness
|
||||
import dev.cobblesurvival.boss.EndBoss
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
import net.minecraft.core.BlockPos
|
||||
@@ -33,8 +37,13 @@ object BossTests {
|
||||
SelfTestCase("boss_arena_leaving_loses", 400, ::arena),
|
||||
SelfTestCase("end_boss_replaces_the_dragon", 200, ::endBoss),
|
||||
SelfTestCase("practice_bosses_spawn", 20, ::practice),
|
||||
SelfTestCase("boss_takes_a_quarter_per_attack", 1200, ::damageCap),
|
||||
)
|
||||
|
||||
/** Guardians stand at least this tall (Morpeko, the smallest, reaches about 5 blocks), the End boss [END_HEIGHT]. */
|
||||
private const val GUARDIAN_HEIGHT = 4.5f
|
||||
private const val END_HEIGHT = 12f
|
||||
|
||||
/** `/cobblesurvival boss <zone|end>`: a copy that fights like the real boss, for every guardian and the End. */
|
||||
private fun practice(s: Scenario) {
|
||||
for (which in Guardians.definitions.keys + "end") {
|
||||
@@ -42,11 +51,51 @@ object BossTests {
|
||||
?: throw GameTestAssertException("no practice boss for $which")
|
||||
s.check(BossFights.defOf(entity) != null && entity.pokemon.isUncatchable()) { "the practice $which is no uncatchable boss" }
|
||||
s.check(entity.pokemon.persistentData.getString(Guardians.TAG).isEmpty()) { "the practice $which counts as the real guardian" }
|
||||
// Adrian, 2026-11-02: way bigger, tougher but not stronger.
|
||||
val tall = if (which == "end") END_HEIGHT else GUARDIAN_HEIGHT
|
||||
s.check(entity.bbHeight >= tall) { "the $which boss is only %.1f blocks tall".format(entity.bbHeight) }
|
||||
val p = entity.pokemon
|
||||
val defensive = listOf(Stats.HP, Stats.DEFENCE, Stats.SPECIAL_DEFENCE)
|
||||
s.check(defensive.all { p.ivs[it] == 31 && p.evs[it] == BossToughness.EVS }) { "the $which boss lacks its HP/Defense/Sp. Def training" }
|
||||
s.check(listOf(Stats.ATTACK, Stats.SPECIAL_ATTACK, Stats.SPEED).all { p.evs[it] == 0 }) { "the $which boss has offensive EVs" }
|
||||
s.check(p.currentHealth == p.maxHealth && BossToughness.isBoss(p)) { "the $which boss isn't at full health or isn't a boss" }
|
||||
entity.discard()
|
||||
}
|
||||
s.helper.succeed()
|
||||
}
|
||||
|
||||
/**
|
||||
* The Guardian's Ward (Adrian, 2026-11-02: no single attack takes more than a quarter of a boss's HP): a Lv 100
|
||||
* Garchomp's Bullet Seed, every hit enough to knock out a Lv 15 boss, takes a quarter at most per use.
|
||||
*/
|
||||
private fun damageCap(s: Scenario) {
|
||||
val player = s.player()
|
||||
s.give(player, s.pokemon("garchomp", 100, "bulletseed"))
|
||||
val bossPokemon = s.pokemon("morpeko", 15, "splash")
|
||||
bossPokemon.persistentData.putString(BossFights.TAG, TEST_BOSS)
|
||||
val boss = s.wild(bossPokemon, s.at(7.5, 1.0, 3.5))
|
||||
val battle = { BattleRegistry.getBattleByParticipatingPlayer(player) }
|
||||
s.afterTest { battle()?.end() }
|
||||
// Two attacks, then the battle waits for a third choice that never comes.
|
||||
var attacks = 0
|
||||
s.answerBattles(player) { actor, _ ->
|
||||
if (attacks == 2) null else MoveActionResponse("bulletseed", actor.getSide().getOppositeSide().activePokemon.first().getPNX()).also { attacks++ }
|
||||
}
|
||||
s.startWildBattle(player, boss)
|
||||
.thenWaitUntil {
|
||||
val b = battle()
|
||||
val waiting = player.getBattleState()?.second?.let { it.mustChoose && it.request != null } == true
|
||||
s.check(b == null || (attacks == 2 && b.turn >= 3 && waiting)) { "two attacks aren't done yet: ${s.battleState(player)}" }
|
||||
}
|
||||
.thenExecute {
|
||||
val max = boss.pokemon.maxHealth
|
||||
val hp = boss.pokemon.currentHealth
|
||||
s.check(hp in (max - 2 * (max / 4))..<max) { "after two attacks that each could knock it out, the boss has $hp of $max HP (a quarter at most per attack)" }
|
||||
battle()?.end()
|
||||
}
|
||||
.thenSucceed()
|
||||
}
|
||||
|
||||
private fun implemented(species: String) = PokemonSpecies.getByName(species)?.implemented == true
|
||||
|
||||
private fun coverage(s: Scenario) {
|
||||
|
||||
@@ -247,7 +247,17 @@ object MachineTests {
|
||||
level.setBlockAndUpdate(furnace.below(), Blocks.HOPPER.defaultBlockState().setValue(HopperBlock.FACING, Direction.DOWN))
|
||||
(level.getBlockEntity(furnace.above()) as HopperBlockEntity).setItem(0, ItemStack(Items.RAW_IRON, 4))
|
||||
val output = level.getBlockEntity(furnace.below()) as HopperBlockEntity
|
||||
val active = { core.blockState.getValue(MachineBlock.ACTIVE) }
|
||||
s.helper.startSequence()
|
||||
// Adrian, 2026-11-02: a working machine shows it (the core glows and animates, particles show).
|
||||
.thenWaitUntil { s.check(active()) { "the Large Furnace's core doesn't show that it works" } }
|
||||
.thenExecute {
|
||||
// The Pokémon inside flinches as if hit, but takes no damage.
|
||||
val shown = displays(level, core.blockPos).firstOrNull() ?: throw GameTestAssertException("no Pokémon shown in the chamber")
|
||||
MachineDisplay.flinch(level, shown)
|
||||
val inside = core.pokemon!!
|
||||
s.check(shown.isAlive && inside.currentHealth == inside.maxHealth) { "a flinch hurt the Pokémon (${inside.currentHealth}/${inside.maxHealth})" }
|
||||
}
|
||||
.thenWaitUntil {
|
||||
val ingots = (0 until output.containerSize).sumOf { output.getItem(it).takeIf { s -> s.`is`(Items.IRON_INGOT) }?.count ?: 0 }
|
||||
s.check(ingots >= 4) { "the hopper under the furnace has $ingots iron ingots, not 4" }
|
||||
@@ -256,6 +266,7 @@ object MachineTests {
|
||||
val fuel = (level.getBlockEntity(furnace) as AbstractFurnaceBlockEntity).getItem(1)
|
||||
s.check(fuel.isEmpty) { "the furnace got fuel: $fuel" }
|
||||
}
|
||||
.thenWaitUntil { s.check(!active()) { "the Large Furnace's core still shows work with nothing left to cook" } }
|
||||
.thenSucceed()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user