diff --git a/README.md b/README.md index e1c282e..b3d8979 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dev images/1-4.png b/dev images/1-4.png deleted file mode 100644 index 767346a..0000000 Binary files a/dev images/1-4.png and /dev/null differ diff --git a/dev images/13-16.png b/dev images/13-16.png deleted file mode 100644 index f0ca568..0000000 Binary files a/dev images/13-16.png and /dev/null differ diff --git a/dev images/17-20.png b/dev images/17-20.png deleted file mode 100644 index 760ceb6..0000000 Binary files a/dev images/17-20.png and /dev/null differ diff --git a/dev images/21-24.png b/dev images/21-24.png deleted file mode 100644 index 930add8..0000000 Binary files a/dev images/21-24.png and /dev/null differ diff --git a/dev images/25-28.png b/dev images/25-28.png deleted file mode 100644 index 9bf6ac7..0000000 Binary files a/dev images/25-28.png and /dev/null differ diff --git a/dev images/29-32.png b/dev images/29-32.png deleted file mode 100644 index fd30b6a..0000000 Binary files a/dev images/29-32.png and /dev/null differ diff --git a/dev images/33-36.png b/dev images/33-36.png deleted file mode 100644 index 3d4694f..0000000 Binary files a/dev images/33-36.png and /dev/null differ diff --git a/dev images/37-40.png b/dev images/37-40.png deleted file mode 100644 index 6672065..0000000 Binary files a/dev images/37-40.png and /dev/null differ diff --git a/dev images/41-44.png b/dev images/41-44.png deleted file mode 100644 index 8494106..0000000 Binary files a/dev images/41-44.png and /dev/null differ diff --git a/dev images/45-48.png b/dev images/45-48.png deleted file mode 100644 index 019a4ee..0000000 Binary files a/dev images/45-48.png and /dev/null differ diff --git a/dev images/49-52.png b/dev images/49-52.png deleted file mode 100644 index dbc16a0..0000000 Binary files a/dev images/49-52.png and /dev/null differ diff --git a/dev images/5-8.png b/dev images/5-8.png deleted file mode 100644 index bfb95b8..0000000 Binary files a/dev images/5-8.png and /dev/null differ diff --git a/dev images/53-56.png b/dev images/53-56.png deleted file mode 100644 index fa25824..0000000 Binary files a/dev images/53-56.png and /dev/null differ diff --git a/dev images/57-60.png b/dev images/57-60.png deleted file mode 100644 index 54d170d..0000000 Binary files a/dev images/57-60.png and /dev/null differ diff --git a/dev images/61-64.png b/dev images/61-64.png deleted file mode 100644 index c1725a3..0000000 Binary files a/dev images/61-64.png and /dev/null differ diff --git a/dev images/65-68.png b/dev images/65-68.png deleted file mode 100644 index c69f482..0000000 Binary files a/dev images/65-68.png and /dev/null differ diff --git a/dev images/69-72.png b/dev images/69-72.png deleted file mode 100644 index d13df37..0000000 Binary files a/dev images/69-72.png and /dev/null differ diff --git a/dev images/73-76.png b/dev images/73-76.png deleted file mode 100644 index 1a63eeb..0000000 Binary files a/dev images/73-76.png and /dev/null differ diff --git a/dev images/77-80.png b/dev images/77-80.png deleted file mode 100644 index 669127d..0000000 Binary files a/dev images/77-80.png and /dev/null differ diff --git a/dev images/81-84.png b/dev images/81-84.png deleted file mode 100644 index 86668f6..0000000 Binary files a/dev images/81-84.png and /dev/null differ diff --git a/dev images/85-88.png b/dev images/85-88.png deleted file mode 100644 index c1bfa61..0000000 Binary files a/dev images/85-88.png and /dev/null differ diff --git a/dev images/9-12.png b/dev images/9-12.png deleted file mode 100644 index d1818e1..0000000 Binary files a/dev images/9-12.png and /dev/null differ diff --git a/dev images/Horizontal Portal.png b/dev images/Horizontal Portal.png deleted file mode 100644 index c91a92c..0000000 Binary files a/dev images/Horizontal Portal.png and /dev/null differ diff --git a/dev images/Pasted image (2).png b/dev images/Pasted image (2).png new file mode 100644 index 0000000..3bcb9ae Binary files /dev/null and b/dev images/Pasted image (2).png differ diff --git a/dev images/book receptical.png b/dev images/book receptical.png deleted file mode 100644 index 0986319..0000000 Binary files a/dev images/book receptical.png and /dev/null differ diff --git a/gradle.properties b/gradle.properties index 5fea858..94e38b2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java b/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java index 9269fa0..a6ccf71 100644 --- a/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java +++ b/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java @@ -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); } diff --git a/src/main/java/net/dnorberg/rivencraft/client/BookReceptacleRenderer.java b/src/main/java/net/dnorberg/rivencraft/client/BookReceptacleRenderer.java new file mode 100644 index 0000000..ebe98d5 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/client/BookReceptacleRenderer.java @@ -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 { + 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(); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java index 2d5ff31..94d4b99 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java +++ b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java @@ -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); } diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/CrystalPortalGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/CrystalPortalGameTests.java index ba6ae63..73a61eb 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/CrystalPortalGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/CrystalPortalGameTests.java @@ -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(); } diff --git a/src/main/resources/assets/rivencraft/blockstates/book_receptacle.json b/src/main/resources/assets/rivencraft/blockstates/book_receptacle.json index fc561ce..d21bddf 100644 --- a/src/main/resources/assets/rivencraft/blockstates/book_receptacle.json +++ b/src/main/resources/assets/rivencraft/blockstates/book_receptacle.json @@ -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}}}