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 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 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. 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 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 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. # 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 mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. # 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 immersive_portals_version=6.0.7
terrablender_version=4.1.0.8 terrablender_version=4.1.0.8
serene_seasons_version_id=pHEgQQUE serene_seasons_version_id=pHEgQQUE
@@ -77,7 +77,7 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
} }
@Override public BlockState getStateForPlacement(BlockPlaceContext context) { @Override public BlockState getStateForPlacement(BlockPlaceContext context) {
Direction face = context.getClickedFace(); 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; context.getClickedPos().relative(face.getOpposite())).is(ModBlocks.CRYSTAL_BLOCK.get())) return null;
return defaultBlockState().setValue(FACING, face); 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) { public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(ModEntities.DROPPED_BOOK.get(), DroppedBookRenderer::new); event.registerEntityRenderer(ModEntities.DROPPED_BOOK.get(), DroppedBookRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::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.WRITING_DESK.get(), WritingDeskRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.STAR_FISSURE.get(), StarFissureRenderer::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.item.PageItem;
import net.dnorberg.rivencraft.book.LinkProperties; import net.dnorberg.rivencraft.book.LinkProperties;
import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
import net.dnorberg.rivencraft.writing.InkModifierIngredients; import net.dnorberg.rivencraft.writing.InkModifierIngredients;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolReference; import net.dnorberg.rivencraft.writing.SymbolReference;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@@ -63,6 +65,11 @@ public final class RivencraftJeiPlugin implements IModPlugin {
case UNRESOLVED_SYMBOL -> "unresolved:" + content.symbol().orElseThrow().id(); 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 @Override
@@ -72,6 +79,7 @@ public final class RivencraftJeiPlugin implements IModPlugin {
var connection = Minecraft.getInstance().getConnection(); var connection = Minecraft.getInstance().getConnection();
if (connection != null) { if (connection != null) {
pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess())); pages.addAll(PageItem.creativeSymbolPages(connection.registryAccess()));
pages.addAll(ModCreativeTabs.creativeSymbolPortfolios(connection.registryAccess()));
} else { } else {
PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage) PageItem.bundledCreativeSymbols().stream().map(PageItem::createBundledCreativePage)
.forEach(pages::add); .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. // JEI discovers the base Page item as a blank stack; only authored pages belong in its list.
runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK, runtime.getIngredientVisibility().hideIngredients(VanillaTypes.ITEM_STACK,
List.of(new ItemStack(ModItems.PAGE.get())), List.of(UidContext.Ingredient)); 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(); var connection = Minecraft.getInstance().getConnection();
if (connection == null) return; if (connection == null) return;
Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream() Set<ResourceLocation> known = runtime.getIngredientManager().getAllItemStacks().stream()
@@ -98,6 +108,19 @@ public final class RivencraftJeiPlugin implements IModPlugin {
if (!missing.isEmpty()) { if (!missing.isEmpty()) {
runtime.getIngredientManager().addIngredientsAtRuntime(VanillaTypes.ITEM_STACK, missing); 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 @Override
@@ -6,6 +6,9 @@ import net.dnorberg.rivencraft.block.CrystalPortalBlock;
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity; import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity; import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
import net.dnorberg.rivencraft.book.LinkTarget; 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.CrystalPortalNetwork;
import net.dnorberg.rivencraft.portal.CrystalPortalSurfaces; import net.dnorberg.rivencraft.portal.CrystalPortalSurfaces;
import net.dnorberg.rivencraft.portal.PortalTravelCooldown; import net.dnorberg.rivencraft.portal.PortalTravelCooldown;
@@ -19,7 +22,11 @@ import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack; 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.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.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
@@ -154,13 +161,54 @@ public final class CrystalPortalGameTests {
BlockPos holderPos = helper.absolutePos(new BlockPos(0, 2, 1)); BlockPos holderPos = helper.absolutePos(new BlockPos(0, 2, 1));
level.setBlock(holderPos, ModBlocks.BOOK_RECEPTACLE.get().defaultBlockState() level.setBlock(holderPos, ModBlocks.BOOK_RECEPTACLE.get().defaultBlockState()
.setValue(BookReceptacleBlock.FACING, Direction.UP), 3); .setValue(BookReceptacleBlock.FACING, Direction.UP), 3);
((BookReceptacleBlockEntity) level.getBlockEntity(holderPos)) LinkTarget destination = new LinkTarget(GlobalPos.of(level.dimension(), centerTarget(helper)), 0, 0);
.setBook(new ItemStack(ModItems.LINKING_BOOK.get())); 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)), helper.assertTrue(CrystalPortalNetwork.activate(level, holderPos, planLoaded(level, holderPos)),
"The horizontal crystal opening must activate"); "The horizontal crystal opening must activate");
helper.assertValueEqual(level.getBlockState(helper.absolutePos(new BlockPos(1, 1, 1))) helper.assertValueEqual(level.getBlockState(helper.absolutePos(new BlockPos(1, 1, 1)))
.getValue(CrystalPortalBlock.AXIS), Direction.Axis.Y, .getValue(CrystalPortalBlock.AXIS), Direction.Axis.Y,
"The fallback surface must face up and down"); "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(); 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) { 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) { public static int capacity(ItemStack stack) {
return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES 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( NotebookContents contents = stack.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
tooltip.add(Component.translatable("item.rivencraft.notebook.pages", tooltip.add(Component.translatable("item.rivencraft.notebook.pages",
contents.pageCount(), stack.is(net.dnorberg.rivencraft.registry.ModItems.COLLATION_FOLDER.get()) contents.pageCount(), CollationFolderItem.capacity(stack))
? CollationFolderItem.MAX_PAGES : NotebookContents.MAX_PAGES)
.withStyle(ChatFormatting.GRAY)); .withStyle(ChatFormatting.GRAY));
if (contents.unresolvedCount() > 0) { if (contents.unresolvedCount() > 0) {
tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved", 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 java.util.Map;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem; 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.core.registries.Registries;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@@ -28,6 +31,7 @@ public final class ModCreativeTabs {
output.accept(ModItems.LINKING_BOOK.get()); output.accept(ModItems.LINKING_BOOK.get());
output.accept(ModItems.NOTEBOOK.get()); output.accept(ModItems.NOTEBOOK.get());
output.accept(ModItems.COLLATION_FOLDER.get()); output.accept(ModItems.COLLATION_FOLDER.get());
creativeSymbolPortfolios(parameters.holders()).forEach(output::accept);
output.accept(ModItems.SEALED_NOTEBOOK.get()); output.accept(ModItems.SEALED_NOTEBOOK.get());
output.accept(ModItems.INK_VIAL.get()); output.accept(ModItems.INK_VIAL.get());
output.accept(ModItems.WRITING_DESK.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 = public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
registerSymbolPageTabs(); 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()); List<ResourceLocation> categories = new ArrayList<>(PageItem.bundledCreativeCategories());
categories.sort(Comparator.comparingInt((ResourceLocation id) -> { categories.sort(Comparator.comparingInt((ResourceLocation id) -> {
int index = CATEGORY_ORDER.indexOf(id.getPath()); int index = CATEGORY_ORDER.indexOf(id.getPath());
return index < 0 ? CATEGORY_ORDER.size() : index; return index < 0 ? CATEGORY_ORDER.size() : index;
}).thenComparing(ResourceLocation::toString)); }).thenComparing(ResourceLocation::toString));
return List.copyOf(categories);
}
private static Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> registerSymbolPageTabs() {
Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> tabs = new LinkedHashMap<>(); 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('/', '_'); String key = "rivencraft_pages_" + category.getNamespace() + "_" + category.getPath().replace('/', '_');
tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder() tabs.put(category, CREATIVE_TABS.register(key, () -> CreativeModeTab.builder()
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + category.getPath(), .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.PageItem;
import net.dnorberg.rivencraft.item.NotebookItem; import net.dnorberg.rivencraft.item.NotebookItem;
import net.dnorberg.rivencraft.item.CollationFolderItem; import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
import net.dnorberg.rivencraft.item.SealedNotebookItem; import net.dnorberg.rivencraft.item.SealedNotebookItem;
import net.dnorberg.rivencraft.item.InkVialItem; import net.dnorberg.rivencraft.item.InkVialItem;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
@@ -39,6 +40,8 @@ public final class ModItems {
); );
public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem( public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem(
"collation_folder", CollationFolderItem::new, new Item.Properties().stacksTo(1)); "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( public static final DeferredItem<SealedNotebookItem> SEALED_NOTEBOOK = ITEMS.registerItem(
"sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16)); "sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16));
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem( 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.page.unresolved": "Unresolved Symbol Page",
"item.rivencraft.notebook": "Notebook", "item.rivencraft.notebook": "Notebook",
"item.rivencraft.collation_folder": "Collation Folder", "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", "item.rivencraft.sealed_notebook": "Sealed Notebook",
"block.rivencraft.link_modifier": "Link Modifier", "block.rivencraft.link_modifier": "Link Modifier",
"container.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