Phase 8: the quest book, generated from docs/quests.yaml

- tools/quests.py writes pack/config/ftbquests/quests/ in FTB Quests 2101's
  format (data.snbt, chapters/*.snbt, texts in lang/en_us.snbt) with ids
  derived from the YAML names, so progress survives regeneration. Flexible
  progression, no lock icons, no rewards (Adrian, 2026-11-03). tools/build.py
  runs it.
- docs/quests.yaml: 18 chapters, 74 quests. Getting Started; one chapter per
  zone and the End from a zone template (reach it, catch three of its
  Pokemon, mine its ores, beat its guardian) plus chapter extras (Mire
  Waders, Eyes of Ender, Eternatus); Pokemon Machines; Base and Jobs.
- Node positions: --pull reads them from an FTB Quests folder into the
  layout block (only moved nodes); tools/prism_instance.py pulls from the
  instance before each sync and rebuilds if something moved.
- The pack's own events as hidden advancements (cobblesurvival:events/...),
  granted by QuestEvents: zone reached, guardian beaten, machine filled, End
  boss beaten.
- Self-tests quest_book_loads and quest_events_mark_zones; the guardian and
  furnace tests check their events. Self-tests and island check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-11-03 10:57:35 +01:00
1 parent 9fb5e9895e
commit c5d1cf93f4
70 files changed
+3665 -7

No files matched your search

@@ -4,6 +4,7 @@ import com.cobblemon.mod.common.battles.BattleBuilder
import com.cobblemon.mod.common.battles.MoveActionResponse
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
import com.cobblemon.mod.common.util.isInBattle
import dev.cobblesurvival.progress.QuestEvents
import dev.cobblesurvival.zone.GuardianBars
import dev.cobblesurvival.zone.Guardians
import dev.cobblesurvival.zone.Zones
@@ -73,6 +74,7 @@ object GuardianTests {
.thenExecute {
s.check(Guardians.guardianOf(level, ZONE) == null) { "the guardian came back right away" }
s.check(GuardianBars.barFor(ZONE, player) == null) { "the beaten guardian's bar is still shown" }
s.check(QuestEvents.done(player, "guardian/$ZONE")) { "beating the guardian didn't mark guardian/$ZONE for the quest book" }
Guardians.expireDefeatForTest(level, ZONE)
}
.thenWaitUntil {
@@ -13,6 +13,7 @@ import dev.cobblesurvival.machine.MachineGhosts
import dev.cobblesurvival.machine.MachineShowcase
import dev.cobblesurvival.machine.MachineWork
import dev.cobblesurvival.machine.Shell
import dev.cobblesurvival.progress.QuestEvents
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.registries.BuiltInRegistries
@@ -240,6 +241,7 @@ object MachineTests {
val player = s.player()
val core = build(s, "heat_core", Blocks.BRICKS)
insert(s, core, player, s.give(player, s.pokemon("charmander", 50)))
s.check(QuestEvents.done(player, "machine/heat_core")) { "a Large Furnace built didn't mark machine/heat_core for the quest book" }
// A furnace against the east wall, a hopper with raw iron above it and an empty one below; no fuel anywhere.
val furnace = core.blockPos.offset(2, 0, -1)
level.setBlockAndUpdate(furnace, Blocks.FURNACE.defaultBlockState())
@@ -2,7 +2,13 @@ package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import dev.cobblesurvival.progress.QuestEvents
import dev.cobblesurvival.zone.Zones
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
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
@@ -12,8 +18,63 @@ object QuestTests {
fun all() = listOf(
SelfTestCase("quests_have_cobblemon_tasks", 20, ::cobblemonTasks),
SelfTestCase("jei_addon_shows_island_spawns", 20, ::addonSpawns),
SelfTestCase("quest_book_loads", 20, ::bookLoads),
SelfTestCase("quest_events_mark_zones", 200, ::zoneEvent),
)
/** At least this many quests in the book (docs/quests.yaml: 74 on 2026-11-03). */
private const val MIN_QUESTS = 70
private fun field(obj: Any, name: String): Any? {
var c: Class<*>? = obj.javaClass
while (c != null) {
runCatching { return c.getDeclaredField(name).apply { isAccessible = true }.get(obj) }
c = c.superclass
}
throw GameTestAssertException("${obj.javaClass.simpleName} has no field $name")
}
/**
* FTB Quests loaded the generated book (tools/quests.py): enough quests, flexible progression, and every task
* understood: no unknown task type, every item exists, every advancement exists on the server.
*/
private fun bookLoads(s: Scenario) {
val file = Class.forName("dev.ftb.mods.ftbquests.quest.ServerQuestFile").getField("INSTANCE").get(null)
?: throw GameTestAssertException("FTB Quests has no server quest file")
val quests = mutableListOf<Any>()
file.javaClass.getMethod("forAllQuests", Consumer::class.java).invoke(file, Consumer<Any> { quests += it })
s.check(quests.size >= MIN_QUESTS) { "FTB Quests loaded ${quests.size} quests, not the book's $MIN_QUESTS or more" }
s.check(field(file, "progressionMode").toString().equals("flexible", ignoreCase = true)) { "the book isn't in flexible mode" }
val problems = mutableListOf<String>()
for (quest in quests) {
for (task in quest.javaClass.getMethod("getTasks").invoke(quest) as Collection<*>) {
when (task!!.javaClass.simpleName) {
"CustomTask" -> problems += "unknown task type in quest ${quest}"
"ItemTask" -> if ((field(task, "itemStack") as ItemStack).isEmpty) problems += "an item task with no item in $quest"
"AdvancementTask" -> {
val id = field(task, "advancement") as ResourceLocation
if (s.level.server.advancements.get(id) == null) problems += "no advancement $id"
}
}
}
}
s.check(problems.isEmpty()) { problems.take(5).joinToString("; ") }
s.helper.succeed()
}
/** Walking into a zone marks the hidden advancement its "reach the zone" quest checks (progress/QuestEvents.kt). */
private fun zoneEvent(s: Scenario) {
val sample = Zones.samples["button_isle"] ?: throw GameTestAssertException("no sample point for Button Isle")
val player = s.player()
s.helper.startSequence()
.thenExecute {
s.level.getChunk(sample.x shr 4, sample.z shr 4)
s.moveTo(player, sample.x + 0.5, s.level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z).toDouble(), sample.z + 0.5)
}
.thenWaitUntil { s.check(QuestEvents.done(player, "zone/button_isle")) { "reaching Button Isle didn't mark zone/button_isle" } }
.thenSucceed()
}
/** Cobblemon Quests Reloaded adds its task type (catch, defeat, evolve, level up, ...) to FTB Quests. */
private fun cobblemonTasks(s: Scenario) {
val types = Class.forName("dev.ftb.mods.ftbquests.quest.task.TaskTypes").getField("TYPES").get(null) as Map<*, *>