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
@@ -0,0 +1,61 @@
|
||||
package dev.cobblesurvival.jei
|
||||
|
||||
import com.google.gson.Gson
|
||||
import net.minecraft.core.registries.BuiltInRegistries
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
|
||||
/**
|
||||
* Where items come from on the island (roadmap Phase 8), for the pack's JEI pages ([DropSourcesJei]):
|
||||
* `assets/cobblesurvival/jei/drop_sources.json`, written by tools/jei_sources.py from the pack's data. It ships inside
|
||||
* the mod because JEI runs on the client, which on a dedicated server has none of the server's data; the self-test
|
||||
* `drop_sources_match_the_server` checks it against the live server instead. Fields as tools/jei_sources.py writes them.
|
||||
*/
|
||||
object DropSources {
|
||||
class Drop(val item: String, val count: String?, val chance: Double?)
|
||||
class Reward(val item: String, val count: Int)
|
||||
class Spawned(val zone: String, val levels: String)
|
||||
class LineMember(val zone: String, val species: String)
|
||||
class Pokemon(val species: String, val drops: List<Drop>, val zones: List<Spawned>, val line: List<LineMember>?)
|
||||
class SpawnDrops(val species: String, val zone: String, val drops: List<Drop>)
|
||||
class Guardian(val zone: String, val species: String, val level: Int, val drops: List<Reward>, val respawn_hours: Double)
|
||||
class EndBoss(val species: String, val level: Int, val xp_first_win: Int, val xp: Int, val respawn_hours: Double, val first_win: List<Reward>)
|
||||
class AltarTier(val min_level: Int, val rewards: List<Reward>)
|
||||
class AltarSpecial(val min_level: Int, val types: List<String>, val rewards: List<Reward>)
|
||||
class Altar(val tiers: List<AltarTier>, val specials: List<AltarSpecial>)
|
||||
class VeinItem(val item: String, val regen_minutes: Int)
|
||||
class Veins(val zone: String, val items: List<VeinItem>)
|
||||
class Find(val item: String, val tiers: List<String>)
|
||||
class Finds(val skill: String, val zone: String?, val items: List<Find>)
|
||||
class Zone(val name: String, val levels: List<Int>)
|
||||
|
||||
class Data(
|
||||
val zones: Map<String, Zone>,
|
||||
val pokemon: List<Pokemon>,
|
||||
val spawn_drops: List<SpawnDrops>,
|
||||
val guardians: List<Guardian>,
|
||||
val end_boss: EndBoss,
|
||||
val altar: Altar,
|
||||
val rebirth: List<Reward>,
|
||||
val ore_veins: List<Veins>,
|
||||
val pasture_finds: List<Finds>,
|
||||
)
|
||||
|
||||
const val PATH = "/assets/cobblesurvival/jei/drop_sources.json"
|
||||
|
||||
val data: Data by lazy {
|
||||
val stream = DropSources::class.java.getResourceAsStream(PATH) ?: error("$PATH is missing from the mod")
|
||||
stream.reader(Charsets.UTF_8).use { Gson().fromJson(it, Data::class.java) }
|
||||
}
|
||||
|
||||
fun zoneName(zone: String?): String = zone?.let { data.zones[it]?.name ?: it } ?: "anywhere"
|
||||
|
||||
fun stack(item: String, count: Int = 1): ItemStack {
|
||||
val found = BuiltInRegistries.ITEM.getOptional(ResourceLocation.parse(item)).orElse(Items.BARRIER)
|
||||
return ItemStack(found, count.coerceIn(1, 99))
|
||||
}
|
||||
|
||||
/** The most a drop gives: "0-2" → 2. */
|
||||
fun most(count: String?): Int = count?.substringAfter('-')?.toIntOrNull() ?: 1
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package dev.cobblesurvival.jei
|
||||
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
|
||||
import com.cobblemon.mod.common.item.PokemonItem
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import mezz.jei.api.IModPlugin
|
||||
import mezz.jei.api.JeiPlugin
|
||||
import mezz.jei.api.gui.builder.IRecipeLayoutBuilder
|
||||
import mezz.jei.api.gui.builder.ITooltipBuilder
|
||||
import mezz.jei.api.gui.drawable.IDrawable
|
||||
import mezz.jei.api.gui.ingredient.IRecipeSlotsView
|
||||
import mezz.jei.api.recipe.IFocusGroup
|
||||
import mezz.jei.api.recipe.RecipeIngredientRole
|
||||
import mezz.jei.api.recipe.RecipeType
|
||||
import mezz.jei.api.recipe.category.IRecipeCategory
|
||||
import mezz.jei.api.registration.IRecipeCatalystRegistration
|
||||
import mezz.jei.api.registration.IRecipeCategoryRegistration
|
||||
import mezz.jei.api.registration.IRecipeRegistration
|
||||
import net.minecraft.ChatFormatting
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.gui.GuiGraphics
|
||||
import net.minecraft.network.chat.Component
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.world.item.ItemStack
|
||||
|
||||
/**
|
||||
* The pack's own JEI pages (roadmap Phase 8; Adrian, 2026-11-02: every item that can drop gets a page naming where it
|
||||
* drops). Look an item up (its recipes) to see each place on the island it comes from: the Pokémon that drop it and
|
||||
* the zones they live in (the Cobblemon Info add-on shows drops and biomes, not zones), guardians and the End boss,
|
||||
* the Sacrifice Altar and the Rebirth Chamber, zone ore veins, and what pastured Pokémon find. Built from
|
||||
* [DropSources]; JEI finds this class by its annotation, on the client only.
|
||||
*/
|
||||
@JeiPlugin
|
||||
class DropSourcesJei : IModPlugin {
|
||||
/** One page: a picture (a Pokémon or a block), up to three lines of text, and the items it gives. */
|
||||
class Page(val icon: ItemStack?, val lines: List<Component>, val outputs: List<Output>)
|
||||
class Output(val stack: ItemStack, val notes: List<Component>)
|
||||
|
||||
private class Kind(val path: String, val title: String, val icon: String, val catalysts: List<String>, val pages: () -> List<Page>) {
|
||||
val type: RecipeType<Page> = RecipeType.create(CobbleSurvival.MOD_ID, path, Page::class.java)
|
||||
val built by lazy(pages)
|
||||
}
|
||||
|
||||
private val kinds = listOf(
|
||||
Kind("pokemon_drops", "Pokémon drops on the island", "cobblemon:poke_ball", listOf("cobblesurvival:synthesis_core"), ::pokemonPages),
|
||||
Kind("boss_rewards", "Guardians and the End boss", "minecraft:netherite_sword", emptyList(), ::bossPages),
|
||||
Kind("altar", "Sacrifice Altar", "cobblesurvival:altar_core", listOf("cobblesurvival:altar_core"), ::altarPages),
|
||||
Kind("rebirth", "Rebirth Chamber", "cobblesurvival:rebirth_core", listOf("cobblesurvival:rebirth_core"), ::rebirthPages),
|
||||
Kind("ore_veins", "Zone ore veins", "minecraft:iron_pickaxe", emptyList(), ::veinPages),
|
||||
Kind("pasture_finds", "Pasture finds", "cobblemon:pasture", listOf("cobblemon:pasture"), ::findPages),
|
||||
)
|
||||
|
||||
override fun getPluginUid(): ResourceLocation = ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "drop_sources")
|
||||
|
||||
override fun registerCategories(registration: IRecipeCategoryRegistration) {
|
||||
val gui = registration.jeiHelpers.guiHelper
|
||||
registration.addRecipeCategories(*kinds.map { kind ->
|
||||
val rows = kind.built.maxOfOrNull { (it.outputs.size + PER_ROW - 1) / PER_ROW }?.coerceAtLeast(1) ?: 1
|
||||
PageCategory(kind.type, Component.literal(kind.title), gui.createDrawableItemStack(DropSources.stack(kind.icon)), rows)
|
||||
}.toTypedArray())
|
||||
}
|
||||
|
||||
override fun registerRecipes(registration: IRecipeRegistration) {
|
||||
kinds.forEach { registration.addRecipes(it.type, it.built) }
|
||||
}
|
||||
|
||||
override fun registerRecipeCatalysts(registration: IRecipeCatalystRegistration) {
|
||||
kinds.forEach { kind -> kind.catalysts.forEach { registration.addRecipeCatalyst(DropSources.stack(it), kind.type) } }
|
||||
}
|
||||
|
||||
class PageCategory(private val type: RecipeType<Page>, private val title: Component, private val icon: IDrawable, private val rows: Int) : IRecipeCategory<Page> {
|
||||
override fun getRecipeType() = type
|
||||
override fun getTitle() = title
|
||||
override fun getIcon() = icon
|
||||
override fun getWidth() = WIDTH
|
||||
override fun getHeight() = TEXT_HEIGHT + rows * SLOT
|
||||
|
||||
override fun setRecipe(builder: IRecipeLayoutBuilder, page: Page, focuses: IFocusGroup) {
|
||||
page.icon?.let { builder.addSlot(RecipeIngredientRole.RENDER_ONLY, 1, 1).addItemStack(it) }
|
||||
page.outputs.forEachIndexed { i, out ->
|
||||
builder.addSlot(RecipeIngredientRole.OUTPUT, 1 + (i % PER_ROW) * SLOT, TEXT_HEIGHT + 1 + (i / PER_ROW) * SLOT)
|
||||
.setStandardSlotBackground()
|
||||
.addItemStack(out.stack)
|
||||
.addRichTooltipCallback { _, tooltip -> out.notes.forEach(tooltip::add) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun textLeft(page: Page) = if (page.icon != null) SLOT + 4 else 0
|
||||
|
||||
override fun draw(page: Page, view: IRecipeSlotsView, graphics: GuiGraphics, mouseX: Double, mouseY: Double) {
|
||||
val font = Minecraft.getInstance().font
|
||||
val left = textLeft(page)
|
||||
page.lines.forEachIndexed { i, line ->
|
||||
font.split(line, WIDTH - left).firstOrNull()?.let { graphics.drawString(font, it, left, 1 + i * LINE, TEXT_COLOR, false) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Lines too long for the page show in full when hovered. */
|
||||
override fun getTooltip(tooltip: ITooltipBuilder, page: Page, view: IRecipeSlotsView, mouseX: Double, mouseY: Double) {
|
||||
if (mouseX >= textLeft(page) && mouseY < TEXT_HEIGHT - 2) page.lines.forEach(tooltip::add)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val WIDTH = 162
|
||||
private const val SLOT = 18
|
||||
private const val PER_ROW = 9
|
||||
private const val LINE = 10
|
||||
private const val TEXT_HEIGHT = 32
|
||||
private const val TEXT_COLOR = 0x404040
|
||||
|
||||
private fun gray(text: String): Component = Component.literal(text).withStyle(ChatFormatting.GRAY)
|
||||
|
||||
private fun speciesName(id: String): Component =
|
||||
PokemonSpecies.getByIdentifier(ResourceLocation.parse(id))?.translatedName ?: Component.literal(id.substringAfter(':'))
|
||||
|
||||
private fun picture(id: String): ItemStack =
|
||||
runCatching { PokemonItem.from(PokemonProperties.parse(id.substringAfter(':'))) }.getOrNull()?.takeIf { !it.isEmpty }
|
||||
?: DropSources.stack("cobblemon:poke_ball")
|
||||
|
||||
private fun dropOutput(drop: DropSources.Drop): Output {
|
||||
val notes = listOfNotNull(
|
||||
drop.chance?.let { gray("%s%% chance".format(if (it % 1.0 == 0.0) it.toInt() else it)) } ?: gray("always"),
|
||||
drop.count?.takeIf { it != "1" }?.let { gray("×${it.replace("-", "–")}") },
|
||||
)
|
||||
return Output(DropSources.stack(drop.item, DropSources.most(drop.count)), notes)
|
||||
}
|
||||
|
||||
private fun rewardOutputs(rewards: List<DropSources.Reward>, note: Component? = null) =
|
||||
rewards.map { Output(DropSources.stack(it.item, it.count), listOfNotNull(note)) }
|
||||
|
||||
private fun levels(zone: String, levels: String) = "${DropSources.zoneName(zone)} (Lv ${levels.replace("-", "–")})"
|
||||
|
||||
fun pokemonPages(): List<Page> {
|
||||
val data = DropSources.data
|
||||
val wild = data.pokemon.map { p ->
|
||||
val where = if (p.zones.isNotEmpty()) {
|
||||
"Lives in " + p.zones.joinToString(", ") { levels(it.zone, it.levels) }
|
||||
} else {
|
||||
"Not wild; its line lives in " + p.line.orEmpty().joinToString(", ") {
|
||||
"${DropSources.zoneName(it.zone)} (${speciesName(it.species).string})"
|
||||
}
|
||||
}
|
||||
Page(picture(p.species), listOf(speciesName(p.species).copy().withStyle(ChatFormatting.BOLD), Component.literal(where),
|
||||
gray("The Drop Synthesizer makes the same drops")), p.drops.map(::dropOutput))
|
||||
}
|
||||
val special = data.spawn_drops.map { s ->
|
||||
Page(picture(s.species), listOf(speciesName(s.species).copy().withStyle(ChatFormatting.BOLD),
|
||||
Component.literal("In ${DropSources.zoneName(s.zone)}: drops these instead")), s.drops.map(::dropOutput))
|
||||
}
|
||||
return wild + special
|
||||
}
|
||||
|
||||
fun bossPages(): List<Page> {
|
||||
val data = DropSources.data
|
||||
val guardians = data.guardians.map { g ->
|
||||
Page(picture(g.species), listOf(
|
||||
speciesName(g.species).copy().append(", Lv ${g.level}").withStyle(ChatFormatting.BOLD),
|
||||
Component.literal("Guardian of ${DropSources.zoneName(g.zone)}"),
|
||||
gray("Back %s hours after a win".format(g.respawn_hours.let { if (it % 1.0 == 0.0) it.toInt() else it })),
|
||||
), rewardOutputs(g.drops, gray("for everyone in the winning battle")))
|
||||
}
|
||||
val end = data.end_boss
|
||||
val boss = Page(picture(end.species), listOf(
|
||||
speciesName(end.species).copy().append(", Lv ${end.level}").withStyle(ChatFormatting.BOLD),
|
||||
Component.literal("The End's boss"),
|
||||
gray("${end.xp_first_win} XP the first time, ${end.xp} after"),
|
||||
), rewardOutputs(end.first_win, gray("first win only")))
|
||||
return guardians + boss
|
||||
}
|
||||
|
||||
fun altarPages(): List<Page> {
|
||||
val altar = DropSources.data.altar
|
||||
return altar.tiers.map { t ->
|
||||
Page(null, listOf(Component.literal("Sacrificing a Pokémon of Lv ${t.min_level}+").withStyle(ChatFormatting.BOLD),
|
||||
gray("The highest tier it reaches decides")), rewardOutputs(t.rewards))
|
||||
} + altar.specials.map { s ->
|
||||
Page(null, listOf(Component.literal("A ${s.types.joinToString("/") { it.replaceFirstChar(Char::uppercase) }} type of Lv ${s.min_level}+").withStyle(ChatFormatting.BOLD),
|
||||
gray("also gives")), rewardOutputs(s.rewards))
|
||||
}
|
||||
}
|
||||
|
||||
fun rebirthPages(): List<Page> = listOf(
|
||||
Page(null, listOf(Component.literal("A Lv 100 Pokémon reborn").withStyle(ChatFormatting.BOLD),
|
||||
gray("as a shiny Lv 1 of its first stage,"), gray("comes back with")), rewardOutputs(DropSources.data.rebirth)),
|
||||
)
|
||||
|
||||
fun veinPages(): List<Page> = DropSources.data.ore_veins.map { v ->
|
||||
Page(null, listOf(Component.literal("Ore veins of ${DropSources.zoneName(v.zone)}").withStyle(ChatFormatting.BOLD),
|
||||
gray("Only there; they grow back after mining")),
|
||||
v.items.map { Output(DropSources.stack(it.item), listOf(gray("grows back after ${it.regen_minutes} min"))) })
|
||||
}
|
||||
|
||||
fun findPages(): List<Page> = DropSources.data.pasture_finds.map { f ->
|
||||
val skill = f.skill.split('_').joinToString(" ") { it.replaceFirstChar(Char::uppercase) }
|
||||
Page(null, listOf(Component.literal(skill).withStyle(ChatFormatting.BOLD),
|
||||
Component.literal(if (f.zone == null) "In any zone" else "Only in ${DropSources.zoneName(f.zone)}"),
|
||||
gray("Cobblebase skill of a pastured Pokémon")),
|
||||
f.items.map { Output(DropSources.stack(it.item), listOf(gray(it.tiers.joinToString(", ")))) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user