Support horizontal portals and visible receptacle books (#93)
Build / build (push) Canceled after 0s
No files matched your search
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 497 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 73 KiB |
@@ -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.3
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}}}
|
||||||