diff --git a/NOTICE b/NOTICE index 7b6b23f..f52a95c 100644 --- a/NOTICE +++ b/NOTICE @@ -10,6 +10,8 @@ The animated ink-vial item texture in this project is copied from Mystcraft Legacy's assets/mystcraft/textures/items/ink_vial.png under that license. The Notebook page atlas is copied from Legacy's assets/mystcraft/gui/bookui_pagel.png under the same license. +The Writing Desk item texture is copied from Legacy's +assets/mystcraft/textures/items/writingdesk.png under the same license. The initial build layout is derived from the official NeoForge 1.21.1 ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle diff --git a/README.md b/README.md index 8a8d153..eff9e1d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ 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 is now craftable from planks, a glass bottle, and a feather. +Its four collection tabs, ink/source/page work slots, 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. ## Linking Book diff --git a/docs/ASSET_PROVENANCE.md b/docs/ASSET_PROVENANCE.md index c68d464..b16fa40 100644 --- a/docs/ASSET_PROVENANCE.md +++ b/docs/ASSET_PROVENANCE.md @@ -8,6 +8,7 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license. | --- | --- | --- | --- | | `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/gui/notebook_pages.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Notebook draws the native 30×40 page at atlas coordinates `(156, 0)` | +| `assets/rivencraft/textures/item/writing_desk.png` | `assets/mystcraft/textures/items/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/item/linking_book_unwritten.png` | `assets/mystcraft/textures/items/unlinked.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/item/page_link_panel.png` | `page_background.png` and `PageBuilder.BasicPageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Reproduces the legacy 160 px scale, black panel at `(25,30)`–`(135,75)`, and 128 px resample | | `assets/rivencraft/textures/item/page_symbol*.png` and `page_unresolved.png` | `page_background.png`, `textures/symbolcomponents.png`, `WordData`, `ModSymbols`, and `PageBuilder.PageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Static 1.21.1 renditions of the legacy runtime-composed four-word symbol pages; `normal_time` uses the legacy Full Length poem | diff --git a/gradle.properties b/gradle.properties index e5133a9..368aa99 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.4.2 +mod_version=0.3.5.0 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/WritingDeskBlock.java b/src/main/java/net/dnorberg/rivencraft/block/WritingDeskBlock.java new file mode 100644 index 0000000..fe611d8 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/block/WritingDeskBlock.java @@ -0,0 +1,63 @@ +package net.dnorberg.rivencraft.block; + +import com.mojang.serialization.MapCodec; +import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; +import net.dnorberg.rivencraft.menu.WritingDeskMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Containers; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.BlockHitResult; + +public final class WritingDeskBlock extends BaseEntityBlock { + public static final MapCodec CODEC = simpleCodec(WritingDeskBlock::new); + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + public WritingDeskBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(FACING, net.minecraft.core.Direction.NORTH)); + } + + @Override public MapCodec codec() { return CODEC; } + @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + @Override public BlockState getStateForPlacement(BlockPlaceContext context) { + return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; } + @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new WritingDeskBlockEntity(pos, state); + } + @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, + Player player, BlockHitResult hit) { + if (!level.isClientSide && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof WritingDeskBlockEntity desk) { + serverPlayer.openMenu(new SimpleMenuProvider( + (id, inventory, ignored) -> new WritingDeskMenu(id, inventory, desk), + Component.translatable("container.rivencraft.writing_desk")), + buffer -> buffer.writeBlockPos(pos)); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + @Override protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState replacement, boolean moving) { + if (!state.is(replacement.getBlock()) && !level.isClientSide + && level.getBlockEntity(pos) instanceof WritingDeskBlockEntity desk) { + Containers.dropContents(level, pos, desk); + level.updateNeighbourForOutputSignal(pos, this); + } + super.onRemove(state, level, pos, replacement, moving); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java b/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java new file mode 100644 index 0000000..05307f8 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/block/entity/WritingDeskBlockEntity.java @@ -0,0 +1,85 @@ +package net.dnorberg.rivencraft.block.entity; + +import net.dnorberg.rivencraft.registry.ModBlockEntities; +import net.dnorberg.rivencraft.registry.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.NonNullList; +import net.minecraft.nbt.CompoundTag; +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.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. */ +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 PAPER = 6; + public static final int OUTPUT = 7; + public static final int SIZE = 8; + private NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private int activeCollection; + + public WritingDeskBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.WRITING_DESK.get(), pos, state); + } + + public int activeCollection() { return activeCollection; } + public void setActiveCollection(int index) { + if (index >= 0 && index < COLLECTIONS && activeCollection != index) { + activeCollection = index; + setChanged(); + } + } + + @Override public int getContainerSize() { return SIZE; } + @Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); } + @Override public ItemStack getItem(int slot) { return items.get(slot); } + @Override public ItemStack removeItem(int slot, int amount) { + ItemStack removed = ContainerHelper.removeItem(items, slot, amount); + if (!removed.isEmpty()) setChanged(); + return removed; + } + @Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); } + @Override public void setItem(int slot, ItemStack stack) { + items.set(slot, stack); + if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize()); + setChanged(); + } + @Override public boolean stillValid(Player player) { + return level != null && level.isLoaded(worldPosition) + && level.getBlockEntity(worldPosition) == this + && player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5, + worldPosition.getZ() + 0.5) <= 64.0; + } + @Override public boolean canPlaceItem(int slot, ItemStack stack) { + 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; + default -> false; + }; + } + @Override public void clearContent() { + items.clear(); + items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + setChanged(); + } + @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + ContainerHelper.loadAllItems(tag, items, registries); + activeCollection = Math.clamp(tag.getInt("ActiveCollection"), 0, COLLECTIONS - 1); + } + @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + ContainerHelper.saveAllItems(tag, items, registries); + tag.putInt("ActiveCollection", activeCollection); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java index 5f65461..32f4b92 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java +++ b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java @@ -14,6 +14,7 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.client.event.EntityRenderersEvent; import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.dnorberg.rivencraft.client.screen.NotebookScreen; +import net.dnorberg.rivencraft.client.screen.WritingDeskScreen; import net.dnorberg.rivencraft.registry.ModMenus; @EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT) @@ -29,6 +30,7 @@ public final class ClientEvents { @SubscribeEvent public static void registerScreens(RegisterMenuScreensEvent event) { event.register(ModMenus.NOTEBOOK.get(), NotebookScreen::new); + event.register(ModMenus.WRITING_DESK.get(), WritingDeskScreen::new); } @SubscribeEvent diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java new file mode 100644 index 0000000..6698c2d --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/WritingDeskScreen.java @@ -0,0 +1,68 @@ +package net.dnorberg.rivencraft.client.screen; + +import net.dnorberg.rivencraft.menu.WritingDeskMenu; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; + +/** Functional storage surface; full Legacy transcription controls are a later Milestone 4 slice. */ +public final class WritingDeskScreen extends AbstractContainerScreen { + public WritingDeskScreen(WritingDeskMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + imageWidth = 398; + imageHeight = 176; + titleLabelX = 222; + inventoryLabelX = 224; + inventoryLabelY = 79; + } + + @Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { + int x = leftPos; + int y = topPos; + graphics.fill(x + 4, y + 8, x + 216, y + 174, 0xFF383230); + graphics.fill(x + 44, y + 14, x + 210, y + 170, 0xFF221B1A); + graphics.fill(x + 218, y + 4, x + 396, y + 174, 0xFF555454); + graphics.fill(x + 220, y + 6, x + 394, y + 172, 0xFFB7B7B7); + for (int i = 0; i < 4; i++) { + graphics.fill(x + 6, y + 16 + i * 37, x + 40, y + 48 + i * 37, + i == menu.activeCollection() ? 0xFF7777B6 : 0xFF999999); + slotFrame(graphics, 12, 20 + i * 37); + } + slotFrame(graphics, 228, 20); + slotFrame(graphics, 266, 20); + slotFrame(graphics, 304, 20); + slotFrame(graphics, 342, 20); + for (int row = 0; row < 3; row++) { + for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 91 + row * 18); + } + for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 149); + } + + private void slotFrame(GuiGraphics graphics, int x, int y) { + graphics.fill(leftPos + x - 1, topPos + y - 1, + leftPos + x + 17, topPos + y + 17, 0xFF4D4D4D); + graphics.fill(leftPos + x, topPos + y, + leftPos + x + 16, topPos + y + 16, 0xFF858585); + } + + @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + super.render(graphics, mouseX, mouseY, partialTick); + renderTooltip(graphics, mouseX, mouseY); + } + + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && mouseX >= leftPos + 29 && mouseX < leftPos + 40) { + for (int i = 0; i < 4; i++) { + int top = topPos + 16 + i * 37; + if (mouseY >= top && mouseY < top + 32) { + if (minecraft != null && minecraft.gameMode != null) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, i); + } + return true; + } + } + } + return super.mouseClicked(mouseX, mouseY, button); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/WritingDeskGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDeskGameTests.java new file mode 100644 index 0000000..18c7c47 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDeskGameTests.java @@ -0,0 +1,85 @@ +package net.dnorberg.rivencraft.gametest; + +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; +import net.dnorberg.rivencraft.menu.WritingDeskMenu; +import net.dnorberg.rivencraft.registry.ModBlocks; +import net.dnorberg.rivencraft.registry.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.AABB; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; + +@GameTestHolder(Rivencraft.MOD_ID) +@PrefixGameTestTemplate(false) +public final class WritingDeskGameTests { + private WritingDeskGameTests() {} + + @GameTest(template = "empty") + public static void inventoryAndActiveCollectionPersist(GameTestHelper helper) { + BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1)); + var original = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState()); + original.setItem(0, new ItemStack(ModItems.NOTEBOOK.get())); + original.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); + original.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(ModItems.PAGE.get(), 12)); + original.setActiveCollection(2); + var saved = original.saveWithoutMetadata(helper.getLevel().registryAccess()); + var restored = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState()); + restored.loadWithComponents(saved, helper.getLevel().registryAccess()); + helper.assertTrue(restored.getItem(0).is(ModItems.NOTEBOOK.get()), "Notebook lost on save/load"); + helper.assertTrue(restored.getItem(WritingDeskBlockEntity.INK).is(ModItems.INK_VIAL.get()), "Ink lost on save/load"); + helper.assertValueEqual(restored.getItem(WritingDeskBlockEntity.PAPER).getCount(), 12, "Pages lost on save/load"); + helper.assertValueEqual(restored.activeCollection(), 2, "Active collection lost on save/load"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void breakingDeskDropsItsStoredItemsOnce(GameTestHelper helper) { + BlockPos relative = new BlockPos(1, 1, 1); + BlockPos pos = helper.absolutePos(relative); + helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3); + WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos); + desk.setItem(0, new ItemStack(ModItems.NOTEBOOK.get())); + desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); + desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(ModItems.PAGE.get(), 12)); + helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + var drops = helper.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(2)); + int notebooks = drops.stream().filter(e -> e.getItem().is(ModItems.NOTEBOOK.get())).mapToInt(e -> e.getItem().getCount()).sum(); + int ink = drops.stream().filter(e -> e.getItem().is(ModItems.INK_VIAL.get())).mapToInt(e -> e.getItem().getCount()).sum(); + int pages = drops.stream().filter(e -> e.getItem().is(ModItems.PAGE.get())).mapToInt(e -> e.getItem().getCount()).sum(); + helper.assertValueEqual(notebooks, 1, "Notebook must drop exactly once"); + helper.assertValueEqual(ink, 1, "Ink must drop exactly once"); + helper.assertValueEqual(pages, 12, "Pages must drop without loss"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void menuTransfersWithoutDuplicationAndClosesWhenReplaced(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); + var player = helper.makeMockServerPlayerInLevel(); + player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + player.getInventory().setItem(9, new ItemStack(ModItems.PAGE.get(), 12)); + WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk); + helper.assertTrue(menu.stillValid(player), "Placed desk must keep its menu valid"); + helper.assertTrue(menu.clickMenuButton(player, 3), "Active collection control must accept a valid tab"); + helper.assertValueEqual(desk.activeCollection(), 3, "Selected tab must be stored on the desk"); + menu.quickMoveStack(player, 8); + helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 12, "Shift-click must deposit all pages"); + helper.assertTrue(player.getInventory().getItem(9).isEmpty(), "Shift-click must consume player source stack"); + menu.quickMoveStack(player, WritingDeskBlockEntity.PAPER); + helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(), "Shift-click must empty desk source slot"); + int total = player.getInventory().items.stream().filter(stack -> stack.is(ModItems.PAGE.get())) + .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(); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java b/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java new file mode 100644 index 0000000..916e713 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/menu/WritingDeskMenu.java @@ -0,0 +1,101 @@ +package net.dnorberg.rivencraft.menu; + +import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; +import net.dnorberg.rivencraft.registry.ModItems; +import net.dnorberg.rivencraft.registry.ModMenus; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.DataSlot; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +public final class WritingDeskMenu extends AbstractContainerMenu { + private static final int PLAYER_START = WritingDeskBlockEntity.SIZE; + private final Container desk; + private final WritingDeskBlockEntity blockEntity; + private int syncedActiveCollection; + + public WritingDeskMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) { + this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()) + instanceof WritingDeskBlockEntity found ? found : null); + } + + public WritingDeskMenu(int id, Inventory inventory, WritingDeskBlockEntity blockEntity) { + super(ModMenus.WRITING_DESK.get(), id); + this.blockEntity = blockEntity; + this.desk = blockEntity == null ? new SimpleContainer(WritingDeskBlockEntity.SIZE) : blockEntity; + 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; } + }); + for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++) { + addSlot(new DeskSlot(desk, i, 12, 20 + i * 37, 1)); + } + addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 228, 20, 1)); + addSlot(new DeskSlot(desk, WritingDeskBlockEntity.SOURCE, 266, 20, 1)); + addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 304, 20, 64)); + addSlot(new DeskSlot(desk, WritingDeskBlockEntity.OUTPUT, 342, 20, 64)); + 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)); + } + } + for (int column = 0; column < 9; column++) { + addSlot(new Slot(inventory, column, 224 + column * 18, 149)); + } + } + + public int activeCollection() { return syncedActiveCollection; } + + @Override public boolean clickMenuButton(Player player, int id) { + if (blockEntity == null || !stillValid(player) || id < 0 || id >= WritingDeskBlockEntity.COLLECTIONS) return false; + blockEntity.setActiveCollection(id); + return true; + } + + @Override public boolean stillValid(Player player) { + return blockEntity != null && blockEntity.stillValid(player); + } + + @Override public ItemStack quickMoveStack(Player player, int index) { + if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY; + Slot slot = slots.get(index); + if (!slot.hasItem()) return ItemStack.EMPTY; + ItemStack moving = slot.getItem(); + ItemStack original = moving.copy(); + if (index < PLAYER_START) { + if (!moveItemStackTo(moving, PLAYER_START, slots.size(), true)) return ItemStack.EMPTY; + } else if (!moveIntoDesk(moving)) { + return ItemStack.EMPTY; + } + if (moving.isEmpty()) slot.setByPlayer(ItemStack.EMPTY); + else slot.setChanged(); + return original; + } + + 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); + } + return false; + } + + private static final class DeskSlot extends Slot { + private final int limit; + private DeskSlot(Container container, int index, int x, int y, int limit) { + super(container, index, x, y); + this.limit = limit; + } + @Override public boolean mayPlace(ItemStack stack) { return container.canPlaceItem(getContainerSlot(), stack); } + @Override public int getMaxStackSize() { return limit; } + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModBlockEntities.java b/src/main/java/net/dnorberg/rivencraft/registry/ModBlockEntities.java index 9452892..e335cd3 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModBlockEntities.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModBlockEntities.java @@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity; +import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; import net.minecraft.core.registries.Registries; import net.minecraft.world.level.block.entity.BlockEntityType; import net.neoforged.bus.api.IEventBus; @@ -16,6 +17,9 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("book_stand", () -> BlockEntityType.Builder .of(BookStandBlockEntity::new, ModBlocks.BOOK_STAND.get()) .build(null)); + public static final DeferredHolder, BlockEntityType> WRITING_DESK = + BLOCK_ENTITIES.register("writing_desk", () -> BlockEntityType.Builder + .of(WritingDeskBlockEntity::new, ModBlocks.WRITING_DESK.get()).build(null)); private ModBlockEntities() { } diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModBlocks.java b/src/main/java/net/dnorberg/rivencraft/registry/ModBlocks.java index 177e6dd..248f520 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModBlocks.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModBlocks.java @@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.block.BookStandBlock; +import net.dnorberg.rivencraft.block.WritingDeskBlock; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockBehaviour; import net.neoforged.bus.api.IEventBus; @@ -16,6 +17,10 @@ public final class ModBlocks { BookStandBlock::new, BlockBehaviour.Properties.ofFullCopy(Blocks.LECTERN) ); + public static final DeferredBlock WRITING_DESK = BLOCKS.registerBlock( + "writing_desk", WritingDeskBlock::new, + BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE) + ); private ModBlocks() { } diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java b/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java index 86eecc6..9a1683d 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java @@ -22,6 +22,7 @@ public final class ModCreativeTabs { output.accept(ModItems.LINKING_BOOK.get()); output.accept(ModItems.NOTEBOOK.get()); output.accept(ModItems.INK_VIAL.get()); + output.accept(ModItems.WRITING_DESK.get()); }) .build()); diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java b/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java index 6497924..4751d04 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModItems.java @@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.NotebookItem; import net.dnorberg.rivencraft.item.InkVialItem; import net.minecraft.world.item.Item; +import net.minecraft.world.item.BlockItem; import net.neoforged.bus.api.IEventBus; import net.neoforged.neoforge.registries.DeferredItem; import net.neoforged.neoforge.registries.DeferredRegister; @@ -39,6 +40,7 @@ public final class ModItems { InkVialItem::new, new Item.Properties().stacksTo(16) ); + public static final DeferredItem WRITING_DESK = ITEMS.registerSimpleBlockItem(ModBlocks.WRITING_DESK); private ModItems() { } diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModMenus.java b/src/main/java/net/dnorberg/rivencraft/registry/ModMenus.java index a3a9a80..757703b 100644 --- a/src/main/java/net/dnorberg/rivencraft/registry/ModMenus.java +++ b/src/main/java/net/dnorberg/rivencraft/registry/ModMenus.java @@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.menu.NotebookMenu; +import net.dnorberg.rivencraft.menu.WritingDeskMenu; import net.minecraft.core.registries.Registries; import net.minecraft.world.inventory.MenuType; import net.neoforged.bus.api.IEventBus; @@ -15,6 +16,8 @@ public final class ModMenus { public static final DeferredHolder, MenuType> NOTEBOOK = MENUS.register("notebook", () -> IMenuTypeExtension.create(NotebookMenu::new)); + public static final DeferredHolder, MenuType> WRITING_DESK = + MENUS.register("writing_desk", () -> IMenuTypeExtension.create(WritingDeskMenu::new)); private ModMenus() { } diff --git a/src/main/resources/assets/rivencraft/blockstates/writing_desk.json b/src/main/resources/assets/rivencraft/blockstates/writing_desk.json new file mode 100644 index 0000000..b860a13 --- /dev/null +++ b/src/main/resources/assets/rivencraft/blockstates/writing_desk.json @@ -0,0 +1,8 @@ +{ + "variants": { + "facing=north": { "model": "rivencraft:block/writing_desk" }, + "facing=east": { "model": "rivencraft:block/writing_desk", "y": 90 }, + "facing=south": { "model": "rivencraft:block/writing_desk", "y": 180 }, + "facing=west": { "model": "rivencraft:block/writing_desk", "y": 270 } + } +} diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index 9335ab2..f66a32e 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -16,6 +16,8 @@ "item.rivencraft.notebook.pages": "Pages: %s/%s", "item.rivencraft.notebook.unresolved": "Unresolved pages: %s", "item.rivencraft.ink_vial": "Ink Vial", + "block.rivencraft.writing_desk": "Writing Desk", + "container.rivencraft.writing_desk": "Writing Desk", "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", diff --git a/src/main/resources/assets/rivencraft/models/block/writing_desk.json b/src/main/resources/assets/rivencraft/models/block/writing_desk.json new file mode 100644 index 0000000..05530a2 --- /dev/null +++ b/src/main/resources/assets/rivencraft/models/block/writing_desk.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cube_bottom_top", + "textures": { + "top": "rivencraft:item/writing_desk", + "side": "minecraft:block/oak_planks", + "bottom": "minecraft:block/oak_planks" + } +} diff --git a/src/main/resources/assets/rivencraft/models/item/writing_desk.json b/src/main/resources/assets/rivencraft/models/item/writing_desk.json new file mode 100644 index 0000000..9104ac8 --- /dev/null +++ b/src/main/resources/assets/rivencraft/models/item/writing_desk.json @@ -0,0 +1,4 @@ +{ + "parent": "minecraft:item/generated", + "textures": { "layer0": "rivencraft:item/writing_desk" } +} diff --git a/src/main/resources/assets/rivencraft/textures/item/writing_desk.png b/src/main/resources/assets/rivencraft/textures/item/writing_desk.png new file mode 100644 index 0000000..08faa33 Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/item/writing_desk.png differ diff --git a/src/main/resources/data/rivencraft/loot_table/blocks/writing_desk.json b/src/main/resources/data/rivencraft/loot_table/blocks/writing_desk.json new file mode 100644 index 0000000..89a073b --- /dev/null +++ b/src/main/resources/data/rivencraft/loot_table/blocks/writing_desk.json @@ -0,0 +1,4 @@ +{ + "type": "minecraft:block", + "pools": [{ "rolls": 1, "entries": [{ "type": "minecraft:item", "name": "rivencraft:writing_desk" }] }] +} diff --git a/src/main/resources/data/rivencraft/recipe/writing_desk.json b/src/main/resources/data/rivencraft/recipe/writing_desk.json new file mode 100644 index 0000000..21f8989 --- /dev/null +++ b/src/main/resources/data/rivencraft/recipe/writing_desk.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "pattern": ["V F", "PPP", "P P"], + "key": { + "V": { "item": "minecraft:glass_bottle" }, + "F": { "item": "minecraft:feather" }, + "P": { "tag": "minecraft:planks" } + }, + "result": { "id": "rivencraft:writing_desk", "count": 1 } +}