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
@@ -118,6 +118,13 @@ python3 tools/quests.py
|
|||||||
```
|
```
|
||||||
Generates the quest book (Phase 8) from `docs/quests.yaml` into `pack/config/ftbquests/quests/`, and the hidden advancements quests check. `tools/build.py` runs it. `--pull <FTB Quests folder>` first copies node positions moved in the in-game editor back into `docs/quests.yaml` (`tools/prism_instance.py` does it from the instance before each sync); `--check` fails if the generated files are out of date.
|
Generates the quest book (Phase 8) from `docs/quests.yaml` into `pack/config/ftbquests/quests/`, and the hidden advancements quests check. `tools/build.py` runs it. `--pull <FTB Quests folder>` first copies node positions moved in the in-game editor back into `docs/quests.yaml` (`tools/prism_instance.py` does it from the instance before each sync); `--check` fails if the generated files are out of date.
|
||||||
|
|
||||||
|
## JEI drop sources
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/jei_sources.py
|
||||||
|
```
|
||||||
|
Writes the data behind the pack's own JEI pages (Phase 8) into the companion mod (`mod/src/main/resources/assets/cobblesurvival/jei/drop_sources.json`): every Pokémon's drops and zones, guardians, the End boss, the altar, ore veins, pasture finds. Run it after changing species drops, spawns, guardians, machines, ores or Cobblebase tables, then rebuild; the self-test `drop_sources_match_the_server` fails while it's stale. Needs the Cobblemon jar in `build/server/mods` (any test-server run).
|
||||||
|
|
||||||
## Adding a mod
|
## Adding a mod
|
||||||
|
|
||||||
1. Read its license and its Cobblemon 1.8 status; add both to `docs/licenses.md` and `docs/addon-status.md`.
|
1. Read its license and its Cobblemon 1.8 status; add both to `docs/licenses.md` and `docs/addon-status.md`.
|
||||||
|
|||||||
@@ -8,12 +8,15 @@
|
|||||||
|
|
||||||
- **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`.
|
||||||
- **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.
|
||||||
|
- [ ] 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.
|
||||||
- [ ] Soft-lock check: get 16 Poké Balls before ticking "Welcome to the Island", then tick it: "Poké Balls" completes too (if it stays stuck, tell me: there's a known flexible-mode bug in older FTB Quests).
|
- [ ] Soft-lock check: get 16 Poké Balls before ticking "Welcome to the Island", then tick it: "Poké Balls" completes too (if it stays stuck, tell me: there's a known flexible-mode bug in older FTB Quests).
|
||||||
|
|||||||
+8
-1
@@ -311,7 +311,14 @@ Adrian's choices: Cobblemon tasks come from **Cobblemon Quests Reloaded** (a mai
|
|||||||
- **Node positions:** Adrian moves nodes in the in-game editor; `tools/quests.py --pull <folder>` records every node that isn't where the generator would put it in the `layout` block at the end of `docs/quests.yaml`. `tools/prism_instance.py` does that from the instance before each sync, and rebuilds if something moved, so a sync never undoes his layout (docs/quests.yaml then needs committing). Titles, texts, tasks and dependencies edited in game are overwritten.
|
- **Node positions:** Adrian moves nodes in the in-game editor; `tools/quests.py --pull <folder>` records every node that isn't where the generator would put it in the `layout` block at the end of `docs/quests.yaml`. `tools/prism_instance.py` does that from the instance before each sync, and rebuilds if something moved, so a sync never undoes his layout (docs/quests.yaml then needs committing). Titles, texts, tasks and dependencies edited in game are overwritten.
|
||||||
- **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.
|
||||||
- JEI (next): our plugin adds, per item, every place on the island it comes from that Cobblemon Info doesn't cover: guardian and End boss rewards, Sacrifice Altar tiers, ore veins by zone, pasture mining and finder tables by zone; and per Pokémon its zone.
|
**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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -21,30 +21,6 @@ book:
|
|||||||
title: Cobblemon Survival
|
title: Cobblemon Survival
|
||||||
icon: cobblemon:poke_ball
|
icon: cobblemon:poke_ball
|
||||||
|
|
||||||
# What each zone's ore veins give when mined (pack/config/regenerating_ore_veins/veins.json).
|
|
||||||
ore_items:
|
|
||||||
coal_node: minecraft:coal
|
|
||||||
copper_node: minecraft:raw_copper
|
|
||||||
iron_node: minecraft:raw_iron
|
|
||||||
redstone_node: minecraft:redstone
|
|
||||||
lapis_node: minecraft:lapis_lazuli
|
|
||||||
gold_node: minecraft:raw_gold
|
|
||||||
emerald_node: minecraft:emerald
|
|
||||||
amethyst_node: minecraft:amethyst_shard
|
|
||||||
diamond_node: minecraft:diamond
|
|
||||||
quartz_node: minecraft:quartz
|
|
||||||
netherite_node: minecraft:ancient_debris
|
|
||||||
thunder_stone_node: cobblemon:thunder_stone
|
|
||||||
shiny_stone_node: cobblemon:shiny_stone
|
|
||||||
leaf_stone_node: cobblemon:leaf_stone
|
|
||||||
water_stone_node: cobblemon:water_stone
|
|
||||||
moon_stone_node: cobblemon:moon_stone
|
|
||||||
sun_stone_node: cobblemon:sun_stone
|
|
||||||
fire_stone_node: cobblemon:fire_stone
|
|
||||||
ice_stone_node: cobblemon:ice_stone
|
|
||||||
dawn_stone_node: cobblemon:dawn_stone
|
|
||||||
dusk_stone_node: cobblemon:dusk_stone
|
|
||||||
|
|
||||||
zone_chapter:
|
zone_chapter:
|
||||||
- id: arrive
|
- id: arrive
|
||||||
title: "{zone}"
|
title: "{zone}"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ val minecraftVersion = property("minecraft_version") as String
|
|||||||
val neoforgeVersion = property("neoforge_version") as String
|
val neoforgeVersion = property("neoforge_version") as String
|
||||||
val cobblemonVersion = property("cobblemon_version") as String
|
val cobblemonVersion = property("cobblemon_version") as String
|
||||||
val kotlinforforgeVersion = property("kotlinforforge_version") as String
|
val kotlinforforgeVersion = property("kotlinforforge_version") as String
|
||||||
|
val jeiVersion = property("jei_version") as String
|
||||||
|
|
||||||
group = "dev.cobblesurvival"
|
group = "dev.cobblesurvival"
|
||||||
version = modVersion
|
version = modVersion
|
||||||
@@ -32,6 +33,8 @@ repositories {
|
|||||||
maven("https://thedarkcolour.github.io/KotlinForForge/")
|
maven("https://thedarkcolour.github.io/KotlinForForge/")
|
||||||
// Cobblemon's coordinates come from its official MDK, not guessed.
|
// Cobblemon's coordinates come from its official MDK, not guessed.
|
||||||
maven("https://artefacts.cobblemon.com/releases/")
|
maven("https://artefacts.cobblemon.com/releases/")
|
||||||
|
// JEI's own Maven, as its README gives it (Phase 8: the pack's drop sources as JEI pages).
|
||||||
|
maven("https://maven.blamejared.com/")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -41,6 +44,7 @@ dependencies {
|
|||||||
|
|
||||||
modCompileOnly("com.cobblemon:mod:$cobblemonVersion") { isTransitive = false }
|
modCompileOnly("com.cobblemon:mod:$cobblemonVersion") { isTransitive = false }
|
||||||
modImplementation("com.cobblemon:neoforge:$cobblemonVersion")
|
modImplementation("com.cobblemon:neoforge:$cobblemonVersion")
|
||||||
|
modCompileOnly("mezz.jei:jei-$minecraftVersion-neoforge-api:$jeiVersion")
|
||||||
implementation("thedarkcolour:kotlinforforge-neoforge:$kotlinforforgeVersion") {
|
implementation("thedarkcolour:kotlinforforge-neoforge:$kotlinforforgeVersion") {
|
||||||
exclude("net.neoforged.fancymodloader", "loader")
|
exclude("net.neoforged.fancymodloader", "loader")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ minecraft_version=1.21.1
|
|||||||
neoforge_version=21.1.265
|
neoforge_version=21.1.265
|
||||||
cobblemon_version=1.8.4+1.21.1
|
cobblemon_version=1.8.4+1.21.1
|
||||||
kotlinforforge_version=5.12.0
|
kotlinforforge_version=5.12.0
|
||||||
|
jei_version=19.64.0.455
|
||||||
@@ -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
|
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.CobblemonSpawnPools
|
||||||
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
|
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.progress.QuestEvents
|
||||||
|
import dev.cobblesurvival.zone.Guardians
|
||||||
import dev.cobblesurvival.zone.Zones
|
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.gametest.framework.GameTestAssertException
|
||||||
import net.minecraft.resources.ResourceLocation
|
import net.minecraft.resources.ResourceLocation
|
||||||
import net.minecraft.world.item.ItemStack
|
import net.minecraft.world.item.ItemStack
|
||||||
|
import net.minecraft.world.item.Items
|
||||||
import net.minecraft.world.level.levelgen.Heightmap
|
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
|
* 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("jei_addon_shows_island_spawns", 20, ::addonSpawns),
|
||||||
SelfTestCase("quest_book_loads", 20, ::bookLoads),
|
SelfTestCase("quest_book_loads", 20, ::bookLoads),
|
||||||
SelfTestCase("quest_events_mark_zones", 200, ::zoneEvent),
|
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). */
|
/** At least this many quests in the book (docs/quests.yaml: 74 on 2026-11-03). */
|
||||||
private const val MIN_QUESTS = 70
|
private const val MIN_QUESTS = 70
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Write the data behind the pack's own JEI pages (roadmap Phase 8; Adrian, 2026-11-02: every item that can drop gets
|
||||||
|
a JEI page naming its sources; 2026-11-03: alongside the Cobblemon Info add-on, which shows spawns and drops but not
|
||||||
|
the island's zones).
|
||||||
|
|
||||||
|
JEI runs on the client, which on a dedicated server doesn't have the pack's server data, so the pages are built from
|
||||||
|
this file inside the companion mod: mod/src/main/resources/assets/cobblesurvival/jei/drop_sources.json. It holds:
|
||||||
|
|
||||||
|
pokemon every species of a line that lives on the island: its drops (as the Drop Synthesizer rolls them)
|
||||||
|
and the zones it spawns in, or the zones where its line lives
|
||||||
|
spawn_drops zone spawns that drop something else than their species (e.g. Beheeyem in the End)
|
||||||
|
guardians each zone guardian: species, level, drops, return time
|
||||||
|
end_boss the End boss and what beating it gives
|
||||||
|
altar the Sacrifice Altar's level tiers and type specials; rebirth: the Rebirth Chamber's extra
|
||||||
|
ore_veins per zone, what its regenerating veins give and how fast they regrow
|
||||||
|
pasture_finds per Cobblebase skill and zone, what a pasture's Pokémon can find (the pack's zone-bound tables)
|
||||||
|
|
||||||
|
The self-test drop_sources_match_the_server compares it with the live server, so run this after changing species
|
||||||
|
drops, spawns, guardians, the End boss, the altar, ore veins or Cobblebase's tables, then rebuild the mod.
|
||||||
|
|
||||||
|
Needs the Cobblemon jar in build/server/mods (any test-server run).
|
||||||
|
|
||||||
|
Usage: tools/jei_sources.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import zipfile
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
DATA = ROOT / "datapacks/core/data"
|
||||||
|
OUT = ROOT / "mod/src/main/resources/assets/cobblesurvival/jei/drop_sources.json"
|
||||||
|
SKILLS = {"mining": "mining", "finder_ore": "ore_finder", "finder_evo": "evolution_item_finder"}
|
||||||
|
|
||||||
|
|
||||||
|
def drop_entries(drops):
|
||||||
|
"""Cobblemon drop table → [{item, chance (percent, absent = always), count ("a-b")}]."""
|
||||||
|
out = []
|
||||||
|
for e in (drops or {}).get("entries", []):
|
||||||
|
if "item" not in e:
|
||||||
|
continue
|
||||||
|
entry = {"item": e["item"], "count": str(e.get("quantityRange", e.get("quantity", 1)))}
|
||||||
|
if "percentage" in e and float(e["percentage"]) < 100:
|
||||||
|
entry["chance"] = float(e["percentage"])
|
||||||
|
out.append(entry)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def species_data(jar):
|
||||||
|
species = {}
|
||||||
|
with zipfile.ZipFile(jar) as z:
|
||||||
|
for name in z.namelist():
|
||||||
|
m = re.fullmatch(r"data/cobblemon/species/[^/]+/([^/]+)\.json", name)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
d = json.loads(z.read(name))
|
||||||
|
pre = d.get("preEvolution")
|
||||||
|
species[f"cobblemon:{m[1]}"] = {
|
||||||
|
"name": d["name"], "implemented": d.get("implemented", False), "drops": d.get("drops"),
|
||||||
|
"pre": f"cobblemon:{pre.split()[0].lower()}" if pre else None,
|
||||||
|
}
|
||||||
|
# The pack's species additions replace a species' drops (datapacks/core/data/cobblesurvival/species_additions).
|
||||||
|
for path in sorted((DATA / "cobblesurvival/species_additions").glob("*.json")):
|
||||||
|
d = json.loads(path.read_text())
|
||||||
|
if "drops" in d and d["target"] in species:
|
||||||
|
species[d["target"]]["drops"] = d["drops"]
|
||||||
|
return species
|
||||||
|
|
||||||
|
|
||||||
|
def root_of(sid, species):
|
||||||
|
seen = set()
|
||||||
|
while species.get(sid, {}).get("pre") and sid not in seen:
|
||||||
|
seen.add(sid)
|
||||||
|
sid = species[sid]["pre"]
|
||||||
|
return sid
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
jar = next((ROOT / "build/server/mods").glob("Cobblemon-*.jar"))
|
||||||
|
species = species_data(jar)
|
||||||
|
plan = json.loads((ROOT / "world/zones.json").read_text())
|
||||||
|
zones = {z["id"]: z for z in plan["zones"] + plan["caves"] + plan.get("dimensions", [])}
|
||||||
|
|
||||||
|
# Where each species spawns, and spawns with drops of their own.
|
||||||
|
where = defaultdict(dict)
|
||||||
|
spawn_drops = []
|
||||||
|
for zid in zones:
|
||||||
|
pool = DATA / f"cobblesurvival/spawn_pool_world/{zid}.json"
|
||||||
|
if not pool.exists():
|
||||||
|
continue
|
||||||
|
for spawn in json.loads(pool.read_text())["spawns"]:
|
||||||
|
sid = f"cobblemon:{spawn['pokemon'].split()[0].lower()}"
|
||||||
|
a, _, b = str(spawn["level"]).partition("-")
|
||||||
|
low, high = where[sid].get(zid, (999, 0))
|
||||||
|
where[sid][zid] = (min(low, int(a)), max(high, int(b or a)))
|
||||||
|
if "drops" in spawn:
|
||||||
|
entry = {"species": sid, "zone": zid, "drops": drop_entries(spawn["drops"])}
|
||||||
|
if entry not in spawn_drops:
|
||||||
|
spawn_drops.append(entry)
|
||||||
|
|
||||||
|
line_zones = defaultdict(dict)
|
||||||
|
for sid, zs in where.items():
|
||||||
|
for zid, levels in zs.items():
|
||||||
|
line_zones[root_of(sid, species)].setdefault(zid, sid)
|
||||||
|
|
||||||
|
pokemon = []
|
||||||
|
for sid, s in sorted(species.items()):
|
||||||
|
drops = drop_entries(s["drops"])
|
||||||
|
root = root_of(sid, species)
|
||||||
|
if not drops or not s["implemented"] or not (where.get(sid) or line_zones.get(root)):
|
||||||
|
continue
|
||||||
|
entry = {"species": sid, "drops": drops,
|
||||||
|
"zones": [{"zone": z, "levels": f"{lo}-{hi}"} for z, (lo, hi) in sorted(where.get(sid, {}).items())]}
|
||||||
|
if not entry["zones"]:
|
||||||
|
entry["line"] = [{"zone": z, "species": member} for z, member in sorted(line_zones[root].items())]
|
||||||
|
pokemon.append(entry)
|
||||||
|
|
||||||
|
guardians = []
|
||||||
|
for path in sorted((DATA / "cobblesurvival/guardians").glob("*.json")):
|
||||||
|
g = json.loads(path.read_text())
|
||||||
|
name, *props = g["pokemon"].split()
|
||||||
|
guardians.append({"zone": g["zone"], "species": f"cobblemon:{name}",
|
||||||
|
"level": int(next(p.split("=")[1] for p in props if p.startswith("level="))),
|
||||||
|
"drops": g["drops"], "respawn_hours": g["respawn_hours"]})
|
||||||
|
|
||||||
|
end = json.loads((DATA / "cobblesurvival/bosses/end.json").read_text())["end"]
|
||||||
|
end_boss = {"species": f"cobblemon:{end['species']}", "level": end["level"], "xp_first_win": end["xp_first_win"],
|
||||||
|
"xp": end["xp"], "respawn_hours": end["respawn_hours"],
|
||||||
|
"first_win": [{"item": "minecraft:dragon_egg", "count": 1}]}
|
||||||
|
|
||||||
|
machines = DATA / "cobblesurvival/machines"
|
||||||
|
altar = json.loads((machines / "altar_core.json").read_text())["altar"]
|
||||||
|
rebirth = json.loads((machines / "rebirth_core.json").read_text())["rebirth"]
|
||||||
|
|
||||||
|
ore_veins = defaultdict(list)
|
||||||
|
for ore, spec in plan["ores"].items():
|
||||||
|
ore_veins[spec["zone"]].append({"item": spec["item"], "regen_minutes": spec["regen_minutes"]})
|
||||||
|
|
||||||
|
finds = defaultdict(lambda: defaultdict(set))
|
||||||
|
for path in sorted((DATA / "cobblebase/loot_table").glob("*.json")):
|
||||||
|
prefix, _, tier = path.stem.rpartition("_")
|
||||||
|
skill = SKILLS.get(prefix)
|
||||||
|
if not skill:
|
||||||
|
continue
|
||||||
|
for pool in json.loads(path.read_text()).get("pools", []):
|
||||||
|
for e in pool.get("entries", []):
|
||||||
|
if e.get("type") != "minecraft:item":
|
||||||
|
continue
|
||||||
|
zone_conditions = [c for c in e.get("conditions", []) if c.get("condition") == "cobblesurvival:in_zone"]
|
||||||
|
for zid in (zone_conditions[0]["zones"] if zone_conditions else ["*"]):
|
||||||
|
finds[(skill, zid)][e["name"]].add(tier)
|
||||||
|
pasture_finds = [{"skill": skill, "zone": None if zid == "*" else zid,
|
||||||
|
"items": [{"item": item, "tiers": sorted(tiers)} for item, tiers in sorted(items.items())]}
|
||||||
|
for (skill, zid), items in sorted(finds.items(), key=lambda kv: (kv[0][0], kv[0][1] != "*", kv[0][1]))]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"_comment": "Generated by tools/jei_sources.py from the pack's data; do not edit by hand. Read by the companion "
|
||||||
|
"mod's JEI plugin (jei/DropSourcesJei.kt); checked against the live server by the self-test "
|
||||||
|
"drop_sources_match_the_server.",
|
||||||
|
"zones": {zid: {"name": z["name"], "levels": z["levels"]} for zid, z in zones.items()},
|
||||||
|
"pokemon": pokemon, "spawn_drops": spawn_drops, "guardians": guardians, "end_boss": end_boss,
|
||||||
|
"altar": {"tiers": altar["tiers"], "specials": altar.get("specials", [])}, "rebirth": rebirth.get("extra", []),
|
||||||
|
"ore_veins": [{"zone": z, "items": items} for z, items in sorted(ore_veins.items())],
|
||||||
|
"pasture_finds": pasture_finds,
|
||||||
|
}
|
||||||
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUT.write_text(json.dumps(data, indent=1, ensure_ascii=False) + "\n")
|
||||||
|
print(f"{len(pokemon)} Pokémon, {len(spawn_drops)} spawns with own drops, {len(guardians)} guardians, "
|
||||||
|
f"{len(ore_veins)} zones with veins, {len(pasture_finds)} pasture find lists; wrote {OUT.relative_to(ROOT)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+3
-2
@@ -161,7 +161,8 @@ def zone_data():
|
|||||||
pool = DATA / f"spawn_pool_world/{zid}.json"
|
pool = DATA / f"spawn_pool_world/{zid}.json"
|
||||||
spawns = json.loads(pool.read_text())["spawns"] if pool.exists() else []
|
spawns = json.loads(pool.read_text())["spawns"] if pool.exists() else []
|
||||||
z["species"] = sorted({f"cobblemon:{s['pokemon'].split()[0].lower()}" for s in spawns})
|
z["species"] = sorted({f"cobblemon:{s['pokemon'].split()[0].lower()}" for s in spawns})
|
||||||
z["veins"] = sorted({n["vein"] for n in nodes if n["zone"] == zid})
|
# What its ore veins give when mined (world/zones.json → ores → item; veins are named <ore>_node).
|
||||||
|
z["ore_items"] = [plan["ores"][v.removesuffix("_node")]["item"] for v in sorted({n["vein"] for n in nodes if n["zone"] == zid})]
|
||||||
guardian = DATA / f"guardians/{zid}.json"
|
guardian = DATA / f"guardians/{zid}.json"
|
||||||
z["guardian_data"] = json.loads(guardian.read_text()) if guardian.exists() else None
|
z["guardian_data"] = json.loads(guardian.read_text()) if guardian.exists() else None
|
||||||
return zones
|
return zones
|
||||||
@@ -194,7 +195,7 @@ def title_case(item):
|
|||||||
|
|
||||||
def zone_fields(zone, chapter, book):
|
def zone_fields(zone, chapter, book):
|
||||||
g = zone["guardian_data"]
|
g = zone["guardian_data"]
|
||||||
ores = [book["ore_items"][v] for v in zone["veins"] if v in book["ore_items"]]
|
ores = zone["ore_items"]
|
||||||
return {
|
return {
|
||||||
"zone": zone["name"], "zone_id": zone["id"], "low": zone["levels"][0], "high": zone["levels"][1],
|
"zone": zone["name"], "zone_id": zone["id"], "low": zone["levels"][0], "high": zone["levels"][1],
|
||||||
"hazard_text": chapter.get("hazard_text", ""),
|
"hazard_text": chapter.get("hazard_text", ""),
|
||||||
|
|||||||
+21
-21
@@ -7,27 +7,27 @@
|
|||||||
"wanderers": {"grace_levels": 10},
|
"wanderers": {"grace_levels": 10},
|
||||||
"ore_defaults": {"fill": 0.7, "node_fill": 0.8, "attempts": 1, "jitter": 0.1},
|
"ore_defaults": {"fill": 0.7, "node_fill": 0.8, "attempts": 1, "jitter": 0.1},
|
||||||
"ores": {
|
"ores": {
|
||||||
"coal": {"zone": "button_isle", "stone": "minecraft:coal_ore", "deepslate": "minecraft:deepslate_coal_ore", "regen_minutes": 20, "nodes": {"chance": 0.5, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-32, 100], "spacing": 6, "attempts": 2}},
|
"coal": {"zone": "button_isle", "item": "minecraft:coal", "stone": "minecraft:coal_ore", "deepslate": "minecraft:deepslate_coal_ore", "regen_minutes": 20, "nodes": {"chance": 0.5, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-32, 100], "spacing": 6, "attempts": 2}},
|
||||||
"copper": {"zone": "southwold", "stone": "minecraft:copper_ore", "deepslate": "minecraft:deepslate_copper_ore", "regen_minutes": 30, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-32, 100], "spacing": 8, "attempts": 2}},
|
"copper": {"zone": "southwold", "item": "minecraft:raw_copper", "stone": "minecraft:copper_ore", "deepslate": "minecraft:deepslate_copper_ore", "regen_minutes": 30, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-32, 100], "spacing": 8, "attempts": 2}},
|
||||||
"iron": {"zone": "westmarch", "stone": "minecraft:iron_ore", "deepslate": "minecraft:deepslate_iron_ore", "regen_minutes": 40, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-48, 90], "spacing": 8, "attempts": 2}},
|
"iron": {"zone": "westmarch", "item": "minecraft:raw_iron", "stone": "minecraft:iron_ore", "deepslate": "minecraft:deepslate_iron_ore", "regen_minutes": 40, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 4], "y": [-48, 90], "spacing": 8, "attempts": 2}},
|
||||||
"redstone": {"zone": "jungle", "stone": "minecraft:redstone_ore", "deepslate": "minecraft:deepslate_redstone_ore", "regen_minutes": 45, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-56, 60], "spacing": 9, "attempts": 1}},
|
"redstone": {"zone": "jungle", "item": "minecraft:redstone", "stone": "minecraft:redstone_ore", "deepslate": "minecraft:deepslate_redstone_ore", "regen_minutes": 45, "nodes": {"chance": 0.2, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-56, 60], "spacing": 9, "attempts": 1}},
|
||||||
"lapis": {"zone": "highlands", "stone": "minecraft:lapis_ore", "deepslate": "minecraft:deepslate_lapis_ore", "regen_minutes": 45, "nodes": {"chance": 0.15, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-56, 80], "spacing": 9, "attempts": 1}},
|
"lapis": {"zone": "highlands", "item": "minecraft:lapis_lazuli", "stone": "minecraft:lapis_ore", "deepslate": "minecraft:deepslate_lapis_ore", "regen_minutes": 45, "nodes": {"chance": 0.15, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-56, 80], "spacing": 9, "attempts": 1}},
|
||||||
"gold": {"zone": "sunscorch", "stone": "minecraft:gold_ore", "deepslate": "minecraft:deepslate_gold_ore", "regen_minutes": 60, "nodes": {"chance": 0.3, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-48, 120], "spacing": 7, "attempts": 2}},
|
"gold": {"zone": "sunscorch", "item": "minecraft:raw_gold", "stone": "minecraft:gold_ore", "deepslate": "minecraft:deepslate_gold_ore", "regen_minutes": 60, "nodes": {"chance": 0.3, "radius": [0, 1]}, "veins": {"radius": [2, 3], "y": [-48, 120], "spacing": 7, "attempts": 2}},
|
||||||
"emerald": {"zone": "frostpeak", "stone": "minecraft:emerald_ore", "deepslate": "minecraft:deepslate_emerald_ore", "regen_minutes": 75, "nodes": {"chance": 0.15, "radius": [0, 1]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 8, "attempts": 2}},
|
"emerald": {"zone": "frostpeak", "item": "minecraft:emerald", "stone": "minecraft:emerald_ore", "deepslate": "minecraft:deepslate_emerald_ore", "regen_minutes": 75, "nodes": {"chance": 0.15, "radius": [0, 1]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 8, "attempts": 2}},
|
||||||
"amethyst": {"zone": "dripstone", "blocks": [["minecraft:amethyst_block", 3], ["minecraft:budding_amethyst", 1]], "regen_minutes": 120, "nodes": {"chance": 0.5, "tries": 8, "radius": [1, 1], "y": [0, 40]}},
|
"amethyst": {"zone": "dripstone", "item": "minecraft:amethyst_shard", "blocks": [["minecraft:amethyst_block", 3], ["minecraft:budding_amethyst", 1]], "regen_minutes": 120, "nodes": {"chance": 0.5, "tries": 8, "radius": [1, 1], "y": [0, 40]}},
|
||||||
"diamond": {"zone": "deep_dark", "deepslate": "minecraft:deepslate_diamond_ore", "regen_minutes": 90, "nodes": {"chance": 0.15, "tries": 8, "radius": [1, 1], "y": [-64, -24]}},
|
"diamond": {"zone": "deep_dark", "item": "minecraft:diamond", "deepslate": "minecraft:deepslate_diamond_ore", "regen_minutes": 90, "nodes": {"chance": 0.15, "tries": 8, "radius": [1, 1], "y": [-64, -24]}},
|
||||||
"quartz": {"zone": "basalt", "blocks": [["minecraft:nether_quartz_ore", 1]], "regen_minutes": 60, "nodes": {"chance": 0.5, "tries": 8, "radius": [1, 1], "y": [-64, 40]}},
|
"quartz": {"zone": "basalt", "item": "minecraft:quartz", "blocks": [["minecraft:nether_quartz_ore", 1]], "regen_minutes": 60, "nodes": {"chance": 0.5, "tries": 8, "radius": [1, 1], "y": [-64, 40]}},
|
||||||
"netherite": {"zone": "basalt", "blocks": [["minecraft:ancient_debris", 1]], "regen_minutes": 180, "nodes": {"chance": 0.06, "tries": 8, "radius": [0, 0], "y": [-64, 40]}},
|
"netherite": {"zone": "basalt", "item": "minecraft:ancient_debris", "blocks": [["minecraft:ancient_debris", 1]], "regen_minutes": 180, "nodes": {"chance": 0.06, "tries": 8, "radius": [0, 0], "y": [-64, 40]}},
|
||||||
"thunder_stone": {"zone": "westmarch", "stone": "cobblemon:thunder_stone_ore", "deepslate": "cobblemon:deepslate_thunder_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.03, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
|
"thunder_stone": {"zone": "westmarch", "item": "cobblemon:thunder_stone", "stone": "cobblemon:thunder_stone_ore", "deepslate": "cobblemon:deepslate_thunder_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.03, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
|
||||||
"shiny_stone": {"zone": "lush_caves", "stone": "cobblemon:shiny_stone_ore", "deepslate": "cobblemon:deepslate_shiny_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.15, "tries": 8, "radius": [0, 1], "y": [-16, 40]}},
|
"shiny_stone": {"zone": "lush_caves", "item": "cobblemon:shiny_stone", "stone": "cobblemon:shiny_stone_ore", "deepslate": "cobblemon:deepslate_shiny_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.15, "tries": 8, "radius": [0, 1], "y": [-16, 40]}},
|
||||||
"leaf_stone": {"zone": "jungle", "stone": "cobblemon:leaf_stone_ore", "deepslate": "cobblemon:deepslate_leaf_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
|
"leaf_stone": {"zone": "jungle", "item": "cobblemon:leaf_stone", "stone": "cobblemon:leaf_stone_ore", "deepslate": "cobblemon:deepslate_leaf_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
|
||||||
"water_stone": {"zone": "mire", "stone": "cobblemon:water_stone_ore", "deepslate": "cobblemon:deepslate_water_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
|
"water_stone": {"zone": "mire", "item": "cobblemon:water_stone", "stone": "cobblemon:water_stone_ore", "deepslate": "cobblemon:deepslate_water_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
|
||||||
"moon_stone": {"zone": "dripstone", "blocks": [["cobblemon:dripstone_moon_stone_ore", 1], ["cobblemon:moon_stone_ore", 1], ["cobblemon:deepslate_moon_stone_ore", 1]], "regen_minutes": 60, "nodes": {"chance": 0.25, "tries": 8, "radius": [0, 1], "y": [0, 40]}},
|
"moon_stone": {"zone": "dripstone", "item": "cobblemon:moon_stone", "blocks": [["cobblemon:dripstone_moon_stone_ore", 1], ["cobblemon:moon_stone_ore", 1], ["cobblemon:deepslate_moon_stone_ore", 1]], "regen_minutes": 60, "nodes": {"chance": 0.25, "tries": 8, "radius": [0, 1], "y": [0, 40]}},
|
||||||
"sun_stone": {"zone": "highlands", "stone": "cobblemon:sun_stone_ore", "deepslate": "cobblemon:deepslate_sun_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.02, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
|
"sun_stone": {"zone": "highlands", "item": "cobblemon:sun_stone", "stone": "cobblemon:sun_stone_ore", "deepslate": "cobblemon:deepslate_sun_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.02, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 100], "spacing": 11, "attempts": 1}},
|
||||||
"fire_stone": {"zone": "sunscorch", "stone": "cobblemon:fire_stone_ore", "deepslate": "cobblemon:deepslate_fire_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.08, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 120], "spacing": 9, "attempts": 2}},
|
"fire_stone": {"zone": "sunscorch", "item": "cobblemon:fire_stone", "stone": "cobblemon:fire_stone_ore", "deepslate": "cobblemon:deepslate_fire_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.08, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-48, 120], "spacing": 9, "attempts": 2}},
|
||||||
"ice_stone": {"zone": "frostpeak", "stone": "cobblemon:ice_stone_ore", "deepslate": "cobblemon:deepslate_ice_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.06, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 9, "attempts": 2}},
|
"ice_stone": {"zone": "frostpeak", "item": "cobblemon:ice_stone", "stone": "cobblemon:ice_stone_ore", "deepslate": "cobblemon:deepslate_ice_stone_ore", "regen_minutes": 60, "nodes": {"chance": 0.06, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 9, "attempts": 2}},
|
||||||
"dawn_stone": {"zone": "frostpeak", "stone": "cobblemon:dawn_stone_ore", "deepslate": "cobblemon:deepslate_dawn_stone_ore", "regen_minutes": 75, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 10, "attempts": 2}},
|
"dawn_stone": {"zone": "frostpeak", "item": "cobblemon:dawn_stone", "stone": "cobblemon:dawn_stone_ore", "deepslate": "cobblemon:deepslate_dawn_stone_ore", "regen_minutes": 75, "nodes": {"chance": 0.04, "radius": [0, 0]}, "veins": {"radius": [1, 2], "y": [-16, 320], "spacing": 10, "attempts": 2}},
|
||||||
"dusk_stone": {"zone": "deep_dark", "deepslate": "cobblemon:deepslate_dusk_stone_ore", "regen_minutes": 75, "nodes": {"chance": 0.1, "tries": 8, "radius": [0, 1], "y": [-64, -24]}}
|
"dusk_stone": {"zone": "deep_dark", "item": "cobblemon:dusk_stone", "deepslate": "cobblemon:deepslate_dusk_stone_ore", "regen_minutes": 75, "nodes": {"chance": 0.1, "tries": 8, "radius": [0, 1], "y": [-64, -24]}}
|
||||||
},
|
},
|
||||||
"species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt",
|
"species": {"durant": "basalt", "torkoal": "basalt", "heatmor": "basalt",
|
||||||
"ditto": {"zone": "deep_dark", "all_entries": true}},
|
"ditto": {"zone": "deep_dark", "all_entries": true}},
|
||||||
|
|||||||
Reference in new issue
Block a user