Complete Milestone 7 crafting and portal guide (#79)
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-10-19 04:01:03 -04:00
1 parent 4a87c9fae6
commit b154455563
14 files changed
+361 -8

No files matched your search

+14
View File
@@ -105,6 +105,20 @@ JEI is optional; vanilla crafting
recipes and all writing interactions work without it. For a development client
with JEI, run `./gradlew runClient -PwithJei=true`.
Four Crystal Shards craft one Crystal Block; eight Crystal Blocks around an
empty crafting-grid center make one Book Receptacle. Picking up a shard unlocks
the block recipe, and picking up a block unlocks the Receptacle recipe. The
existing Ink Vial recipe uses two black dyes, a glass bottle, and a water
bucket; the empty bucket stays in the crafting grid. These JSON recipes appear
in JEI when it is installed and can be replaced by a data pack at
`data/rivencraft/recipe/<recipe_name>.json`. To test a replacement, use a fresh
world or `/reload`, then check the result in a crafting table and vanilla
Crafter. A vanilla Lectern converts to a Rivencraft Book Stand when a Rivencraft
book or page is placed on it; this in-world interaction has no JEI guide.
With Create installed, Crystal Blocks and Book Receptacles have an optional Ponder scene
showing crystal-frame construction, book activation, and a parrot crossing the portal. Use the Ponder shortcut
while hovering either item; Rivencraft does not require Create to run.
Craft a Book Binder from three iron ingots and five planks. Put leather or an
empty Notebook in the left cover slot. Enter a title, then place a Link Panel
Page first in the horizontal page strip. Click a page in the strip with a
+1 -1
View File
@@ -52,7 +52,7 @@ Legacy's travel path creates no automatic Return Link. Rivencraft currently defa
| Area | Legacy evidence and modern boundary |
| --- | --- |
| Crafting | [Legacy recipes](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/resources/assets/mystcraft/recipes) specify the Receptacle, Binder, Mixer, Folder, Desk, and other writing blocks. [`RecipeLinkingbook`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/RecipeLinkingbook.java#L28-L60) combines one Link Panel and leather anywhere in the grid. Rivencraft already has Binder, Mixer, Ink Vial, Notebook, Desk, Crystal Block, and Linking Book recipes. #79 adds only missing recipes/processes; preserve vanilla-Lectern conversion, noncraftable Blank Pages, and optional JEI. |
| Crafting | [Legacy recipes](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/resources/assets/mystcraft/recipes) specify the Receptacle, Binder, Mixer, Folder, Desk, and other writing blocks. [`RecipeLinkingbook`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/RecipeLinkingbook.java#L28-L60) combines one Link Panel and leather anywhere in the grid. Rivencraft has Binder, Mixer, Ink Vial, Notebook, Desk, Crystal Block, Linking Book, and Receptacle recipes. #79 keeps these data-driven and adds recipe-book unlocks for Crystal Block, Receptacle, and Ink Vial. The Legacy potion-based ink variant is omitted: in 1.21.1 a simple potion ingredient does not return its glass bottle, while the existing water-bucket recipe returns its bucket. The Legacy Folder/Portfolio use Rivencraft's Notebook; the old two-part Desk is one item; the old Lectern/Book Stand recipes use a vanilla Lectern converted in-world by adding a Rivencraft book or page. Blank Pages remain noncraftable and Link Panels remain outside the Desk. JEI automatically displays the JSON crafting recipes; its existing optional categories cover Mixer, Desk, and Binder processes. The lectern interaction intentionally has no JEI guide. With Create installed, an optional Ponder scene attached to the Receptacle item demonstrates frame construction and book insertion; the core mod still runs without Create. |
| Sounds | [`Sounds`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/Sounds.java#L8-L26) registers pop, link, disarm, following, intra-link, fissure-link, portal-link, and meteor roar. The matching OGGs are in the local extraction. Rivencraft already carries link, fissure-link, and meteor roar. #80 should reuse and credit available Legacy files before creating alternatives. |
| Advancements | [Legacy advancement JSONs](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/resources/assets/mystcraft/advancements) have inventory milestones for a symbol page, unlinked Linking Book, and Age Book; a Writing Desk trigger; a safe Age-travel trigger; and a hidden Quinn trigger. #81 should use modern server-owned criteria. Quinn has no established Rivencraft equivalent and should be documented or deliberately omitted, not awarded on preview. |
+1 -1
View File
@@ -36,7 +36,7 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.6.3.4
mod_version=0.6.4.4
immersive_portals_version=6.0.7
terrablender_version=4.1.0.8
serene_seasons_version_id=B6nTjh4b
@@ -3,10 +3,9 @@ package net.dnorberg.rivencraft.block;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.portal.CrystalPortalNetwork;
import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
import net.dnorberg.rivencraft.portal.PortalTravelQueue;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
@@ -64,8 +63,7 @@ public final class CrystalPortalBlock extends BaseEntityBlock {
ItemStack book = holder.getBook();
if (book.isEmpty()) return;
PortalTravelCooldown.mark(player, now);
if (book.is(ModItems.LINKING_BOOK.get())) LinkingBookItem.usePortalBook(player, book);
else if (book.is(ModItems.DESCRIPTIVE_BOOK.get())) DescriptiveBookItem.usePortalBook(player, book);
PortalTravelQueue.enqueue(player, server, pos, portal.receptacle(), book, now);
}
@Override protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbor,
@@ -85,6 +85,8 @@ public final class ClientEvents {
// the portal cell itself is invisible while its live view is active.
if (ModList.get().isLoaded("immersive_portals_core"))
ItemBlockRenderTypes.setRenderLayer(ModBlocks.CRYSTAL_BLOCK.get(), RenderType.translucent());
if (ModList.get().isLoaded("create"))
net.dnorberg.rivencraft.compat.RivencraftPonderGuide.register();
});
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem);
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickBlock);
@@ -0,0 +1,108 @@
package net.dnorberg.rivencraft.compat;
import net.createmod.ponder.api.PonderPalette;
import net.createmod.ponder.api.registration.PonderPlugin;
import net.createmod.ponder.api.registration.PonderSceneRegistrationHelper;
import net.createmod.ponder.api.scene.SceneBuilder;
import net.createmod.ponder.api.scene.SceneBuildingUtil;
import net.createmod.ponder.foundation.PonderIndex;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.BookReceptacleBlock;
import net.dnorberg.rivencraft.block.CrystalPortalBlock;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;
/** Create-only Ponder registration; this class is never loaded without Create. */
public final class RivencraftPonderGuide implements PonderPlugin {
private static final ResourceLocation DEPOT_TEMPLATE = ResourceLocation.fromNamespaceAndPath("create", "depot");
private RivencraftPonderGuide() {}
public static void register() {
PonderIndex.addPlugin(new RivencraftPonderGuide());
}
@Override
public String getModId() {
return Rivencraft.MOD_ID;
}
@Override
public void registerScenes(PonderSceneRegistrationHelper<ResourceLocation> helper) {
// Create is already present when this plugin is registered. Its small
// depot schematic supplies an emptyable 6×4×6 canvas, so Rivencraft
// does not need to ship a separate binary Ponder template.
helper.forComponents(ModItems.BOOK_RECEPTACLE.getId(), ModItems.CRYSTAL_BLOCK.getId())
.addStoryBoard(DEPOT_TEMPLATE, RivencraftPonderGuide::crystalPortal);
}
private static void crystalPortal(SceneBuilder scene, SceneBuildingUtil util) {
scene.title("crystal_portal", "Building a Crystal Portal");
scene.configureBasePlate(0, 0, 6);
scene.scaleSceneView(0.85F);
scene.showBasePlate();
scene.world().setBlocks(util.select().fromTo(0, 1, 0, 5, 3, 5),
Blocks.AIR.defaultBlockState(), false);
scene.idle(10);
BlockPos center = util.grid().at(3, 2, 3);
scene.overlay().showText(65).text("Build a closed crystal frame with an open center.")
.pointAt(util.vector().centerOf(center)).placeNearTarget();
for (int x = 2; x <= 4; x++) {
for (int y = 1; y <= 3; y++) {
if (x == 3 && y == 2) continue;
BlockPos pos = util.grid().at(x, y, 3);
scene.world().setBlock(pos, ModBlocks.CRYSTAL_BLOCK.get().defaultBlockState(), false);
scene.world().showSection(util.select().position(pos), Direction.DOWN);
scene.idle(3);
}
}
scene.idle(30);
BlockPos receptacle = util.grid().at(2, 1, 2);
scene.overlay().showText(70).text("Attach a Book Receptacle to one crystal, facing outward.")
.pointAt(util.vector().centerOf(receptacle)).placeNearTarget();
scene.world().setBlock(receptacle, ModBlocks.BOOK_RECEPTACLE.get().defaultBlockState()
.setValue(BookReceptacleBlock.FACING, Direction.NORTH), false);
scene.world().showSection(util.select().position(receptacle), Direction.DOWN);
scene.idle(75);
scene.overlay().showText(65).text("Insert a bound Linking or Descriptive Book to activate the portal.")
.colored(PonderPalette.INPUT).pointAt(util.vector().centerOf(receptacle)).placeNearTarget();
scene.overlay().showControls(util.vector().centerOf(receptacle),
net.createmod.catnip.math.Pointing.RIGHT, 55)
.rightClick().withItem(new ItemStack(ModItems.LINKING_BOOK.get()));
scene.idle(65);
scene.world().setBlock(center, ModBlocks.CRYSTAL_PORTAL.get().defaultBlockState()
.setValue(CrystalPortalBlock.AXIS, Direction.Axis.Z), false);
scene.world().showSection(util.select().position(center), Direction.DOWN);
scene.effects().indicateSuccess(center);
scene.overlay().showText(80).text("Step through the portal. Remove the book to close it.")
.colored(PonderPalette.OUTPUT).pointAt(util.vector().centerOf(center)).placeNearTarget();
scene.idle(20);
var parrot = scene.world().createEntity(world -> {
var bird = EntityType.PARROT.create(world);
bird.setNoAi(true);
bird.setPos(3.5, 2.05, 1.5);
bird.setYRot(0);
return bird;
});
scene.idle(10);
for (int step = 1; step <= 7; step++) {
double z = 1.5 + step * 0.42;
scene.world().modifyEntity(parrot, bird -> bird.setPos(3.5, 2.05, z));
scene.idle(5);
}
scene.world().modifyEntity(parrot, bird -> bird.discard());
scene.effects().indicateSuccess(center);
scene.idle(20);
scene.markAsFinished();
}
}
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.event;
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
import net.dnorberg.rivencraft.portal.PortalTravelQueue;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.fml.ModList;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
@@ -14,7 +15,10 @@ public final class AgeLifecycleEvents {
}
public static void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) {
if (event.getEntity() instanceof ServerPlayer player) PortalTravelCooldown.forget(player);
if (event.getEntity() instanceof ServerPlayer player) {
PortalTravelCooldown.forget(player);
PortalTravelQueue.forget(player);
}
if (event.getEntity() instanceof ServerPlayer player
&& ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.close(player);
@@ -23,12 +27,14 @@ public final class AgeLifecycleEvents {
public static void onServerStopping(ServerStoppingEvent event) {
PortalTravelCooldown.clear();
PortalTravelQueue.clear();
if (ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.closeAll();
}
}
public static void onServerTickPost(ServerTickEvent.Post event) {
PortalTravelQueue.process(event.getServer());
if (ModList.get().isLoaded("immersive_portals_core"))
ImmersivePortalsChunkLoaders.expire(event.getServer());
}
@@ -0,0 +1,105 @@
package net.dnorberg.rivencraft.gametest;
import java.util.ArrayList;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.crafting.CraftingInput;
import net.minecraft.world.item.crafting.RecipeType;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
/** Data-driven Milestone 7 crafting checks; every result is obtained from the loaded recipe manager. */
@GameTestHolder(Rivencraft.MOD_ID)
@PrefixGameTestTemplate(false)
public final class Milestone7CraftingGameTests {
private Milestone7CraftingGameTests() {}
@GameTest(template = "empty")
public static void crystalProgressionMatchesLegacyCounts(GameTestHelper helper) {
var level = helper.getLevel();
ItemStack shard = new ItemStack(ModItems.CRYSTAL_SHARD.get());
CraftingInput crystalInput = CraftingInput.of(2, 2, List.of(shard, shard, shard, shard));
var crystalRecipe = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, crystalInput, level)
.orElseThrow();
helper.assertValueEqual(crystalRecipe.id(), id("crystal_block"),
"Four shards must craft a Crystal Block");
ItemStack crystal = crystalRecipe.value().assemble(crystalInput, level.registryAccess());
helper.assertTrue(crystal.is(ModItems.CRYSTAL_BLOCK.get()) && crystal.getCount() == 1,
"The shard recipe must yield one Crystal Block");
List<ItemStack> ring = new ArrayList<>();
for (int slot = 0; slot < 9; slot++) ring.add(slot == 4 ? ItemStack.EMPTY : crystal.copy());
CraftingInput receptacleInput = CraftingInput.of(3, 3, ring);
var receptacleRecipe = level.getRecipeManager()
.getRecipeFor(RecipeType.CRAFTING, receptacleInput, level).orElseThrow();
helper.assertValueEqual(receptacleRecipe.id(), id("book_receptacle"),
"The Legacy eight-block ring must craft the Receptacle");
ItemStack result = receptacleRecipe.value().assemble(receptacleInput, level.registryAccess());
helper.assertTrue(result.is(ModItems.BOOK_RECEPTACLE.get()) && result.getCount() == 1,
"One ring must yield exactly one Book Receptacle");
helper.assertTrue(receptacleRecipe.value().getRemainingItems(receptacleInput).stream()
.allMatch(ItemStack::isEmpty), "Crystal crafting must not duplicate source blocks as remainders");
ring.set(4, crystal.copy());
helper.assertTrue(level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING,
CraftingInput.of(3, 3, ring), level).isEmpty(),
"A filled center must not match the Book Receptacle ring");
helper.succeed();
}
@GameTest(template = "empty")
public static void bucketInkReturnsItsContainer(GameTestHelper helper) {
var level = helper.getLevel();
ItemStack dye = new ItemStack(Items.BLACK_DYE);
CraftingInput bucketInput = CraftingInput.of(2, 2, List.of(
dye, dye, new ItemStack(Items.GLASS_BOTTLE), new ItemStack(Items.WATER_BUCKET)));
var bucketRecipe = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, bucketInput, level)
.orElseThrow();
helper.assertValueEqual(bucketRecipe.id(), id("ink_vial"), "Bucket ink recipe must remain registered");
helper.assertTrue(bucketRecipe.value().assemble(bucketInput, level.registryAccess())
.is(ModItems.INK_VIAL.get()), "Bucket recipe must yield an Ink Vial");
helper.assertTrue(bucketRecipe.value().getRemainingItems(bucketInput).stream()
.anyMatch(stack -> stack.is(Items.BUCKET)), "The water bucket must return an empty bucket");
helper.succeed();
}
@GameTest(template = "empty")
public static void recipeUnlocksAreLoadedAndBlankPagesStayUncraftable(GameTestHelper helper) {
var level = helper.getLevel();
for (String path : List.of("crystal_block", "book_receptacle", "ink_vial")) {
helper.assertTrue(level.getServer().getAdvancements().get(id("recipes/misc/" + path)) != null,
"Recipe unlock must load for " + path);
}
var player = helper.makeMockServerPlayerInLevel();
var shardUnlock = level.getServer().getAdvancements().get(id("recipes/misc/crystal_block"));
player.getAdvancements().award(shardUnlock, "has_crystal_shard");
helper.assertTrue(player.getRecipeBook().contains(id("crystal_block")),
"Discovering a shard must unlock the Crystal Block recipe");
var receptacleUnlock = level.getServer().getAdvancements().get(id("recipes/misc/book_receptacle"));
player.getAdvancements().award(receptacleUnlock, "has_crystal_block");
helper.assertTrue(player.getRecipeBook().contains(id("book_receptacle")),
"Discovering a Crystal Block must unlock the Receptacle recipe");
CraftingInput blankInput = CraftingInput.of(2, 2, List.of(
new ItemStack(Items.PAPER), new ItemStack(ModItems.INK_VIAL.get()),
ItemStack.EMPTY, ItemStack.EMPTY));
helper.assertTrue(level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, blankInput, level).isEmpty(),
"Blank Pages and Link Panels must not become grid-craftable");
ItemStack panel = PageItem.createLinkPanelPage();
CraftingInput linkInput = CraftingInput.of(2, 1, List.of(panel, new ItemStack(Items.LEATHER)));
helper.assertValueEqual(level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, linkInput, level)
.orElseThrow().id(), id("linking_book"),
"Link Panel crafting remains limited to the established Linking Book recipe");
helper.succeed();
}
private static ResourceLocation id(String path) {
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
}
}
@@ -0,0 +1,64 @@
package net.dnorberg.rivencraft.portal;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
/** Defers block-collision travel until the movement packet that entered the portal has finished. */
public final class PortalTravelQueue {
private static final Map<ServerPlayer, Pending> PENDING = new IdentityHashMap<>();
private record Pending(ServerLevel source, BlockPos portal, BlockPos receptacle,
ItemStack book, long queuedTick) {}
private record Travel(ServerPlayer player, Pending pending) {}
private PortalTravelQueue() {}
public static void enqueue(ServerPlayer player, ServerLevel source, BlockPos portal,
BlockPos receptacle, ItemStack book, long now) {
PENDING.putIfAbsent(player, new Pending(source, portal.immutable(), receptacle.immutable(),
book.copyWithCount(1), now));
}
public static void process(MinecraftServer server) {
List<Travel> ready = new ArrayList<>();
Iterator<Map.Entry<ServerPlayer, Pending>> iterator = PENDING.entrySet().iterator();
while (iterator.hasNext()) {
var entry = iterator.next();
if (server.getTickCount() <= entry.getValue().queuedTick()) continue;
ready.add(new Travel(entry.getKey(), entry.getValue()));
iterator.remove();
}
// Travel can itself cause block-collision callbacks, so do not keep an
// iterator over the pending map while changing the player's dimension.
for (Travel travel : ready) {
ServerPlayer player = travel.player();
Pending pending = travel.pending();
if (server.getPlayerList().getPlayer(player.getUUID()) != player
|| !player.isAlive() || player.serverLevel() != pending.source()
|| !(pending.source().getBlockEntity(pending.portal()) instanceof CrystalPortalBlockEntity portal)
|| !pending.receptacle().equals(portal.receptacle())
|| !(pending.source().getBlockEntity(pending.receptacle()) instanceof BookReceptacleBlockEntity holder)
|| !ItemStack.isSameItemSameComponents(holder.getBook(), pending.book())) continue;
if (pending.book().is(ModItems.LINKING_BOOK.get()))
LinkingBookItem.usePortalBook(player, pending.book());
else if (pending.book().is(ModItems.DESCRIPTIVE_BOOK.get()))
DescriptiveBookItem.usePortalBook(player, pending.book());
}
}
public static void forget(ServerPlayer player) { PENDING.remove(player); }
public static void clear() { PENDING.clear(); }
}
@@ -341,6 +341,11 @@
"jei.rivencraft.writing.copy": "At a Writing Desk, select a stored Page, add paper and ink, then take the copy. The source Page stays.",
"jei.rivencraft.writing.bind": "At a Book Binder, name the Age and add a cover plus Link Panel. Optional symbol Pages follow the panel.",
"jei.rivencraft.writing.linking_book": "Craft one Link Panel Page with leather to make an unwritten Linking Book. Open it to name and bind it.",
"rivencraft.ponder.crystal_portal.header": "Building a Crystal Portal",
"rivencraft.ponder.crystal_portal.text_1": "Build a closed crystal frame with an open center.",
"rivencraft.ponder.crystal_portal.text_2": "Attach a Book Receptacle to one crystal, facing outward.",
"rivencraft.ponder.crystal_portal.text_3": "Insert a bound Linking or Descriptive Book to activate the portal.",
"rivencraft.ponder.crystal_portal.text_4": "Step through the portal. Remove the book to close it.",
"itemGroup.rivencraft": "Rivencraft",
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
"itemGroup.rivencraft.pages.terrain": "Rivencraft Terrain Pages",
@@ -0,0 +1,15 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_crystal_block": {
"trigger": "minecraft:inventory_changed",
"conditions": {"items": [{"items": "rivencraft:crystal_block"}]}
},
"has_the_recipe": {
"trigger": "minecraft:recipe_unlocked",
"conditions": {"recipe": "rivencraft:book_receptacle"}
}
},
"requirements": [["has_crystal_block", "has_the_recipe"]],
"rewards": {"recipes": ["rivencraft:book_receptacle"]}
}
@@ -0,0 +1,15 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_crystal_shard": {
"trigger": "minecraft:inventory_changed",
"conditions": {"items": [{"items": "rivencraft:crystal_shard"}]}
},
"has_the_recipe": {
"trigger": "minecraft:recipe_unlocked",
"conditions": {"recipe": "rivencraft:crystal_block"}
}
},
"requirements": [["has_crystal_shard", "has_the_recipe"]],
"rewards": {"recipes": ["rivencraft:crystal_block"]}
}
@@ -0,0 +1,15 @@
{
"parent": "minecraft:recipes/root",
"criteria": {
"has_black_dye": {
"trigger": "minecraft:inventory_changed",
"conditions": {"items": [{"items": "minecraft:black_dye"}]}
},
"has_the_recipe": {
"trigger": "minecraft:recipe_unlocked",
"conditions": {"recipe": "rivencraft:ink_vial"}
}
},
"requirements": [["has_black_dye", "has_the_recipe"]],
"rewards": {"recipes": ["rivencraft:ink_vial"]}
}
@@ -1 +1,7 @@
{"type":"minecraft:crafting_shaped","pattern":["CCC","C C","CCC"],"key":{"C":{"item":"rivencraft:crystal_block"}},"result":{"id":"rivencraft:book_receptacle","count":1}}
{
"type": "minecraft:crafting_shaped",
"category": "misc",
"pattern": ["CCC", "C C", "CCC"],
"key": {"C": {"item": "rivencraft:crystal_block"}},
"result": {"id": "rivencraft:book_receptacle", "count": 1}
}