Compare commits

...
1 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
32 changed files with 126 additions and 5 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.3
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);
}
@@ -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();
}
@@ -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}}}