Public Access
This commit is contained in:
1 parent
4a87c9fae6
commit
b154455563
14 files changed
+361
-8
No files matched your search
@@ -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}
|
||||
}
|
||||
Reference in new issue
Block a user