Compare commits

...
2 Commits
Author SHA1 Message Date
dnorberg 9044835ec5 Support horizontal portals and visible receptacle books (#93)
Build / build (push) Canceled after 0s
2026-10-21 23:02:32 -04:00
dnorberg 7ae41da23e Add Legacy-style symbol portfolios (#92)
Build / build (push) Canceled after 0s
2026-10-21 14:13:34 -04:00
42 changed files with 283 additions and 11 deletions

No files matched your search

+5
View File
@@ -118,6 +118,11 @@ 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.
Crystal frames may stand vertically or lie flat: a 3×3 Crystal Block ring with
an empty center forms a horizontal portal when a Book Receptacle is attached to
the top or underside of one of its blocks. Insert a bound Linking or Descriptive
Book to activate it; the book remains visible on the receptacle. A Linking Book aimed at its
own dimension still opens a visual portal, but travel requires Intra Linking.
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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

+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.7.2
mod_version=0.6.8.4
immersive_portals_version=6.0.7
terrablender_version=4.1.0.8
serene_seasons_version_id=pHEgQQUE
@@ -77,7 +77,7 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
}
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
Direction face = context.getClickedFace();
if (face == Direction.DOWN || !context.getLevel().getBlockState(
if (!context.getLevel().getBlockState(
context.getClickedPos().relative(face.getOpposite())).is(ModBlocks.CRYSTAL_BLOCK.get())) return null;
return defaultBlockState().setValue(FACING, face);
}
@@ -0,0 +1,67 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Axis;
import net.dnorberg.rivencraft.block.BookReceptacleBlock;
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.model.BookModel;
import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
/** Legacy's almost-closed book, oriented to the receptacle's mounting face. */
public final class BookReceptacleRenderer implements BlockEntityRenderer<BookReceptacleBlockEntity> {
private static final ResourceLocation LINKING_TEXTURE = ResourceLocation.fromNamespaceAndPath(
"rivencraft", "textures/entity/linking_book.png");
private static final ResourceLocation DESCRIPTIVE_TEXTURE = ResourceLocation.fromNamespaceAndPath(
"rivencraft", "textures/entity/descriptive_book.png");
private final BookModel model;
public BookReceptacleRenderer(BlockEntityRendererProvider.Context context) {
model = new BookModel(context.bakeLayer(ModelLayers.BOOK));
}
@Override
public void render(BookReceptacleBlockEntity receptacle, float partialTick, PoseStack poseStack,
MultiBufferSource buffers, int packedLight, int packedOverlay) {
if (receptacle.getBook().isEmpty()) return;
poseStack.pushPose();
poseStack.translate(0.5, 0.5, 0.5);
Direction facing = receptacle.getBlockState().getValue(BookReceptacleBlock.FACING);
switch (facing) {
case UP -> {
// Rotate the whole assembled top-mounted receptacle/book pair
// around the block center, matching the x=90 block model.
poseStack.mulPose(Axis.ZP.rotationDegrees(180));
poseStack.mulPose(Axis.XP.rotationDegrees(-90));
poseStack.mulPose(Axis.YP.rotationDegrees(90));
// Preserve the local half-turn that seats the book in its slot.
poseStack.mulPose(Axis.ZP.rotationDegrees(180));
}
case DOWN -> {
// The ceiling-mounted model is the inverted top assembly.
poseStack.mulPose(Axis.XP.rotationDegrees(-90));
poseStack.mulPose(Axis.YP.rotationDegrees(90));
poseStack.mulPose(Axis.ZP.rotationDegrees(180));
}
case NORTH -> poseStack.mulPose(Axis.YP.rotationDegrees(-90));
case SOUTH -> poseStack.mulPose(Axis.YP.rotationDegrees(90));
case EAST -> poseStack.mulPose(Axis.YP.rotationDegrees(180));
case WEST -> { }
}
poseStack.scale(0.8F, 0.8F, 0.8F);
model.setupAnim(0.0F, 0.1F, 0.1F, 0.005F);
ResourceLocation texture = receptacle.getBook().is(ModItems.DESCRIPTIVE_BOOK.get())
? DESCRIPTIVE_TEXTURE : LINKING_TEXTURE;
VertexConsumer vertices = buffers.getBuffer(RenderType.entitySolid(texture));
model.render(poseStack, vertices, packedLight, packedOverlay, -1);
poseStack.popPose();
}
}
@@ -38,6 +38,7 @@ public final class ClientEvents {
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(ModEntities.DROPPED_BOOK.get(), DroppedBookRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_RECEPTACLE.get(), BookReceptacleRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.WRITING_DESK.get(), WritingDeskRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.STAR_FISSURE.get(), StarFissureRenderer::new);
}
@@ -25,12 +25,14 @@ import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.book.LinkProperties;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
import net.dnorberg.rivencraft.writing.InkModifierIngredients;
import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolReference;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
@@ -63,6 +65,11 @@ public final class RivencraftJeiPlugin implements IModPlugin {
case UNRESOLVED_SYMBOL -> "unresolved:" + content.symbol().orElseThrow().id();
};
});
registration.registerSubtypeInterpreter(ModItems.SYMBOL_PORTFOLIO.get(),
(IIngredientSubtypeInterpreter<ItemStack>) (stack, context) -> {
Component name = stack.get(DataComponents.CUSTOM_NAME);
return name == null ? "empty" : name.getString();
});
}
@Override
@@ -72,6 +79,7 @@ public final class RivencraftJeiPlugin implements IModPlugin {
var connection = Minecraft.getInstance().getConnection();
if (connection != null) {
pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess()));
pages.addAll(ModCreativeTabs.creativeSymbolPortfolios(connection.registryAccess()));
} else {
PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage)
.forEach(pages::add);
@@ -84,6 +92,8 @@ public final class RivencraftJeiPlugin implements IModPlugin {
// JEI discovers the base Page item as a blank stack; only authored pages belong in its list.
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
List.of(new ItemStack(ModItems.PAGE.get())), List.of(UidContext.Ingredient));
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
List.of(new ItemStack(ModItems.SYMBOL_PORTFOLIO.get())), List.of(UidContext.Ingredient));
var connection = Minecraft.getInstance().getConnection();
if (connection == null) return;
Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream()
@@ -98,6 +108,19 @@ public final class RivencraftJeiPlugin implements IModPlugin {
if (!missing.isEmpty()) {
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missing);
}
Set<String> knownPortfolios = runtime.getIngredientManager().getAllItemStacks().stream()
.filter(stack -> stack.is(ModItems.SYMBOL_PORTFOLIO.get()))
.map(stack -> stack.get(DataComponents.CUSTOM_NAME))
.filter(java.util.Objects::nonNull)
.map(Component::getString)
.collect(Collectors.toSet());
List<ItemStack> missingPortfolios = ModCreativeTabs.creativeSymbolPortfolios(connection.registryAccess())
.stream()
.filter(stack -> !knownPortfolios.contains(stack.getHoverName().getString()))
.toList();
if (!missingPortfolios.isEmpty()) {
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missingPortfolios);
}
}
@Override
@@ -6,6 +6,9 @@ import net.dnorberg.rivencraft.block.CrystalPortalBlock;
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.LinkProperties;
import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.portal.CrystalPortalNetwork;
import net.dnorberg.rivencraft.portal.CrystalPortalSurfaces;
import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
@@ -19,7 +22,11 @@ import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
@@ -154,13 +161,54 @@ public final class CrystalPortalGameTests {
BlockPos holderPos = helper.absolutePos(new BlockPos(0, 2, 1));
level.setBlock(holderPos, ModBlocks.BOOK_RECEPTACLE.get().defaultBlockState()
.setValue(BookReceptacleBlock.FACING, Direction.UP), 3);
((BookReceptacleBlockEntity) level.getBlockEntity(holderPos))
.setBook(new ItemStack(ModItems.LINKING_BOOK.get()));
LinkTarget destination = new LinkTarget(GlobalPos.of(level.dimension(), centerTarget(helper)), 0, 0);
ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get());
book.set(ModDataComponents.LINK_TARGET.get(), destination);
BookReceptacleBlockEntity holder = (BookReceptacleBlockEntity) level.getBlockEntity(holderPos);
holder.setBook(book);
holder.setDestination(destination);
helper.assertTrue(CrystalPortalNetwork.activate(level, holderPos, planLoaded(level, holderPos)),
"The horizontal crystal opening must activate");
helper.assertValueEqual(level.getBlockState(helper.absolutePos(new BlockPos(1, 1, 1)))
.getValue(CrystalPortalBlock.AXIS), Direction.Axis.Y,
"The fallback surface must face up and down");
helper.assertValueEqual(CrystalPortalSurfaces.group(level, planLoaded(level, holderPos)).getFirst().normal(),
Direction.UP, "The horizontal opening must provide an upward-facing destination view");
var player = helper.makeMockServerPlayerInLevel();
helper.assertValueEqual(LinkingService.travelPortal(player, destination, LinkProperties.NONE),
LinkingService.LinkResult.INTRA_LINKING_REQUIRED,
"A normal book may show a portal but must not link within its own dimension");
helper.assertTrue(level.getBlockState(helper.absolutePos(new BlockPos(1, 1, 1)))
.is(ModBlocks.CRYSTAL_PORTAL.get()) && !holder.getBook().isEmpty(),
"Rejected intra-link travel must leave the visual portal and its book intact");
helper.succeed();
}
@GameTest(template = "empty")
public static void undersideReceptaclePlacesAndActivatesHorizontalFrame(GameTestHelper helper) {
var level = helper.getLevel();
for (int x = 0; x < 3; x++) for (int z = 0; z < 3; z++) {
level.setBlock(helper.absolutePos(new BlockPos(x, 2, z)),
x == 1 && z == 1 ? Blocks.AIR.defaultBlockState()
: ModBlocks.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
}
BlockPos support = helper.absolutePos(new BlockPos(0, 2, 1));
BlockPos holderPos = support.below();
BlockPlaceContext context = new BlockPlaceContext(level, null, InteractionHand.MAIN_HAND,
new ItemStack(ModItems.BOOK_RECEPTACLE.get()),
new BlockHitResult(Vec3.atCenterOf(support).add(0, -0.5, 0),
Direction.DOWN, support, false));
var placed = ModBlocks.BOOK_RECEPTACLE.get().getStateForPlacement(context);
helper.assertTrue(placed != null && placed.getValue(BookReceptacleBlock.FACING) == Direction.DOWN,
"Clicking the underside of a Crystal Block must place a downward-facing receptacle");
level.setBlock(holderPos, placed, 3);
((BookReceptacleBlockEntity) level.getBlockEntity(holderPos))
.setBook(new ItemStack(ModItems.LINKING_BOOK.get()));
helper.assertTrue(CrystalPortalNetwork.activate(level, holderPos, planLoaded(level, holderPos)),
"A ceiling-mounted receptacle must activate its horizontal crystal frame");
helper.assertValueEqual(level.getBlockState(helper.absolutePos(new BlockPos(1, 2, 1)))
.getValue(CrystalPortalBlock.AXIS), Direction.Axis.Y,
"The underside-mounted portal surface remains horizontal");
helper.succeed();
}
@@ -0,0 +1,59 @@
package net.dnorberg.rivencraft.gametest;
import java.util.ArrayList;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.PageContent;
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.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
@GameTestHolder(Rivencraft.MOD_ID)
@PrefixGameTestTemplate(false)
public final class SymbolPortfolioGameTests {
private SymbolPortfolioGameTests() {}
@GameTest(template = "empty")
public static void portfoliosCoverEverySymbolCategoryWithoutTruncation(GameTestHelper helper) {
var registries = helper.getLevel().registryAccess();
List<ItemStack> creative = ModCreativeTabs.creativeSymbolPortfolios(registries);
helper.assertTrue(!creative.isEmpty(), "Creative tab must include prefilled portfolios");
for (ResourceLocation category : PageItem.bundledCreativeCategories()) {
List<ItemStack> expected = PageItem.creativeSymbolPages(registries, category);
List<ItemStack> volumes = SymbolPortfolioItem.createVolumes(category, expected);
helper.assertTrue(!volumes.isEmpty(), "Missing portfolio for " + category);
List<ItemStack> actual = new ArrayList<>();
for (ItemStack volume : volumes) {
helper.assertTrue(volume.is(ModItems.SYMBOL_PORTFOLIO.get())
&& CollationFolderItem.isCollection(volume)
&& creative.stream().anyMatch(candidate ->
candidate.getHoverName().getString().equals(volume.getHoverName().getString())),
"Portfolio must be an editable creative collection for " + category);
NotebookContents contents = volume.get(ModDataComponents.NOTEBOOK_CONTENTS.get());
helper.assertTrue(contents != null && contents.pageCount() > 0
&& contents.pageCount() <= NotebookContents.MAX_PAGES,
"Each portfolio volume must contain valid pages");
helper.assertTrue(contents.pages().stream().allMatch(page ->
PageItem.content(page).kind() == PageContent.Kind.SYMBOL),
"Portfolios must contain symbol pages, not Link Panels");
actual.addAll(contents.pages());
}
helper.assertTrue(actual.size() == expected.size(), "Portfolio omitted pages in " + category);
for (int index = 0; index < expected.size(); index++) {
helper.assertTrue(ItemStack.isSameItemSameComponents(actual.get(index), expected.get(index)),
"Portfolio pages must match the category tab in " + category);
}
}
helper.succeed();
}
}
@@ -13,11 +13,13 @@ public final class CollationFolderItem extends NotebookItem {
}
public static boolean isCollection(ItemStack stack) {
return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get());
return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get())
|| stack.is(ModItems.SYMBOL_PORTFOLIO.get());
}
public static int capacity(ItemStack stack) {
return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES
: stack.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES : 0;
: stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.SYMBOL_PORTFOLIO.get())
? NotebookContents.MAX_PAGES : 0;
}
}
@@ -39,8 +39,7 @@ public class NotebookItem extends Item {
NotebookContents contents = stack.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
tooltip.add(Component.translatable("item.rivencraft.notebook.pages",
contents.pageCount(), stack.is(net.dnorberg.rivencraft.registry.ModItems.COLLATION_FOLDER.get())
? CollationFolderItem.MAX_PAGES : NotebookContents.MAX_PAGES)
contents.pageCount(), CollationFolderItem.capacity(stack))
.withStyle(ChatFormatting.GRAY));
if (contents.unresolvedCount() > 0) {
tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved",
@@ -0,0 +1,40 @@
package net.dnorberg.rivencraft.item;
import java.util.ArrayList;
import java.util.List;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/** Non-craftable, prefilled Legacy-style collections of one symbol-page category. */
public final class SymbolPortfolioItem extends NotebookItem {
public SymbolPortfolioItem(Properties properties) {
super(properties);
}
public static List<ItemStack> createVolumes(ResourceLocation category, List<ItemStack> pages) {
if (pages.isEmpty()) return List.of();
int capacity = NotebookContents.MAX_PAGES;
int volumeCount = (pages.size() + capacity - 1) / capacity;
List<ItemStack> volumes = new ArrayList<>(volumeCount);
for (int volume = 0; volume < volumeCount; volume++) {
ItemStack portfolio = new ItemStack(ModItems.SYMBOL_PORTFOLIO.get());
int first = volume * capacity;
int last = Math.min(first + capacity, pages.size());
portfolio.set(ModDataComponents.NOTEBOOK_CONTENTS.get(),
new NotebookContents(pages.subList(first, last)));
Component categoryName = Component.translatableWithFallback(
"itemGroup.rivencraft.pages." + category.getPath(), category.getPath());
portfolio.set(DataComponents.CUSTOM_NAME, volumeCount == 1
? Component.translatable("item.rivencraft.symbol_portfolio.named", categoryName)
: Component.translatable("item.rivencraft.symbol_portfolio.volume",
categoryName, volume + 1, volumeCount));
volumes.add(portfolio);
}
return List.copyOf(volumes);
}
}
@@ -7,6 +7,9 @@ import java.util.List;
import java.util.Map;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
import net.minecraft.core.HolderLookup;
import net.minecraft.world.item.ItemStack;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
@@ -28,6 +31,7 @@ public final class ModCreativeTabs {
output.accept(ModItems.LINKING_BOOK.get());
output.accept(ModItems.NOTEBOOK.get());
output.accept(ModItems.COLLATION_FOLDER.get());
creativeSymbolPortfolios(parameters.holders()).forEach(output::accept);
output.accept(ModItems.SEALED_NOTEBOOK.get());
output.accept(ModItems.INK_VIAL.get());
output.accept(ModItems.WRITING_DESK.get());
@@ -59,14 +63,28 @@ public final class ModCreativeTabs {
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
registerSymbolPageTabs();
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() {
/** Prefilled portfolios mirror the symbol-page tabs, including data-pack and modded pages. */
public static List<ItemStack> creativeSymbolPortfolios(HolderLookup.Provider registries) {
List<ItemStack> portfolios = new ArrayList<>();
for (ResourceLocation category : orderedSymbolCategories()) {
portfolios.addAll(SymbolPortfolioItem.createVolumes(category,
PageItem.creativeSymbolPages(registries, category)));
}
return List.copyOf(portfolios);
}
private static List<ResourceLocation> orderedSymbolCategories() {
List<ResourceLocation> categories = new ArrayList<>(PageItem.bundledCreativeCategories());
categories.sort(Comparator.comparingInt((ResourceLocation id) -> {
int index = CATEGORY_ORDER.indexOf(id.getPath());
return index < 0 ? CATEGORY_ORDER.size() : index;
}).thenComparing(ResourceLocation::toString));
return List.copyOf(categories);
}
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() {
Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> tabs = new LinkedHashMap<>();
for (ResourceLocation category : categories) {
for (ResourceLocation category : orderedSymbolCategories()) {
String key = "rivencraft_pages_" + category.getNamespace() + "_" + category.getPath().replace('/', '_');
tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder()
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + category.getPath(),
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.NotebookItem;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
import net.dnorberg.rivencraft.item.SealedNotebookItem;
import net.dnorberg.rivencraft.item.InkVialItem;
import net.minecraft.world.item.Item;
@@ -39,6 +40,8 @@ public final class ModItems {
);
public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem(
"collation_folder", CollationFolderItem::new, new Item.Properties().stacksTo(1));
public static final DeferredItem<SymbolPortfolioItem> SYMBOL_PORTFOLIO = ITEMS.registerItem(
"symbol_portfolio", SymbolPortfolioItem::new, new Item.Properties().stacksTo(1));
public static final DeferredItem<SealedNotebookItem> SEALED_NOTEBOOK = ITEMS.registerItem(
"sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16));
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
@@ -1 +1 @@
{"variants":{"facing=north":{"model":"rivencraft:block/book_receptacle","y":180},"facing=east":{"model":"rivencraft:block/book_receptacle","y":270},"facing=south":{"model":"rivencraft:block/book_receptacle"},"facing=west":{"model":"rivencraft:block/book_receptacle","y":90},"facing=up":{"model":"rivencraft:block/book_receptacle","x":270},"facing=down":{"model":"rivencraft:block/book_receptacle","x":90}}}
{"variants":{"facing=north":{"model":"rivencraft:block/book_receptacle","y":180},"facing=east":{"model":"rivencraft:block/book_receptacle","y":270},"facing=south":{"model":"rivencraft:block/book_receptacle"},"facing=west":{"model":"rivencraft:block/book_receptacle","y":90},"facing=up":{"model":"rivencraft:block/book_receptacle","x":90},"facing=down":{"model":"rivencraft:block/book_receptacle","x":270,"y":180}}}
@@ -22,6 +22,9 @@
"item.rivencraft.page.unresolved": "Unresolved Symbol Page",
"item.rivencraft.notebook": "Notebook",
"item.rivencraft.collation_folder": "Collation Folder",
"item.rivencraft.symbol_portfolio": "Symbol Portfolio",
"item.rivencraft.symbol_portfolio.named": "%s Portfolio",
"item.rivencraft.symbol_portfolio.volume": "%s Portfolio %s/%s",
"item.rivencraft.sealed_notebook": "Sealed Notebook",
"block.rivencraft.link_modifier": "Link Modifier",
"container.rivencraft.link_modifier": "Link Modifier",
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/symbol_portfolio" }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B