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:
1 parent
9fb5e9895e
commit
c5d1cf93f4
70 files changed
+3665
-7
No files matched your search
@@ -21,6 +21,7 @@ import dev.cobblesurvival.selftest.SelfTest
|
||||
import dev.cobblesurvival.world.OnlyPokemon
|
||||
import dev.cobblesurvival.world.SharedLoot
|
||||
import dev.cobblesurvival.world.SpecialPokemon
|
||||
import dev.cobblesurvival.progress.QuestEvents
|
||||
import dev.cobblesurvival.zone.GuardianBars
|
||||
import dev.cobblesurvival.zone.Guardians
|
||||
import dev.cobblesurvival.zone.InZoneCondition
|
||||
@@ -80,6 +81,7 @@ class CobbleSurvival {
|
||||
NeoForge.EVENT_BUS.addListener(Zones::onAddReloadListeners)
|
||||
Guardians.register()
|
||||
GuardianBars.register()
|
||||
QuestEvents.register()
|
||||
BossFights.register()
|
||||
BossToughness.register()
|
||||
EndBoss.register()
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.cobblemon.mod.common.api.events.battles.BattleFaintedEvent
|
||||
import com.cobblemon.mod.common.api.pokemon.PokemonProperties
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import dev.cobblesurvival.progress.QuestEvents
|
||||
import dev.cobblesurvival.zone.GuardianState
|
||||
import net.minecraft.ChatFormatting
|
||||
import net.minecraft.core.BlockPos
|
||||
@@ -149,6 +150,7 @@ object EndBoss {
|
||||
}
|
||||
val xp = if (firstWin) def?.xpFirstWin ?: 0 else def?.xp ?: 0
|
||||
players.forEach {
|
||||
QuestEvents.award(it, "end_boss")
|
||||
it.giveExperiencePoints(xp)
|
||||
it.sendSystemMessage(Component.translatableWithFallback("message.cobblesurvival.boss.end_beaten",
|
||||
"The End's guardian is beaten! The way home is open.").withStyle(ChatFormatting.GOLD))
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.cobblemon.mod.common.Cobblemon
|
||||
import com.cobblemon.mod.common.entity.pokemon.PokemonEntity
|
||||
import com.cobblemon.mod.common.pokemon.Pokemon
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import dev.cobblesurvival.progress.QuestEvents
|
||||
import java.util.UUID
|
||||
import net.minecraft.ChatFormatting
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.HolderLookup
|
||||
@@ -22,7 +24,6 @@ import net.minecraft.world.phys.AABB
|
||||
import net.neoforged.neoforge.energy.IEnergyStorage
|
||||
import net.neoforged.neoforge.items.IItemHandler
|
||||
import net.neoforged.neoforge.items.ItemStackHandler
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* The core of a Pokémon machine (roadmap Phase 6). It keeps the Pokémon put in (its data lives only here while it's
|
||||
@@ -148,6 +149,7 @@ class MachineBlockEntity(pos: BlockPos, state: BlockState) : BlockEntity(Machine
|
||||
MachineWork.onInsert(this, level, def)
|
||||
setChanged()
|
||||
display = MachineDisplay.spawn(level, pokemon, worldPosition, match.chamber, facing())
|
||||
QuestEvents.award(player, "machine/${BuiltInRegistries.BLOCK.getKey(blockState.block).path}")
|
||||
tell(player, Component.translatableWithFallback("message.cobblesurvival.machine.inserted", "%s went into the %s.", pokemon.getDisplayName(), machineName)
|
||||
.withStyle(ChatFormatting.GREEN))
|
||||
tell(player, MachineWork.status(this, level, def))
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package dev.cobblesurvival.progress
|
||||
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import dev.cobblesurvival.zone.Zones
|
||||
import net.minecraft.resources.ResourceLocation
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.neoforged.neoforge.common.NeoForge
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent
|
||||
|
||||
/**
|
||||
* The pack's own events as hidden advancements (roadmap Phase 8), for quest tasks of FTB Quests' "advancement" type:
|
||||
* `cobblesurvival:events/zone/<zone>` on reaching a zone, `events/guardian/<zone>` on beating its guardian,
|
||||
* `events/machine/<core>` on putting a Pokémon into a complete machine, `events/end_boss` on beating the End boss.
|
||||
* They have no display (no toast, not in the advancement screen) and unlock nothing (no flags: Adrian, 2026-10-31);
|
||||
* tools/quests.py writes them.
|
||||
*/
|
||||
object QuestEvents {
|
||||
private const val ZONE_CHECK_TICKS = 20
|
||||
|
||||
fun register() {
|
||||
NeoForge.EVENT_BUS.addListener<ServerTickEvent.Post> { event ->
|
||||
if (event.server.tickCount % ZONE_CHECK_TICKS != 0) return@addListener
|
||||
for (player in event.server.playerList.players) {
|
||||
Zones.at(player.serverLevel(), player.blockPosition())?.let { award(player, "zone/${it.id}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun id(event: String) = ResourceLocation.fromNamespaceAndPath(CobbleSurvival.MOD_ID, "events/$event")
|
||||
|
||||
fun award(player: ServerPlayer, event: String) {
|
||||
val advancement = player.server.advancements.get(id(event)) ?: return
|
||||
if (!player.advancements.getOrStartProgress(advancement).isDone) player.advancements.award(advancement, "done")
|
||||
}
|
||||
|
||||
fun done(player: ServerPlayer, event: String): Boolean =
|
||||
player.server.advancements.get(id(event))?.let { player.advancements.getOrStartProgress(it).isDone } == true
|
||||
}
|
||||
@@ -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<*, *>
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.google.gson.Gson
|
||||
import com.google.gson.JsonElement
|
||||
import dev.cobblesurvival.CobbleSurvival
|
||||
import dev.cobblesurvival.boss.BossToughness
|
||||
import dev.cobblesurvival.progress.QuestEvents
|
||||
import net.minecraft.ChatFormatting
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.HolderLookup
|
||||
@@ -204,6 +205,7 @@ object Guardians : SimpleJsonResourceReloadListener(Gson(), "guardians") {
|
||||
val zoneName = Zones.byId[zone]?.name ?: zone
|
||||
val hours = definition.respawnTicks / TICKS_PER_HOUR
|
||||
for (player in players) {
|
||||
QuestEvents.award(player, "guardian/$zone")
|
||||
for ((item, count) in definition.drops) {
|
||||
val stack = ItemStack(BuiltInRegistries.ITEM.get(item), count)
|
||||
if (!player.inventory.add(stack)) player.drop(stack, false)
|
||||
|
||||
Reference in new issue
Block a user