JEI pages styled like Cobblemon Info's; every Pokemon obtainable on the roadmap

Adrian, 2026-11-09, after testing: he likes having both drop pages, but they
should look alike. Our JEI pages now use Cobblemon Info's look: its dark
panel and border, header with icon, title, accent tag and divider, its text
colours and spacing. Long text wraps instead of being cut off (a zone's
levels were lost). Shorter page names (Island Drops, Boss Rewards, Ore
Veins, Pasture Finds), since JEI cut the long title too. The line about the
Drop Synthesizer is gone; its core is the page's catalyst.

Roadmap Phase 9: every implemented Pokemon must be obtainable without
commands, even if rarely or through crafting. 95 of 905 implemented species
aren't reachable by a wild spawn or by evolving a wild line today. The
roster-size default changes to match.

Self-tests pass (drop_sources_match_the_server, jei_pages_build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-11-09 12:09:07 +01:00
1 parent 72965caedf
commit 0be2c2e5c6
4 files changed
+103 -65

No files matched your search

+2 -2
View File
@@ -8,14 +8,14 @@
- **Add-ons in the pack:** Cobblemon Quests Reloaded 2.0.2 (Cobblemon task type for FTB Quests) and Cobblemon Info for REI / JEI / EMI 2.32.14. Automated: `quests_have_cobblemon_tasks`, `jei_addon_shows_island_spawns`. - **Add-ons in the pack:** Cobblemon Quests Reloaded 2.0.2 (Cobblemon task type for FTB Quests) and Cobblemon Info for REI / JEI / EMI 2.32.14. Automated: `quests_have_cobblemon_tasks`, `jei_addon_shows_island_spawns`.
- **Spawn info in JEI:** right for the pack (species, levels, rarity come from the zone pools), but it shows biomes, not zones; our own pages will add the zone. - **Spawn info in JEI:** right for the pack (species, levels, rarity come from the zone pools), but it shows biomes, not zones; our own pages will add the zone.
- **JEI pages** (ours): look an item up in JEI (its recipes, key G) for "Pokémon drops on the island" (each Pokémon with its zone and levels), "Guardians and the End boss", "Sacrifice Altar", "Rebirth Chamber", "Zone ore veins" and "Pasture finds". Automated: `drop_sources_match_the_server`, `jei_pages_build`. - **JEI pages** (ours): look an item up in JEI (its recipes, key G) for "Island Drops" (each Pokémon with its zone and levels), "Boss Rewards", "Sacrifice Altar", "Rebirth Chamber", "Ore Veins" and "Pasture Finds". Styled like Cobblemon Info's pages (2026-11-09). Automated: `drop_sources_match_the_server`, `jei_pages_build`.
- **Quest book:** 18 chapters, 74 quests, generated from `docs/quests.yaml`: Getting Started, one chapter per zone and the End, Pokémon Machines, Base and Jobs. No rewards, nothing locked. Zone chapters: reach the zone, catch three of its Pokémon, mine its ores, beat its guardian. Automated: `quest_book_loads`, `quest_events_mark_zones` (and the guardian and furnace tests check their quest events). - **Quest book:** 18 chapters, 74 quests, generated from `docs/quests.yaml`: Getting Started, one chapter per zone and the End, Pokémon Machines, Base and Jobs. No rewards, nothing locked. Zone chapters: reach the zone, catch three of its Pokémon, mine its ores, beat its guardian. Automated: `quest_book_loads`, `quest_events_mark_zones` (and the guardian and furnace tests check their quest events).
## In game (Prism instance) ## In game (Prism instance)
- [ ] Open JEI, look up a Pokémon (e.g. Pikachu) in Cobblemon Info's pages: the spawn levels match its zone (`docs/zone-species.md`), and its drops are listed. - [ ] Open JEI, look up a Pokémon (e.g. Pikachu) in Cobblemon Info's pages: the spawn levels match its zone (`docs/zone-species.md`), and its drops are listed.
- [ ] In JEI, look up the recipes of Light Ball: Pikachu's page shows its picture, "Lives in Westmarch (Lv 16–25)" and its drops with chances; Raichu's page says where its line lives. Look up Ender Pearl: Beheeyem in the End has a page of its own. - [ ] In JEI, look up the recipes of Light Ball: Pikachu's page shows its picture, Westmarch with Lv 16–25 and its drops with chances; Raichu's page says where its line lives. Same dark panel as Cobblemon Info's "Item Drops", no text cut off. Look up Ender Pearl: Beheeyem in the End has a page of its own.
- [ ] Look up the recipes of Rare Candy (Sacrifice Altar tiers), Great Ball (Southwold's guardian), Raw Copper (Southwold's veins, and pasture mining in Southwold). - [ ] Look up the recipes of Rare Candy (Sacrifice Altar tiers), Great Ball (Southwold's guardian), Raw Copper (Southwold's veins, and pasture mining in Southwold).
- [ ] Open the quest book (FTB Quests' book item, or its key): every chapter shows, nodes are joined by lines, none shows a lock. - [ ] Open the quest book (FTB Quests' book item, or its key): every chapter shows, nodes are joined by lines, none shows a lock.
- [ ] Getting Started: tick "Welcome to the Island"; catch a Pokémon: "A Partner" completes. - [ ] Getting Started: tick "Welcome to the Island"; catch a Pokémon: "A Partner" completes.
+5 -3
View File
@@ -312,12 +312,14 @@ Adrian's choices: Cobblemon tasks come from **Cobblemon Quests Reloaded** (a mai
- **Self-tests:** `quest_book_loads` (FTB Quests on the server has the book in flexible mode, every task type is known, every item and advancement exists), `quest_events_mark_zones`, and the guardian and furnace tests check their events. - **Self-tests:** `quest_book_loads` (FTB Quests on the server has the book in flexible mode, every task type is known, every item and advancement exists), `quest_events_mark_zones`, and the guardian and furnace tests check their events.
- Still open: the flexible-mode soft-lock a fix mod reports for 1.20.1 (tasks done before their dependencies); checked in game for now. - Still open: the flexible-mode soft-lock a fix mod reports for 1.20.1 (tasks done before their dependencies); checked in game for now.
**Drop sources in JEI** (Adrian, 2026-11-02/03). The companion mod adds its own JEI pages (`jei/DropSourcesJei.kt`); look an item up (its recipes) to see every place on the island it comes from: **Drop sources in JEI** (Adrian, 2026-11-02/03). The companion mod adds its own JEI pages (`jei/DropSourcesJei.kt`); look an item up (its recipes) to see every place on the island it comes from:
- **Pokémon drops on the island:** one page per species of a line that lives on the island (794): its drops with chance and amount, the zones it spawns in with their levels, or for a form that isn't wild, where its line lives ("Not wild; its line lives in Westmarch (Pichu)"). The Drop Synthesizer core is the page's catalyst (it rolls the same drops). Spawns with drops of their own get a page too (Beheeyem in the End: ender pearls). This is the zone information the Cobblemon Info add-on can't show. - **Island Drops:** one page per species of a line that lives on the island (794): its drops with chance and amount, the zones it spawns in with their levels, or for a form that isn't wild, where its line lives ("Not wild; its line lives in Westmarch (Pichu)"). The Drop Synthesizer core is the page's catalyst (it rolls the same drops). Spawns with drops of their own get a page too (Beheeyem in the End: ender pearls). This is the zone information the Cobblemon Info add-on can't show.
- **Guardians and the End boss:** each guardian's species, level, zone, drops and return time; Eternatus with its experience and the dragon egg on the first win. - **Boss Rewards:** each guardian's species, level, zone, drops and return time; Eternatus with its experience and the dragon egg on the first win.
- **Sacrifice Altar** (each level tier and the Dragon-type special) and **Rebirth Chamber** (the Ability Patch), with their cores as catalysts. - **Sacrifice Altar** (each level tier and the Dragon-type special) and **Rebirth Chamber** (the Ability Patch), with their cores as catalysts.
- **Zone ore veins:** per zone, what its veins give and how fast they regrow. - **Ore Veins:** per zone, what its veins give and how fast they regrow.
- **Pasture finds:** per Cobblebase skill (mining, ore finder, evolution-item finder) and zone, what a pastured Pokémon can find there; the Pasture Block is the catalyst. - **Pasture finds:** per Cobblebase skill (mining, ore finder, evolution-item finder) and zone, what a pastured Pokémon can find there; the Pasture Block is the catalyst.
The pages sit next to Cobblemon Info's own, and Adrian likes having both, so they share its look (2026-11-09): the same dark panel with a thin border, a header with icon, white title, warm tag on the right and a divider, the same text colours and spacing. Long text wraps. JEI gives all pages of a category the height of the tallest.
JEI runs on the client, which on a dedicated server doesn't have the server's data, so the pages are built from `assets/cobblesurvival/jei/drop_sources.json` inside the mod, written by `tools/jei_sources.py` from the pack's own files (Cobblemon's species drops with the pack's species additions, the zone spawn pools, guardians, the End boss, the machines, `world/zones.json` ores, Cobblebase's zone tables). Which ore a vein gives now lives in `world/zones.json` → `ores` → `item` (the quest book reads it too). Self-tests: `drop_sources_match_the_server` compares the file with the live server (every listed Pokémon's drops and zones, spawns with own drops, guardians, the End boss, altar tiers) and fails when it's stale; `jei_pages_build` builds every page with the same code JEI calls and checks every item exists. JEI runs on the client, which on a dedicated server doesn't have the server's data, so the pages are built from `assets/cobblesurvival/jei/drop_sources.json` inside the mod, written by `tools/jei_sources.py` from the pack's own files (Cobblemon's species drops with the pack's species additions, the zone spawn pools, guardians, the End boss, the machines, `world/zones.json` ores, Cobblebase's zone tables). Which ore a vein gives now lives in `world/zones.json` → `ores` → `item` (the quest book reads it too). Self-tests: `drop_sources_match_the_server` compares the file with the live server (every listed Pokémon's drops and zones, spawns with own drops, guardians, the End boss, altar tiers) and fails when it's stale; `jei_pages_build` builds every page with the same code JEI calls and checks every item exists.
## Open decisions for Adrian (proceeding with the default, flagged) ## Open decisions for Adrian (proceeding with the default, flagged)
+3 -2
View File
@@ -190,11 +190,12 @@ Goal: a quest book that points players onward, laid out like a research tree who
## Phase 9 – Balancing and release ## Phase 9 – Balancing and release
- **Balance pass** on levels, drop rates, vein timers, job cooldowns, hazard damage and machine output, all from `docs/balance.md`. - **Balance pass** on levels, drop rates, vein timers, job cooldowns, hazard damage and machine output, all from `docs/balance.md`.
- **Every implemented Pokémon is obtainable** (Adrian, 2026-11-09). Every species Cobblemon has a model for must be reachable somewhere in the world, even if rarely or through crafting: a wild spawn, evolving a wild line, a fossil, or a crafted or summoned encounter. Legendaries and the like count too; they can be very rare or expensive. Checked on 2026-11-09: 95 of 905 implemented species are not reachable by a wild spawn or by evolving a wild line (legendaries, fossils, and lines the zone assignment left out, such as Aron, Litwick and Piplup). Report what is missing with a tool or self-test so new Cobblemon versions can't reopen the gap.
- **Performance.** Profile with spark on a server with realistic pen and machine counts; cap entity density through pasture config if needed. - **Performance.** Profile with spark on a server with realistic pen and machine counts; cap entity density through pasture config if needed.
- **Multiplayer test** with Adrian's group. - **Multiplayer test** with Adrian's group.
- **Release.** Final `docs/licenses.md`, credits and non-affiliation disclaimer; publish client and server packs on Modrinth and CurseForge as dependency-based packs. - **Release.** Final `docs/licenses.md`, credits and non-affiliation disclaimer; publish client and server packs on Modrinth and CurseForge as dependency-based packs.
**Done when:** a full playthrough from spawn to the last zone is possible without commands, and a server runs a multi-hour session without tick-time spikes from Pokémon entities. **Done when:** a full playthrough from spawn to the last zone is possible without commands, every implemented Pokémon can be obtained without commands, and a server runs a multi-hour session without tick-time spikes from Pokémon entities.
## Open decisions for Adrian ## Open decisions for Adrian
@@ -208,7 +209,7 @@ The agent should proceed with the default in each row and flag it, rather than b
| Do boss area attacks hit party Pokémon? | No, player only | | Do boss area attacks hit party Pokémon? | No, player only |
| Player dies during a battle | The battle ends as a loss; Pokémon that had not fainted survive | | Player dies during a battle | The battle ends as a loss; Pokémon that had not fainted survive |
| Separate storage per base | No; all PC blocks share one storage | | Separate storage per base | No; all PC blocks share one storage |
| Roster size for v1 | Curated 150–250 species | | Roster size for v1 | Every species Cobblemon has a model for (Adrian, 2026-11-09; see Phase 9), replacing the curated 150–250 |
| Pen footprint rule | Lower pasture limits only; no size-based rule in v1 | | Pen footprint rule | Lower pasture limits only; no size-based rule in v1 |
| Multiplayer target | Small private server group | | Multiplayer target | Small private server group |
@@ -7,7 +7,6 @@ import dev.cobblesurvival.CobbleSurvival
import mezz.jei.api.IModPlugin import mezz.jei.api.IModPlugin
import mezz.jei.api.JeiPlugin import mezz.jei.api.JeiPlugin
import mezz.jei.api.gui.builder.IRecipeLayoutBuilder 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.drawable.IDrawable
import mezz.jei.api.gui.ingredient.IRecipeSlotsView import mezz.jei.api.gui.ingredient.IRecipeSlotsView
import mezz.jei.api.recipe.IFocusGroup import mezz.jei.api.recipe.IFocusGroup
@@ -19,6 +18,7 @@ import mezz.jei.api.registration.IRecipeCategoryRegistration
import mezz.jei.api.registration.IRecipeRegistration import mezz.jei.api.registration.IRecipeRegistration
import net.minecraft.ChatFormatting import net.minecraft.ChatFormatting
import net.minecraft.client.Minecraft import net.minecraft.client.Minecraft
import net.minecraft.client.gui.Font
import net.minecraft.client.gui.GuiGraphics import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.chat.Component import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation import net.minecraft.resources.ResourceLocation
@@ -30,11 +30,16 @@ import net.minecraft.world.item.ItemStack
* the zones they live in (the Cobblemon Info add-on shows drops and biomes, not zones), guardians and the End boss, * 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 * 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. * [DropSources]; JEI finds this class by its annotation, on the client only.
*
* Styled like Cobblemon Info's pages, which sit next to these (Adrian, 2026-11-09): the same dark panel, header with
* icon, title, accent tag and divider, and the same text colours.
*/ */
@JeiPlugin @JeiPlugin
class DropSourcesJei : IModPlugin { class DropSourcesJei : IModPlugin {
/** One page: a picture (a Pokémon or a block), up to three lines of text, and the items it gives. */ /** One page: an icon and title with a short [tag], rows of text, and the items it gives. */
class Page(val icon: ItemStack?, val lines: List<Component>, val outputs: List<Output>) class Page(val icon: ItemStack, val title: Component, val tag: String, val rows: List<Row>, val outputs: List<Output>)
/** A line of text, wrapped if long, with optional [right]-aligned text on its first line. */
class Row(val left: Component, val right: Component? = null)
class Output(val stack: ItemStack, val notes: List<Component>) 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>) { private class Kind(val path: String, val title: String, val icon: String, val catalysts: List<String>, val pages: () -> List<Page>) {
@@ -43,12 +48,12 @@ class DropSourcesJei : IModPlugin {
} }
private val kinds = listOf( private val kinds = listOf(
Kind("pokemon_drops", "Pokémon drops on the island", "cobblemon:poke_ball", listOf("cobblesurvival:synthesis_core"), ::pokemonPages), Kind("pokemon_drops", "Island Drops", "cobblemon:poke_ball", listOf("cobblesurvival:synthesis_core"), ::pokemonPages),
Kind("boss_rewards", "Guardians and the End boss", "minecraft:netherite_sword", emptyList(), ::bossPages), Kind("boss_rewards", "Boss Rewards", "minecraft:netherite_sword", emptyList(), ::bossPages),
Kind("altar", "Sacrifice Altar", "cobblesurvival:altar_core", listOf("cobblesurvival:altar_core"), ::altarPages), 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("rebirth", "Rebirth Chamber", "cobblesurvival:rebirth_core", listOf("cobblesurvival:rebirth_core"), ::rebirthPages),
Kind("ore_veins", "Zone ore veins", "minecraft:iron_pickaxe", emptyList(), ::veinPages), Kind("ore_veins", "Ore Veins", "minecraft:iron_pickaxe", emptyList(), ::veinPages),
Kind("pasture_finds", "Pasture finds", "cobblemon:pasture", listOf("cobblemon:pasture"), ::findPages), Kind("pasture_finds", "Pasture Finds", "cobblemon:pasture", listOf("cobblemon:pasture"), ::findPages),
) )
override fun getPluginUid(): ResourceLocation = ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "drop_sources") override fun getPluginUid(): ResourceLocation = ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "drop_sources")
@@ -56,8 +61,7 @@ class DropSourcesJei : IModPlugin {
override fun registerCategories(registration: IRecipeCategoryRegistration) { override fun registerCategories(registration: IRecipeCategoryRegistration) {
val gui = registration.jeiHelpers.guiHelper val gui = registration.jeiHelpers.guiHelper
registration.addRecipeCategories(*kinds.map { kind -> 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)), kind.built)
PageCategory(kind.type, Component.literal(kind.title), gui.createDrawableItemStack(DropSources.stack(kind.icon)), rows)
}.toTypedArray()) }.toTypedArray())
} }
@@ -69,48 +73,89 @@ class DropSourcesJei : IModPlugin {
kinds.forEach { kind -> kind.catalysts.forEach { registration.addRecipeCatalyst(DropSources.stack(it), kind.type) } } 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> { class PageCategory(private val type: RecipeType<Page>, private val title: Component, private val icon: IDrawable, pages: List<Page>) : IRecipeCategory<Page> {
/** JEI gives every page of a category the same size: that of its tallest. */
private val height = pages.maxOf { slotsTop(it) + (it.outputs.size + PER_ROW - 1) / PER_ROW * SLOT } + PADDING - 2
override fun getRecipeType() = type override fun getRecipeType() = type
override fun getTitle() = title override fun getTitle() = title
override fun getIcon() = icon override fun getIcon() = icon
override fun getWidth() = WIDTH override fun getWidth() = WIDTH
override fun getHeight() = TEXT_HEIGHT + rows * SLOT override fun getHeight() = height
private val font: Font get() = Minecraft.getInstance().font
private fun rowWidth(row: Row) = INNER - (row.right?.let { font.width(it) + RIGHT_GAP } ?: 0)
private fun rowLines(row: Row) = font.split(row.left, rowWidth(row))
private fun slotsTop(page: Page): Int {
val lines = page.rows.sumOf { rowLines(it).size.coerceAtLeast(1) }
return BODY_TOP + lines * LINE + if (lines > 0) ROWS_GAP else 0
}
override fun setRecipe(builder: IRecipeLayoutBuilder, page: Page, focuses: IFocusGroup) { override fun setRecipe(builder: IRecipeLayoutBuilder, page: Page, focuses: IFocusGroup) {
page.icon?.let { builder.addSlot(RecipeIngredientRole.RENDER_ONLY, 1, 1).addItemStack(it) } builder.addSlot(RecipeIngredientRole.RENDER_ONLY, PADDING, ICON_TOP).addItemStack(page.icon)
val top = slotsTop(page)
page.outputs.forEachIndexed { i, out -> page.outputs.forEachIndexed { i, out ->
builder.addSlot(RecipeIngredientRole.OUTPUT, 1 + (i % PER_ROW) * SLOT, TEXT_HEIGHT + 1 + (i / PER_ROW) * SLOT) builder.addSlot(RecipeIngredientRole.OUTPUT, PADDING + 1 + (i % PER_ROW) * SLOT, top + 1 + (i / PER_ROW) * SLOT)
.setStandardSlotBackground()
.addItemStack(out.stack) .addItemStack(out.stack)
.addRichTooltipCallback { _, tooltip -> out.notes.forEach(tooltip::add) } .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) { override fun draw(page: Page, view: IRecipeSlotsView, graphics: GuiGraphics, mouseX: Double, mouseY: Double) {
val font = Minecraft.getInstance().font // An opaque panel of our own, so the text reads the same whatever JEI's theme is.
val left = textLeft(page) graphics.fill(0, 0, WIDTH, height, SURFACE)
page.lines.forEachIndexed { i, line -> graphics.fill(0, 0, WIDTH, 1, BORDER)
font.split(line, WIDTH - left).firstOrNull()?.let { graphics.drawString(font, it, left, 1 + i * LINE, TEXT_COLOR, false) } graphics.fill(0, height - 1, WIDTH, height, BORDER)
} graphics.fill(0, 0, 1, height, BORDER)
} graphics.fill(WIDTH - 1, 0, WIDTH, height, BORDER)
/** Lines too long for the page show in full when hovered. */ val right = WIDTH - PADDING
override fun getTooltip(tooltip: ITooltipBuilder, page: Page, view: IRecipeSlotsView, mouseX: Double, mouseY: Double) { val tagWidth = font.width(page.tag)
if (mouseX >= textLeft(page) && mouseY < TEXT_HEIGHT - 2) page.lines.forEach(tooltip::add) graphics.drawString(font, page.tag, right - tagWidth, TITLE_TOP, ACCENT, true)
font.split(page.title, right - TITLE_LEFT - tagWidth - RIGHT_GAP).firstOrNull()?.let { graphics.drawString(font, it, TITLE_LEFT, TITLE_TOP, TITLE, true) }
graphics.fill(PADDING, DIVIDER_TOP, right, DIVIDER_TOP + 1, DIVIDER)
var y = BODY_TOP
for (row in page.rows) {
row.right?.let { graphics.drawString(font, it, right - font.width(it), y, SUBTLE, true) }
for (line in rowLines(row)) {
graphics.drawString(font, line, PADDING, y, BODY, true)
y += LINE
}
}
} }
} }
companion object { companion object {
private const val WIDTH = 162 // Cobblemon Info's panel metrics and colours (its PanelLayout and DexColors).
private const val SLOT = 18 private const val SLOT = 18
private const val PER_ROW = 9 private const val PER_ROW = 9
private const val LINE = 10 private const val PADDING = 6
private const val TEXT_HEIGHT = 32 private const val WIDTH = PADDING * 2 + PER_ROW * SLOT
private const val TEXT_COLOR = 0x404040 private const val INNER = WIDTH - PADDING * 2
private const val LINE = 11
private const val ICON_TOP = 3
private const val TITLE_LEFT = PADDING + 22
private const val TITLE_TOP = 6
private const val DIVIDER_TOP = 20
private const val BODY_TOP = 24
private const val ROWS_GAP = 2
private const val RIGHT_GAP = 6
private const val SURFACE = 0xFF17171B.toInt()
private const val BORDER = 0xFF4A4A52.toInt()
private const val TITLE = 0xFFFFFFFF.toInt()
private const val BODY = 0xFFDDDDDD.toInt()
private const val SUBTLE = 0xFFBBBBBB.toInt()
private const val MUTED = 0xFF888888.toInt()
private const val ACCENT = 0xFFDDCC99.toInt()
private const val DIVIDER = 0x50FFFFFF
private fun gray(text: String): Component = Component.literal(text).withStyle(ChatFormatting.GRAY) private fun gray(text: String): Component = Component.literal(text).withStyle(ChatFormatting.GRAY)
private fun muted(text: String): Component = Component.literal(text).withColor(MUTED)
private fun text(value: String): Component = Component.literal(value)
private fun speciesName(id: String): Component = private fun speciesName(id: String): Component =
PokemonSpecies.getByIdentifier(ResourceLocation.parse(id))?.translatedName ?: Component.literal(id.substringAfter(':')) PokemonSpecies.getByIdentifier(ResourceLocation.parse(id))?.translatedName ?: Component.literal(id.substringAfter(':'))
@@ -130,24 +175,20 @@ class DropSourcesJei : IModPlugin {
private fun rewardOutputs(rewards: List<DropSources.Reward>, note: Component? = null) = private fun rewardOutputs(rewards: List<DropSources.Reward>, note: Component? = null) =
rewards.map { Output(DropSources.stack(it.item, it.count), listOfNotNull(note)) } rewards.map { Output(DropSources.stack(it.item, it.count), listOfNotNull(note)) }
private fun levels(zone: String, levels: String) = "${DropSources.zoneName(zone)} (Lv ${levels.replace("-", "–")})" private fun zoneRow(zone: String, levels: String) = Row(text(DropSources.zoneName(zone)), text("Lv ${levels.replace("-", "–")}"))
fun pokemonPages(): List<Page> { fun pokemonPages(): List<Page> {
val data = DropSources.data val data = DropSources.data
val wild = data.pokemon.map { p -> val wild = data.pokemon.map { p ->
val where = if (p.zones.isNotEmpty()) { val rows = if (p.zones.isNotEmpty()) {
"Lives in " + p.zones.joinToString(", ") { levels(it.zone, it.levels) } p.zones.map { zoneRow(it.zone, it.levels) }
} else { } else {
"Not wild; its line lives in " + p.line.orEmpty().joinToString(", ") { listOf(Row(muted("Not wild. Its line lives in:"))) + p.line.orEmpty().map { Row(text(DropSources.zoneName(it.zone)), speciesName(it.species)) }
"${DropSources.zoneName(it.zone)} (${speciesName(it.species).string})"
}
} }
Page(picture(p.species), listOf(speciesName(p.species).copy().withStyle(ChatFormatting.BOLD), Component.literal(where), Page(picture(p.species), speciesName(p.species), "Drops", rows, p.drops.map(::dropOutput))
gray("The Drop Synthesizer makes the same drops")), p.drops.map(::dropOutput))
} }
val special = data.spawn_drops.map { s -> val special = data.spawn_drops.map { s ->
Page(picture(s.species), listOf(speciesName(s.species).copy().withStyle(ChatFormatting.BOLD), Page(picture(s.species), speciesName(s.species), "Drops here", listOf(Row(text(DropSources.zoneName(s.zone)), text("instead"))), s.drops.map(::dropOutput))
Component.literal("In ${DropSources.zoneName(s.zone)}: drops these instead")), s.drops.map(::dropOutput))
} }
return wild + special return wild + special
} }
@@ -155,48 +196,42 @@ class DropSourcesJei : IModPlugin {
fun bossPages(): List<Page> { fun bossPages(): List<Page> {
val data = DropSources.data val data = DropSources.data
val guardians = data.guardians.map { g -> val guardians = data.guardians.map { g ->
Page(picture(g.species), listOf( val hours = g.respawn_hours.let { if (it % 1.0 == 0.0) it.toInt() else it }
speciesName(g.species).copy().append(", Lv ${g.level}").withStyle(ChatFormatting.BOLD), Page(picture(g.species), speciesName(g.species), "Guardian", listOf(
Component.literal("Guardian of ${DropSources.zoneName(g.zone)}"), Row(text(DropSources.zoneName(g.zone)), text("Lv ${g.level}")), Row(muted("Back $hours hours after a win")),
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"))) ), rewardOutputs(g.drops, gray("for everyone in the winning battle")))
} }
val end = data.end_boss val end = data.end_boss
val boss = Page(picture(end.species), listOf( val boss = Page(picture(end.species), speciesName(end.species), "End boss", listOf(
speciesName(end.species).copy().append(", Lv ${end.level}").withStyle(ChatFormatting.BOLD), Row(text("The End"), text("Lv ${end.level}")), Row(muted("${end.xp_first_win} XP the first time, ${end.xp} after")),
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"))) ), rewardOutputs(end.first_win, gray("first win only")))
return guardians + boss return guardians + boss
} }
fun altarPages(): List<Page> { fun altarPages(): List<Page> {
val altar = DropSources.data.altar val altar = DropSources.data.altar
val core = DropSources.stack("cobblesurvival:altar_core")
return altar.tiers.map { t -> return altar.tiers.map { t ->
Page(null, listOf(Component.literal("Sacrificing a Pokémon of Lv ${t.min_level}+").withStyle(ChatFormatting.BOLD), Page(core, text("Lv ${t.min_level} or more"), "Sacrifice", listOf(Row(muted("The highest tier reached decides"))), rewardOutputs(t.rewards))
gray("The highest tier it reaches decides")), rewardOutputs(t.rewards))
} + altar.specials.map { s -> } + 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), Page(core, text("Lv ${s.min_level} ${s.types.joinToString("/") { it.replaceFirstChar(Char::uppercase) }} type"), "Also gives", emptyList(), rewardOutputs(s.rewards))
gray("also gives")), rewardOutputs(s.rewards))
} }
} }
fun rebirthPages(): List<Page> = listOf( fun rebirthPages(): List<Page> = listOf(
Page(null, listOf(Component.literal("A Lv 100 Pokémon reborn").withStyle(ChatFormatting.BOLD), Page(DropSources.stack("cobblesurvival:rebirth_core"), text("Lv 100 Pokémon"), "Rebirth",
gray("as a shiny Lv 1 of its first stage,"), gray("comes back with")), rewardOutputs(DropSources.data.rebirth)), listOf(Row(muted("Reborn as a shiny Lv 1 of its first stage"))), rewardOutputs(DropSources.data.rebirth)),
) )
fun veinPages(): List<Page> = DropSources.data.ore_veins.map { v -> 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), Page(DropSources.stack("minecraft:iron_pickaxe"), text(DropSources.zoneName(v.zone)), "Ore veins", listOf(Row(muted("Only here; they grow back after mining"))),
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"))) }) 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 -> fun findPages(): List<Page> = DropSources.data.pasture_finds.map { f ->
val skill = f.skill.split('_').joinToString(" ") { it.replaceFirstChar(Char::uppercase) } val skill = f.skill.split('_').joinToString(" ") { it.replaceFirstChar(Char::uppercase) }
Page(null, listOf(Component.literal(skill).withStyle(ChatFormatting.BOLD), Page(DropSources.stack("cobblemon:pasture"), text(if (f.zone == null) "Any zone" else DropSources.zoneName(f.zone)), skill,
Component.literal(if (f.zone == null) "In any zone" else "Only in ${DropSources.zoneName(f.zone)}"), listOf(Row(muted("Found by a pastured Pokémon with this skill"))),
gray("Cobblebase skill of a pastured Pokémon")),
f.items.map { Output(DropSources.stack(it.item), listOf(gray(it.tiers.joinToString(", ")))) }) f.items.map { Output(DropSources.stack(it.item), listOf(gray(it.tiers.joinToString(", ")))) })
} }
} }