Phase 8: the pack's own JEI pages for every drop source
Adrian, 2026-11-02/03: every item that can drop gets a JEI page naming its sources, alongside the Cobblemon Info add-on (which shows drops and biomes, not zones). The companion mod's JEI plugin (jei/DropSourcesJei.kt) adds: - Pokemon drops on the island: 794 species of lines that live on the island, with chances, amounts and the zones they spawn in (or where their line lives); spawns with their own drops (Beheeyem in the End). The Drop Synthesizer core is the catalyst. - Guardians and the End boss; Sacrifice Altar tiers and specials; Rebirth Chamber; zone ore veins with regrow times; Cobblebase pasture finds per skill and zone. JEI runs on the client, so the pages read assets/cobblesurvival/jei/ drop_sources.json inside the mod, written by tools/jei_sources.py from the pack's files. Self-tests drop_sources_match_the_server (file vs live server: drops, zones, own-drop spawns, guardians, End boss, altar) and jei_pages_build (every page builds, every item exists). The ore a vein gives moved into world/zones.json (ores -> item), shared with the quest book. JEI's API (19.64.0.455) is a compile-only dependency from JEI's own Maven. Self-tests and the island check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c5d1cf93f4
commit
72965caedf
13 files changed
+19637
-49
No files matched your search
@@ -1,14 +1,24 @@
|
||||
package dev.cobblesurvival.selftest
|
||||
|
||||
import com.cobblemon.mod.common.api.drop.DropTable
|
||||
import com.cobblemon.mod.common.api.drop.ItemDropEntry
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
|
||||
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
|
||||
import dev.cobblesurvival.boss.BossDefs
|
||||
import dev.cobblesurvival.jei.DropSources
|
||||
import dev.cobblesurvival.jei.DropSourcesJei
|
||||
import dev.cobblesurvival.machine.MachineDefs
|
||||
import dev.cobblesurvival.progress.QuestEvents
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
import dev.cobblesurvival.zone.Zones
|
||||
import java.util.function.Consumer
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.gametest.framework.GameTestAssertException
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.levelgen.Heightmap
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Roadmap Phase 8 (Adrian, 2026-11-03): the add-ons the quest book and JEI lean on work with the pack. Read by
|
||||
@@ -20,8 +30,73 @@ object QuestTests {
|
||||
SelfTestCase("jei_addon_shows_island_spawns", 20, ::addonSpawns),
|
||||
SelfTestCase("quest_book_loads", 20, ::bookLoads),
|
||||
SelfTestCase("quest_events_mark_zones", 200, ::zoneEvent),
|
||||
SelfTestCase("drop_sources_match_the_server", 20, ::dropSources),
|
||||
SelfTestCase("jei_pages_build", 20, ::jeiPages),
|
||||
)
|
||||
|
||||
/**
|
||||
* Every JEI page builds (the same code JEI calls on the client), one per Pokémon, guardian, altar tier, vein zone
|
||||
* and pasture list, and every item on them exists (an unknown id would show as a barrier).
|
||||
*/
|
||||
private fun jeiPages(s: Scenario) {
|
||||
val data = DropSources.data
|
||||
val pages = mapOf(
|
||||
"pokemon" to DropSourcesJei.pokemonPages(), "bosses" to DropSourcesJei.bossPages(), "altar" to DropSourcesJei.altarPages(),
|
||||
"rebirth" to DropSourcesJei.rebirthPages(), "veins" to DropSourcesJei.veinPages(), "finds" to DropSourcesJei.findPages(),
|
||||
)
|
||||
s.check(pages.getValue("pokemon").size == data.pokemon.size + data.spawn_drops.size) { "${pages.getValue("pokemon").size} Pokémon pages for ${data.pokemon.size} Pokémon" }
|
||||
s.check(pages.getValue("bosses").size == data.guardians.size + 1) { "${pages.getValue("bosses").size} boss pages" }
|
||||
val unknown = pages.values.flatten().flatMap { it.outputs }.map { it.stack }.filter { it.`is`(Items.BARRIER) || it.isEmpty }
|
||||
s.check(unknown.isEmpty()) { "${unknown.size} items on the JEI pages don't exist" }
|
||||
val noPicture = pages.getValue("pokemon").count { it.icon?.`is`(Items.BARRIER) != false }
|
||||
s.check(noPicture == 0) { "$noPicture Pokémon pages without a picture" }
|
||||
s.helper.succeed()
|
||||
}
|
||||
|
||||
/** A drop as "item chance% ×count", the same way from the JEI data and from Cobblemon's live tables. */
|
||||
private fun dropKey(item: String, chance: Double?, count: String?) = "$item ${"%.2f".format(chance ?: 100.0)}% ×${count ?: "1"}"
|
||||
|
||||
private fun liveDrops(table: DropTable?): Set<String> = table?.entries.orEmpty().filterIsInstance<ItemDropEntry>().map { e ->
|
||||
dropKey(e.item.toString(), e.percentage.toDouble().takeIf { it < 100.0 }, e.quantityRange?.let { if (it.first == it.last) "${it.first}" else "${it.first}-${it.last}" } ?: e.quantity.toString())
|
||||
}.toSet()
|
||||
|
||||
/**
|
||||
* The JEI pages' data (assets/cobblesurvival/jei/drop_sources.json, tools/jei_sources.py) still says what the
|
||||
* server does: every Pokémon's drops and zones, the spawns with drops of their own, the guardians, the End boss
|
||||
* and the Sacrifice Altar. When this fails, run tools/jei_sources.py and rebuild.
|
||||
*/
|
||||
private fun dropSources(s: Scenario) {
|
||||
val data = DropSources.data
|
||||
val problems = mutableListOf<String>()
|
||||
val details = CobblemonSpawnPools.WORLD_SPAWN_POOL.details.filterIsInstance<PokemonSpawnDetail>()
|
||||
val zonesOf = details.groupBy({ "cobblemon:${it.pokemon.species?.lowercase()}" }, { it.id.substringBefore('-') }).mapValues { it.value.toSet() }
|
||||
for (p in data.pokemon) {
|
||||
val species = PokemonSpecies.getByIdentifier(ResourceLocation.parse(p.species)) ?: continue.also { problems += "no species ${p.species}" }
|
||||
val shown = p.drops.map { dropKey(it.item, it.chance, it.count) }.toSet()
|
||||
if (shown != liveDrops(species.drops)) problems += "${p.species} drops ${liveDrops(species.drops)}, the JEI data says $shown"
|
||||
if (p.zones.map { it.zone }.toSet() != zonesOf[p.species].orEmpty()) problems += "${p.species} spawns in ${zonesOf[p.species]}, the JEI data says ${p.zones.map { it.zone }}"
|
||||
}
|
||||
val ownDrops = details.filter { it.drops != null }.map { "cobblemon:${it.pokemon.species?.lowercase()} in ${it.id.substringBefore('-')}" }.toSet()
|
||||
val shownOwn = data.spawn_drops.map { "${it.species} in ${it.zone}" }.toSet()
|
||||
if (ownDrops != shownOwn) problems += "spawns with their own drops: $ownDrops, the JEI data says $shownOwn"
|
||||
for (g in data.guardians) {
|
||||
val def = Guardians.definitions[g.zone] ?: continue.also { problems += "no guardian for ${g.zone}" }
|
||||
val live = "${def.pokemon.substringBefore(' ')} ${Regex("level=(\\d+)").find(def.pokemon)?.groupValues?.get(1)} ${def.drops.map { "${it.first} ×${it.second}" }}"
|
||||
val shown = "${g.species.substringAfter(':')} ${g.level} ${g.drops.map { "${it.item} ×${it.count}" }}"
|
||||
if (live != shown) problems += "guardian of ${g.zone}: $live, the JEI data says $shown"
|
||||
}
|
||||
if (data.guardians.size != Guardians.definitions.size) problems += "${Guardians.definitions.size} guardians, the JEI data has ${data.guardians.size}"
|
||||
BossDefs.end?.end?.let { end ->
|
||||
if ("cobblemon:${end.species}" != data.end_boss.species || end.level != data.end_boss.level) problems += "the End boss is ${end.species} Lv ${end.level}"
|
||||
}
|
||||
val altar = MachineDefs.byCore.values.firstNotNullOfOrNull { it.altar }
|
||||
val liveTiers = altar?.tiers.orEmpty().map { t -> "${t.minLevel}: ${t.rewards.map { "${BuiltInRegistries.ITEM.getKey(it.item)} ×${it.count}" }}" }
|
||||
val shownTiers = data.altar.tiers.map { t -> "${t.min_level}: ${t.rewards.map { "${it.item} ×${it.count}" }}" }
|
||||
if (liveTiers != shownTiers) problems += "altar tiers $liveTiers, the JEI data says $shownTiers"
|
||||
s.check(problems.isEmpty()) { "${problems.size} differences (run tools/jei_sources.py): ${problems.take(4).joinToString("; ")}" }
|
||||
s.helper.succeed()
|
||||
}
|
||||
|
||||
/** At least this many quests in the book (docs/quests.yaml: 74 on 2026-11-03). */
|
||||
private const val MIN_QUESTS = 70
|
||||
|
||||
|
||||
Reference in new issue
Block a user