From 1dcd3a178efceffdce71382216513641e8ab248a Mon Sep 17 00:00:00 2001 From: Dustin Norberg Date: Thu, 24 Sep 2026 05:38:51 -0400 Subject: [PATCH] Add Writing Desk paper copying and ink meter --- README.md | 12 +- docs/WRITING_DATA.md | 5 +- gradle.properties | 2 +- .../block/entity/WritingDeskBlockEntity.java | 55 +++++++- .../client/screen/WritingDeskScreen.java | 17 +++ .../gametest/WritingDeskGameTests.java | 130 +++++++++++++++++- .../rivencraft/menu/WritingDeskMenu.java | 114 +++++++++++++-- .../assets/rivencraft/lang/en_us.json | 1 + 8 files changed, 309 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4a40527..75df96d 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,18 @@ hand and use the Ink Vial in the other to prepare a baseline Link Panel Page. Each vial holds 16 ink units, and the default link-panel cost is two units. The spent vial returns a glass bottle. Server owners can adjust the symbol-copy and link-panel costs in the `writing` section of `rivencraft-server.toml`. -Symbol-page transcription will become available through the Writing Desk. +The Writing Desk can copy a selected written page from its collection tabs. The Writing Desk is now craftable from planks, a glass bottle, and a feather. -Its four collection tabs, ink/source/page work slots, and inventory persist +Its four collection tabs, paper input, ink vial input, empty glass-bottle return, +16-unit ink meter, and inventory persist through world saves; breaking it drops stored contents. The desk screen is a functional foundation styled after Legacy's left tabs, central writing surface, -and right inventory. Transcription controls are not implemented yet. +and right inventory. Put vanilla paper (or a blank Rivencraft Page) in the +upper-left slot and Ink Vials in the upper-right; a copy appears in the +lower-left result slot when the selected page is valid. Taking it consumes one +paper and, by default, one ink. An empty meter consumes a vial to refill and +returns a vanilla glass bottle to the lower-right slot. Writing new symbols +from a catalog remains future work. Selecting a stored page on the desk surface shows its Legacy-proportioned portrait preview and name above the player inventory. diff --git a/docs/WRITING_DATA.md b/docs/WRITING_DATA.md index fc6a25a..cf6f33f 100644 --- a/docs/WRITING_DATA.md +++ b/docs/WRITING_DATA.md @@ -86,5 +86,8 @@ panel, hold a blank Page in one hand and use an Ink Vial in the other. The server checks the held items, blank state, and sufficient ink before consuming anything. A stack of blank Pages yields exactly one Link Panel Page per use; if the inventory is full, the result is dropped instead of deleted. Symbol -copying will be implemented by the Writing Desk. Link-property modifiers are +copying is implemented by the Writing Desk: it previews a copy of the selected +resolved page, then spends paper and ink only when that copy is taken. The +desk's 16-unit ink meter refills from an Ink Vial when needed and returns a +vanilla glass bottle. Link-property modifiers are deferred beyond this baseline flow. diff --git a/gradle.properties b/gradle.properties index b131bdf..42041d1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,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.3.5.1 +mod_version=0.3.5.2 immersive_portals_version=6.0.7 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. diff --git a/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java b/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java index 05307f8..28958ca 100644 --- a/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java +++ b/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java @@ -1,5 +1,8 @@ package net.dnorberg.rivencraft.block.entity; +import net.dnorberg.rivencraft.item.InkVialItem; +import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.writing.WritingInkService; import net.dnorberg.rivencraft.registry.ModBlockEntities; import net.dnorberg.rivencraft.registry.ModItems; import net.minecraft.core.BlockPos; @@ -10,25 +13,29 @@ import net.minecraft.world.Container; import net.minecraft.world.ContainerHelper; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; -/** The four Legacy-like collection tabs and four work slots are owned by the block, not its viewers. */ +/** Four collection tabs, paper, ink vials and empty-vial return are owned by the block. */ public final class WritingDeskBlockEntity extends BlockEntity implements Container { public static final int COLLECTIONS = 4; public static final int INK = 4; - public static final int SOURCE = 5; + public static final int RETURN = 5; public static final int PAPER = 6; - public static final int OUTPUT = 7; + /** Unused by new desks; retained so old saves cannot silently lose slot contents. */ + public static final int LEGACY_OUTPUT = 7; public static final int SIZE = 8; private NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); private int activeCollection; + private int inkRemaining; public WritingDeskBlockEntity(BlockPos pos, BlockState state) { super(ModBlockEntities.WRITING_DESK.get(), pos, state); } public int activeCollection() { return activeCollection; } + public int inkRemaining() { return inkRemaining; } public void setActiveCollection(int index) { if (index >= 0 && index < COLLECTIONS && activeCollection != index) { activeCollection = index; @@ -60,15 +67,49 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain if (slot < COLLECTIONS) return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.PAGE.get()); return switch (slot) { case INK -> stack.is(ModItems.INK_VIAL.get()); - case SOURCE -> stack.is(ModItems.PAGE.get()) || stack.is(ModItems.NOTEBOOK.get()); - case PAPER -> stack.is(ModItems.PAGE.get()); - case OUTPUT -> false; + case RETURN -> false; + case PAPER -> stack.is(Items.PAPER) + || stack.is(ModItems.PAGE.get()) && PageItem.content(stack).kind() + == net.dnorberg.rivencraft.writing.PageContent.Kind.BLANK; + case LEGACY_OUTPUT -> false; default -> false; }; } + + /** The output is virtual; inputs are committed only after a result can be given to a player. */ + public boolean canConsumeCopyMaterials() { + ItemStack paper = items.get(PAPER); + if (!canPlaceItem(PAPER, paper) || paper.isEmpty()) return false; + int cost = WritingInkService.symbolCopyCost(); + if (inkRemaining >= cost) return true; + ItemStack vial = items.get(INK); + ItemStack returned = items.get(RETURN); + return vial.is(ModItems.INK_VIAL.get()) && inkRemaining + InkVialItem.remaining(vial) >= cost + && (returned.isEmpty() || returned.is(Items.GLASS_BOTTLE) + && returned.getCount() < returned.getMaxStackSize()); + } + + public boolean consumeCopyMaterials() { + if (!canConsumeCopyMaterials()) return false; + int cost = WritingInkService.symbolCopyCost(); + if (inkRemaining < cost) { + ItemStack vial = items.get(INK); + inkRemaining += Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY); + vial.shrink(1); + if (vial.isEmpty()) items.set(INK, ItemStack.EMPTY); + ItemStack returned = items.get(RETURN); + if (returned.isEmpty()) items.set(RETURN, new ItemStack(Items.GLASS_BOTTLE)); + else returned.grow(1); + } + inkRemaining -= cost; + removeItem(PAPER, 1); + setChanged(); + return true; + } @Override public void clearContent() { items.clear(); items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + inkRemaining = 0; setChanged(); } @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { @@ -76,10 +117,12 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain items = NonNullList.withSize(SIZE, ItemStack.EMPTY); ContainerHelper.loadAllItems(tag, items, registries); activeCollection = Math.clamp(tag.getInt("ActiveCollection"), 0, COLLECTIONS - 1); + inkRemaining = Math.clamp(tag.getInt("InkRemaining"), 0, InkVialItem.CAPACITY); } @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.saveAdditional(tag, registries); ContainerHelper.saveAllItems(tag, items, registries); tag.putInt("ActiveCollection", activeCollection); + tag.putInt("InkRemaining", inkRemaining); } } diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java index 7fe538e..4a7a05b 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java @@ -31,6 +31,10 @@ public final class WritingDeskScreen extends AbstractContainerScreen 0) graphics.fill(x + 352, y + 73 - filled, x + 362, y + 73, 0xFF6666AA); + graphics.renderOutline(x + 350, y + 20, 14, 55, 0xFF484848); for (int row = 0; row < 3; row++) { for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 91 + row * 18); } @@ -68,6 +77,11 @@ public final class WritingDeskScreen extends AbstractContainerScreen= leftPos + 350 && mouseX < leftPos + 364 + && mouseY >= topPos + 20 && mouseY < topPos + 75) { + graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.ink", + menu.inkRemaining(), 16), mouseX, mouseY); + } } private List pages() { @@ -133,6 +147,9 @@ public final class WritingDeskScreen extends AbstractContainerScreen stack.is(ModItems.PAGE.get())) + int total = player.getInventory().items.stream().filter(stack -> stack.is(Items.PAPER)) .mapToInt(ItemStack::getCount).sum(); helper.assertValueEqual(total, 12, "Round-trip must not lose or duplicate pages"); helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3); helper.assertFalse(menu.stillValid(player), "Removed desk must invalidate menu"); helper.succeed(); } + + @GameTest(template = "empty") + public static void takingCopySpendsPaperAndOneInkThenReturnsGlassBottle(GameTestHelper helper) { + BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1)); + helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3); + WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos); + ItemStack source = PageItem.createLinkPanelPage(); + desk.setItem(0, source.copy()); + desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 2)); + desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); + var player = helper.makeMockServerPlayerInLevel(); + player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk); + menu.broadcastChanges(); + helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().is(ModItems.PAGE.get()), + "A source page with paper and ink must preview one copy"); + helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 2, + "Previewing must not consume paper"); + helper.assertValueEqual(desk.inkRemaining(), 0, "Previewing must not consume ink"); + menu.clicked(WritingDeskMenu.COPY_RESULT, 0, ClickType.PICKUP, player); + helper.assertTrue(menu.getCarried().is(ModItems.PAGE.get()), "Taking result must give the copied page"); + helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1, + "Taking a copy must consume one paper"); + helper.assertValueEqual(desk.inkRemaining(), 15, "First copy must refill to 16 and consume one ink"); + helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).isEmpty(), "Refill must consume one vial"); + helper.assertTrue(desk.getItem(WritingDeskBlockEntity.RETURN).is(Items.GLASS_BOTTLE), + "Refill must return a vanilla glass bottle"); + helper.assertTrue(desk.getItem(0).is(ModItems.PAGE.get()), "Source page must remain untouched"); + helper.assertTrue(menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT).is(ModItems.PAGE.get()), + "A second copy must remain available from the same ink fill"); + helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(), + "Two copies must consume exactly two paper"); + helper.assertValueEqual(desk.inkRemaining(), 14, "Two copies must spend exactly two of sixteen ink"); + helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.RETURN).getCount(), 1, + "A second copy must not consume another vial"); + var saved = desk.saveWithoutMetadata(helper.getLevel().registryAccess()); + var restored = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState()); + restored.loadWithComponents(saved, helper.getLevel().registryAccess()); + helper.assertValueEqual(restored.inkRemaining(), 14, "Ink meter must persist across saves"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void shiftClickCopiesOnlyWhenResourcesAndBottleSpaceExist(GameTestHelper helper) { + BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1)); + helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3); + WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos); + desk.setItem(0, PageItem.createLinkPanelPage()); + desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 2)); + desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); + desk.setItem(WritingDeskBlockEntity.RETURN, new ItemStack(Items.GLASS_BOTTLE, 64)); + var player = helper.makeMockServerPlayerInLevel(); + player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk); + helper.assertTrue(menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT).isEmpty(), + "Full bottle return must block refill without consuming supplies"); + helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 2, + "Blocked copy must leave paper untouched"); + desk.removeItem(WritingDeskBlockEntity.RETURN, 1); + ItemStack copied = menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT); + helper.assertTrue(copied.is(ModItems.PAGE.get()), "Shift-click must transfer one copy"); + helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1, + "Shift-click must consume one paper"); + helper.assertValueEqual(desk.inkRemaining(), 15, "Shift-click must consume one ink"); + int copies = player.getInventory().items.stream().filter(stack -> stack.is(ModItems.PAGE.get())) + .mapToInt(ItemStack::getCount).sum(); + helper.assertValueEqual(copies, 1, "Shift-click must give exactly one page"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void selectedNotebookPageControlsCopyPreview(GameTestHelper helper) { + BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1)); + helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3); + WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos); + ItemStack notebook = new ItemStack(ModItems.NOTEBOOK.get()); + notebook.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), + new NotebookContents(List.of(ItemStack.EMPTY, PageItem.createLinkPanelPage()))); + desk.setItem(0, notebook); + desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER)); + desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); + var player = helper.makeMockServerPlayerInLevel(); + player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk); + menu.broadcastChanges(); + helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().isEmpty(), + "An empty selected position must not create a copy"); + helper.assertTrue(menu.clickMenuButton(player, 17), "Second page selection must be accepted"); + menu.broadcastChanges(); + helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().is(ModItems.PAGE.get()), + "Selecting the populated page must show a copy"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void twoViewersCannotTakeTheSameLastSheet(GameTestHelper helper) { + BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1)); + helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3); + WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos); + desk.setItem(0, PageItem.createLinkPanelPage()); + desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER)); + desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); + var first = helper.makeMockServerPlayerInLevel(); + var second = helper.makeMockServerPlayerInLevel(); + first.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + second.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + WritingDeskMenu firstMenu = new WritingDeskMenu(1, first.getInventory(), desk); + WritingDeskMenu secondMenu = new WritingDeskMenu(2, second.getInventory(), desk); + firstMenu.broadcastChanges(); + secondMenu.broadcastChanges(); + helper.assertTrue(firstMenu.quickMoveStack(first, WritingDeskMenu.COPY_RESULT).is(ModItems.PAGE.get()), + "First viewer should take the last available copy"); + helper.assertTrue(secondMenu.quickMoveStack(second, WritingDeskMenu.COPY_RESULT).isEmpty(), + "Second viewer must not copy the already-consumed paper"); + helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(), + "Shared desk must spend paper only once"); + helper.succeed(); + } } diff --git a/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java b/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java index 5e8f5cd..fab1c42 100644 --- a/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java +++ b/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java @@ -1,8 +1,13 @@ package net.dnorberg.rivencraft.menu; import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; +import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModMenus; +import net.dnorberg.rivencraft.writing.NotebookContents; +import net.dnorberg.rivencraft.writing.PageContent; +import java.util.List; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.world.Container; import net.minecraft.world.SimpleContainer; @@ -12,12 +17,18 @@ import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.DataSlot; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; public final class WritingDeskMenu extends AbstractContainerMenu { - private static final int PLAYER_START = WritingDeskBlockEntity.SIZE; + public static final int COPY_RESULT = WritingDeskBlockEntity.SIZE; + private static final int PLAYER_START = COPY_RESULT + 1; private final Container desk; private final WritingDeskBlockEntity blockEntity; + private final SimpleContainer result = new SimpleContainer(1); + private final boolean clientSide; private int syncedActiveCollection; + private int syncedInkRemaining; + private int selectedPage; public WritingDeskMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) { this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()) @@ -28,18 +39,25 @@ public final class WritingDeskMenu extends AbstractContainerMenu { super(ModMenus.WRITING_DESK.get(), id); this.blockEntity = blockEntity; this.desk = blockEntity == null ? new SimpleContainer(WritingDeskBlockEntity.SIZE) : blockEntity; + this.clientSide = inventory.player.level().isClientSide; addDataSlot(new DataSlot() { @Override public int get() { return WritingDeskMenu.this.blockEntity == null ? syncedActiveCollection : WritingDeskMenu.this.blockEntity.activeCollection(); } @Override public void set(int value) { syncedActiveCollection = value; } }); + addDataSlot(new DataSlot() { + @Override public int get() { return WritingDeskMenu.this.blockEntity == null + ? syncedInkRemaining : WritingDeskMenu.this.blockEntity.inkRemaining(); } + @Override public void set(int value) { syncedInkRemaining = value; } + }); for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++) { addSlot(new DeskSlot(desk, i, 12, 20 + i * 37, 1)); } - addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 370, 20, 1)); - addSlot(new DeskSlot(desk, WritingDeskBlockEntity.SOURCE, 228, 20, 1)); - addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 228, 57, 64)); - addSlot(new DeskSlot(desk, WritingDeskBlockEntity.OUTPUT, 370, 57, 64)); + addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 370, 20, 16)); + addSlot(new DeskSlot(desk, WritingDeskBlockEntity.RETURN, 370, 57, 64)); + addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 228, 20, 64)); + addSlot(new DeskSlot(desk, WritingDeskBlockEntity.LEGACY_OUTPUT, -1000, -1000, 64)); + addSlot(new CopySlot(result, 0, 228, 57)); for (int row = 0; row < 3; row++) { for (int column = 0; column < 9; column++) { addSlot(new Slot(inventory, column + row * 9 + 9, 224 + column * 18, 91 + row * 18)); @@ -51,10 +69,17 @@ public final class WritingDeskMenu extends AbstractContainerMenu { } public int activeCollection() { return syncedActiveCollection; } + public int inkRemaining() { return syncedInkRemaining; } @Override public boolean clickMenuButton(Player player, int id) { - if (blockEntity == null || !stillValid(player) || id < 0 || id >= WritingDeskBlockEntity.COLLECTIONS) return false; - blockEntity.setActiveCollection(id); + if (blockEntity == null || !stillValid(player)) return false; + if (id >= 0 && id < WritingDeskBlockEntity.COLLECTIONS) { + blockEntity.setActiveCollection(id); + selectedPage = 0; + } else if (id >= 16 && id < 16 + NotebookContents.MAX_PAGES) { + selectedPage = id - 16; + } else return false; + refreshOutput(); return true; } @@ -64,11 +89,23 @@ public final class WritingDeskMenu extends AbstractContainerMenu { @Override public ItemStack quickMoveStack(Player player, int index) { if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY; + if (index == COPY_RESULT) { + refreshOutput(); + if (!canCopy()) return ItemStack.EMPTY; + ItemStack copy = selectedSource().copyWithCount(1); + ItemStack movingCopy = copy.copy(); + if (!moveItemStackTo(movingCopy, PLAYER_START, slots.size(), true) || !movingCopy.isEmpty()) { + return ItemStack.EMPTY; + } + blockEntity.consumeCopyMaterials(); + refreshOutput(); + return copy; + } Slot slot = slots.get(index); if (!slot.hasItem()) return ItemStack.EMPTY; ItemStack moving = slot.getItem(); ItemStack original = moving.copy(); - if (index < PLAYER_START) { + if (index < COPY_RESULT) { if (!moveItemStackTo(moving, PLAYER_START, slots.size(), true)) return ItemStack.EMPTY; } else if (!moveIntoDesk(moving)) { return ItemStack.EMPTY; @@ -81,14 +118,65 @@ public final class WritingDeskMenu extends AbstractContainerMenu { private boolean moveIntoDesk(ItemStack moving) { if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving, 4, 5, false); if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, 0, 4, false); - if (moving.is(ModItems.PAGE.get())) { - return moveItemStackTo(moving, 6, 7, false) - || moveItemStackTo(moving, 0, 4, false) - || moveItemStackTo(moving, 5, 6, false); - } + if (moving.is(Items.PAPER) || moving.is(ModItems.PAGE.get()) + && PageItem.content(moving).kind() == PageContent.Kind.BLANK) + return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER, + WritingDeskBlockEntity.PAPER + 1, false); + if (moving.is(ModItems.PAGE.get())) return moveItemStackTo(moving, 0, 4, false); return false; } + private ItemStack selectedSource() { + ItemStack collection = desk.getItem(blockEntity == null ? syncedActiveCollection : blockEntity.activeCollection()); + if (collection.is(ModItems.PAGE.get())) return selectedPage == 0 ? collection : ItemStack.EMPTY; + if (collection.is(ModItems.NOTEBOOK.get())) { + List pages = collection.getOrDefault( + ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); + return selectedPage < pages.size() ? pages.get(selectedPage) : ItemStack.EMPTY; + } + return ItemStack.EMPTY; + } + + private boolean canCopy() { + ItemStack source = selectedSource(); + return blockEntity != null && source.is(ModItems.PAGE.get()) + && PageItem.content(source).kind() != PageContent.Kind.BLANK + && PageItem.isUsableWriting(source) && blockEntity.canConsumeCopyMaterials(); + } + + private void refreshOutput() { + if (clientSide) return; + ItemStack preview = canCopy() ? selectedSource().copyWithCount(1) : ItemStack.EMPTY; + if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview); + } + + @Override public void broadcastChanges() { + refreshOutput(); + super.broadcastChanges(); + } + + @Override public void clicked(int slotId, int button, net.minecraft.world.inventory.ClickType clickType, + Player player) { + if (!stillValid(player)) { + player.closeContainer(); + return; + } + refreshOutput(); + super.clicked(slotId, button, clickType, player); + } + + private final class CopySlot extends Slot { + private CopySlot(Container container, int index, int x, int y) { super(container, index, x, y); } + @Override public boolean mayPlace(ItemStack stack) { return false; } + @Override public boolean mayPickup(Player player) { + return clientSide ? hasItem() : WritingDeskMenu.this.stillValid(player) && canCopy(); + } + @Override public void onTake(Player player, ItemStack stack) { + if (!clientSide && blockEntity.consumeCopyMaterials()) refreshOutput(); + super.onTake(player, stack); + } + } + private static final class DeskSlot extends Slot { private final int limit; private DeskSlot(Container container, int index, int x, int y, int limit) { diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index f66a32e..ccaae3f 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -18,6 +18,7 @@ "item.rivencraft.ink_vial": "Ink Vial", "block.rivencraft.writing_desk": "Writing Desk", "container.rivencraft.writing_desk": "Writing Desk", + "screen.rivencraft.writing_desk.ink": "Ink: %s/%s", "item.rivencraft.ink_vial.remaining": "Ink: %s/%s", "item.rivencraft.ink_vial.usage": "Use with a blank Page in the other hand to prepare a Link Panel.", "screen.rivencraft.notebook.search": "Search pages",