diff --git a/dev images/Dropped Book.png b/dev images/Dropped Book.png deleted file mode 100644 index 08bb1be..0000000 Binary files a/dev images/Dropped Book.png and /dev/null differ diff --git a/dev images/Example1.png b/dev images/Example1.png deleted file mode 100644 index 7e2e7d9..0000000 Binary files a/dev images/Example1.png and /dev/null differ diff --git a/dev images/Example2.png b/dev images/Example2.png deleted file mode 100644 index 760d005..0000000 Binary files a/dev images/Example2.png and /dev/null differ diff --git a/dev images/Example3.png b/dev images/Example3.png deleted file mode 100644 index 6e30d05..0000000 Binary files a/dev images/Example3.png and /dev/null differ diff --git a/dev images/Example4.png b/dev images/Example4.png deleted file mode 100644 index 719f042..0000000 Binary files a/dev images/Example4.png and /dev/null differ diff --git a/dev images/Example5.png b/dev images/Example5.png deleted file mode 100644 index c3c7f9d..0000000 Binary files a/dev images/Example5.png and /dev/null differ diff --git a/dev images/Lectern.png b/dev images/Lectern.png deleted file mode 100644 index 17c0083..0000000 Binary files a/dev images/Lectern.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..fbcc2fe Binary files /dev/null and b/dev images/Pasted image (2).png differ diff --git a/dev images/Pasted image (3).png b/dev images/Pasted image (3).png new file mode 100644 index 0000000..b9655da Binary files /dev/null and b/dev images/Pasted image (3).png differ diff --git a/dev images/Pasted image (4).png b/dev images/Pasted image (4).png new file mode 100644 index 0000000..dc8dfbb Binary files /dev/null and b/dev images/Pasted image (4).png differ diff --git a/dev images/Pasted image (5).png b/dev images/Pasted image (5).png new file mode 100644 index 0000000..51be084 Binary files /dev/null and b/dev images/Pasted image (5).png differ diff --git a/dev images/Pasted image (6).png b/dev images/Pasted image (6).png new file mode 100644 index 0000000..abefcbd Binary files /dev/null and b/dev images/Pasted image (6).png differ diff --git a/dev images/Pasted image (7).png b/dev images/Pasted image (7).png new file mode 100644 index 0000000..7789370 Binary files /dev/null and b/dev images/Pasted image (7).png differ diff --git a/dev images/Pasted image (8).png b/dev images/Pasted image (8).png new file mode 100644 index 0000000..ecd5523 Binary files /dev/null and b/dev images/Pasted image (8).png differ diff --git a/dev images/Pasted image.png b/dev images/Pasted image.png new file mode 100644 index 0000000..647b315 Binary files /dev/null and b/dev images/Pasted image.png differ diff --git a/dev images/Standard portals.png b/dev images/Standard portals.png deleted file mode 100644 index 070f242..0000000 Binary files a/dev images/Standard portals.png and /dev/null differ diff --git a/dev images/Work tables.png b/dev images/Work tables.png deleted file mode 100644 index 891bb15..0000000 Binary files a/dev images/Work tables.png and /dev/null differ diff --git a/dev images/book binder gui.png b/dev images/book binder gui.png deleted file mode 100644 index 3cd0c46..0000000 Binary files a/dev images/book binder gui.png and /dev/null differ diff --git a/dev images/bound book gui.png b/dev images/bound book gui.png deleted file mode 100644 index 6cc6afb..0000000 Binary files a/dev images/bound book gui.png and /dev/null differ diff --git a/dev images/bound book no mods.png b/dev images/bound book no mods.png deleted file mode 100644 index ee0cf31..0000000 Binary files a/dev images/bound book no mods.png and /dev/null differ diff --git a/dev images/bound book ponder.png b/dev images/bound book ponder.png deleted file mode 100644 index a9ae860..0000000 Binary files a/dev images/bound book ponder.png and /dev/null differ diff --git a/dev images/immersive portals.png b/dev images/immersive portals.png deleted file mode 100644 index f3c4a5b..0000000 Binary files a/dev images/immersive portals.png and /dev/null differ diff --git a/docs/MINECRAFT_26_3_PORT.md b/docs/MINECRAFT_26_3_PORT.md index d1fec7f..f40f437 100644 --- a/docs/MINECRAFT_26_3_PORT.md +++ b/docs/MINECRAFT_26_3_PORT.md @@ -83,6 +83,12 @@ Age must retain its identity, seed, chunks and access across save/restart. identify the derived files. Prefer the smallest backend Rivencraft needs over a general-purpose library fork. +For gameplay systems, treat the 1.21.1 Rivencraft source and assets as the +porting baseline. Carry across unchanged rules, probabilities, recipes, and +textures directly; adapt only the Minecraft/NeoForge APIs that changed in +26.3. Record deliberate behavior gaps (such as prototype Age generation) +instead of silently replacing established mechanics with new ones. + The release gate is a 26.3 integrated-client **and** dedicated-server matrix: create two Ages with distinct seeds, explore chunks, travel while another client remains online, reconnect, restart, revisit saved chunks, reseed for @@ -419,6 +425,61 @@ without carrying its Page identity onto the Book. No Link Panel modifiers are ported yet. The player passed the in-game crafting check on 2026-10-29. The full Book Binder GUI and Age authoring remain the next larger port step. +The local `1.0.1.10-alpha.1` iteration adds the first three Link Panel +modifiers as a prerequisite to Binder authoring. All eight combinations of +Disarm, Maintain Momentum, and Intra-Linking are testable in the Link Pages +creative tab and persist as a synced component through leather crafting and +book binding. The server uses the book's flags when linking: Intra-Linking +allows same-dimension travel, Maintain Momentum rotates horizontal velocity +to the destination yaw, and Disarm drops player inventory at the departure +point only after a successful teleport. The Ink Mixer, additional Link Modifier +flags, and Descriptive Book panel propagation are not yet ported. In-game +testing of each modifier and their combinations passed in game. This local slice +must not be committed, pushed or published without the player's explicit request. + +The next local `1.0.1.11-alpha.1` slice introduces the Book Binder block and +its first server-authoritative binding workflow. Leather and a Link Panel +produce a named, bound Descriptive Book for a random prototype Age; the output +is persisted in the Binder until collected. The panel's Disarm and Maintain +Momentum flags carry onto the book and apply after successful Age travel; +Intra-Linking is inert for Descriptive Books. This is not yet symbol-page +authoring or the full Legacy Binder interface. Build and dedicated-server +smoke pass. The player passed basic Binder crafting and travel in game, but +reported that typing E closed the GUI and asked us to reuse the 1.21.1 GUI +more faithfully. No +commit, push, or publication without an explicit request during alpha. + +The local `1.0.1.12-alpha.1` follow-up ports the existing Binder layout's +top title field, flank slots, center Page strip, and pulsing red missing-panel +cue. It persists the title on the block entity and consumes focused keyboard +events before the container can treat E as inventory-close. Click the output +once to bind and again to collect the result. The full authored-symbol strip +and 1.21.1 grammar still await later port slices. The same slice adapts the +existing 1.21.1 Ink Mixer block, persistent basin, additive probabilities, +16-ink-per-panel exchange, virtual output, Legacy atlas and animated glyph to +the 26.3 storage, menu and GUI APIs. The build and dedicated-server Age smoke +pass. In-game Binder and Mixer verification is +pending; do not commit, push, or publish without explicit authorization. + +The local `1.0.1.13-alpha.1` iteration moves the Binder's freshly assembled +book directly to the cursor so one output click both binds and takes it. Its +central strip now displays five Legacy page thumbnails, while only the Link +Panel is currently writable. The Mixer recomputes its virtual output in the +same server click as an input change to address an intermittent first-attempt +delay. The user's second Mixer attempt succeeded but exposed a cost mismatch: +the Mixer now spends one ink and one paper per Link Panel and resets its +additive chances on output pickup. Build passes; in-game retest is +pending. This alpha remains local until explicitly authorized for release. + +The local `1.0.1.14-alpha.1` visual follow-up uses the new Binder dev images +as a reference: the page strip now grows only when a Link Panel is present or +carried, its required-panel cue appears above the strip, and the first page +can be clicked to place/take the panel. The stored page has a tooltip. The +rest of the 1.21.1 strip (symbol pages and scrolling) depends on porting the +symbol-page data and authoring pipeline, not just drawing more placeholders. +The local `1.0.1.15-alpha.1` follow-up changes the missing-panel cue to the +Link Panel's 16×16 item icon while preserving its red pulse and tooltip. + ## Verification of this first slice The 1.21.1 `compileJava` and `build` tasks passed offline after the backend diff --git a/ports/mc263/README.md b/ports/mc263/README.md index 45c6078..3783590 100644 --- a/ports/mc263/README.md +++ b/ports/mc263/README.md @@ -7,7 +7,7 @@ commands create and inspect separate vanilla-terrain levels. The current development slice can name an unbound Descriptive Book to create a prototype Age, then enter it from the book GUI; the player verified this interaction in game. Named Linking Book binding/travel works as a limited probe. Authored -generation, most content, recipes, workstations, portal systems, modifiers and +generation, most content, recipes, workstations, portal systems, most modifiers and the destination preview renderer remain unported. Use a test world and keep a backup; do not substitute this JAR for the 1.21.1 mod or import 1.21.1 saves. @@ -23,12 +23,62 @@ using the existing item texture. Blank Pages remain unavailable in Creative; the player verified the Link Panel's appearance in game. Ink Vials now use the existing crafting recipe and animated sprite. With paper in the other hand, an Ink Vial prepares a Link Panel for two of its sixteen ink uses, as -in the current 1.21.1 default. The Book Binder and symbol-writing systems -are still unported. The player verified the Ink Vial interaction in game. +in the current 1.21.1 default. The player verified the Ink Vial interaction +in game. Symbol writing remains unported. A prepared Link Panel can now be combined shapelessly with leather to craft an unwritten Linking Book, as in 1.21.1. This custom recipe rejects a blank -Page and extra ingredients. The Book Binder remains the next larger step for -assembling Descriptive Books from authored pages. +Page and extra ingredients. The first Book Binder slice now assembles random-Age +Descriptive Books from leather and a Link Panel. Authored pages remain later. + +The next local `1.0.1.10-alpha.1` slice exposes all eight combinations of +plain, Disarm, Maintain Momentum, and Intra-Linking Link Panels in the Link +Pages creative tab. Leather crafting preserves those flags on the Linking +Book. Intra-Linking permits same-dimension travel; Maintain Momentum rotates +horizontal speed to the destination yaw; Disarm leaves the player's inventory +at the departure point after a successful teleport. These are development +variants until the Ink Mixer is ported; other Link Panel modifiers are not +implemented in 26.3. The player passed this modifier slice in game. + +The local `1.0.1.11-alpha.1` slice adds a craftable Book Binder with persistent +leather, Link Panel, and output slots. Name an Age in its GUI and press Bind; +the server creates a random prototype Age and places its completed Descriptive +Book in the output. Disarm and Maintain Momentum carry from the panel into the +book; Intra-Linking has no effect on Age creation. Symbol-page authoring and +the Ink Mixer remain unported. This slice passes the build and dedicated-server +smoke test. The player passed crafting and travel in game, but found that E +closed the focused title field and the provisional layout did not match the +existing 1.21.1 Binder GUI. + +The local `1.0.1.12-alpha.1` follow-up reuses the 1.21.1 Binder layout: title +bar, cover and output at opposite sides, and a central Link Panel page strip. +The title persists in the block, the missing-panel cue pulses red, and a +focused title field consumes inventory/hotbar keys. Click the output once to +bind, then click again to take the completed book. Authored symbol strips and +the full 1.21.1 Binder grammar remain unported. This same slice also ports the +1.21.1 Ink Mixer block, menu, animation, modifier additives, 16-ink-per-panel +cost, and automatic vial exchange. The creative tab includes it and its +vanilla-shaped recipe is available. This slice needs an in-game Binder visual/ +keyboard check and Ink Mixer interaction check. + +The local `1.0.1.13-alpha.1` iteration makes Binder assembly place the new +Descriptive Book directly on the cursor with one click. It also renders the +five-page strip from the 1.21.1 GUI and refreshes the Mixer's virtual result +in the same server click that changes paper or ink. The Mixer now consumes +one ink unit and one paper per Link Panel (16 panels per full vial); additive +chances reset when a panel is taken. In-game retest of the first-update +behavior and Binder visual/click flow is pending. + +The local `1.0.1.14-alpha.1` follow-up compares the current 26.3 Binder against +the new dev-image sequence. The Binder no longer paints five permanently empty +pages: the first strip page appears when a Link Panel is inserted or carried, +and the missing-panel cue remains above it. Clicking that page inserts/removes +the Link Panel using the same server-owned cursor flow as 1.21.1. Its tooltip +also shows the stored panel. Multi-page symbol writing and strip scrolling +cannot be exercised yet because symbol pages are still unported to 26.3. + +The local `1.0.1.15-alpha.1` Binder polish uses the Link Panel's actual 16×16 +item icon for the missing-panel cue instead of a full-size page sprite. The +red pulse and tooltip remain confined to that icon. From the repository root: diff --git a/ports/mc263/build.gradle b/ports/mc263/build.gradle index 3b1f791..04e58c5 100644 --- a/ports/mc263/build.gradle +++ b/ports/mc263/build.gradle @@ -79,6 +79,22 @@ tasks.named('processResources', ProcessResources).configure { include 'assets/rivencraft/textures/gui/notebook_pages.png' include 'assets/rivencraft/textures/gui/bookui_pager.png' include 'assets/rivencraft/textures/block/crystal_block.png' + include 'assets/rivencraft/textures/block/book_binder.png' + include 'assets/rivencraft/textures/block/book_binder_press.png' + include 'assets/rivencraft/textures/gui/book_binder_legacy.png' + include 'assets/rivencraft/textures/gui/ink_mixer_legacy.png' + include 'assets/rivencraft/textures/block/inkwell_base.png' + include 'assets/rivencraft/textures/block/inkwell_cup.png' + include 'assets/rivencraft/textures/block/ink.png' + include 'assets/rivencraft/textures/block/ink_glass.png' + include 'assets/rivencraft/models/block/ink_mixer.json' + include 'assets/rivencraft/models/item/ink_mixer.json' + include 'assets/rivencraft/blockstates/ink_mixer.json' + include 'data/rivencraft/loot_table/blocks/ink_mixer.json' + include 'assets/rivencraft/models/block/book_binder.json' + include 'assets/rivencraft/models/item/book_binder.json' + include 'assets/rivencraft/blockstates/book_binder.json' + include 'data/rivencraft/loot_table/blocks/book_binder.json' include 'assets/rivencraft/models/block/crystal_block.json' include 'assets/rivencraft/blockstates/crystal_block.json' include 'assets/rivencraft/blockstates/budding_crystal.json' diff --git a/ports/mc263/gradle.properties b/ports/mc263/gradle.properties index 911f33d..426a7eb 100644 --- a/ports/mc263/gradle.properties +++ b/ports/mc263/gradle.properties @@ -7,4 +7,4 @@ neo_version=26.3.0.99 mod_id=rivencraft mod_name=Rivencraft mod_license=LGPL-3.0-or-later -mod_version=1.0.1.9-alpha.1 +mod_version=1.0.1.15-alpha.1 diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java index ebdc32e..ebd8070 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java @@ -6,8 +6,15 @@ import net.dnorberg.rivencraft.age.RuntimeAgeBackend; import net.dnorberg.rivencraft.age.PortAgeCommands; import net.dnorberg.rivencraft.age.AgeCatalogSync; import net.dnorberg.rivencraft.block.BuddingCrystalBlock; +import net.dnorberg.rivencraft.block.PortBookBinderBlock; +import net.dnorberg.rivencraft.block.InkMixerBlock; +import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity; +import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity; +import net.dnorberg.rivencraft.menu.PortBookBinderMenu; +import net.dnorberg.rivencraft.menu.InkMixerMenu; import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.LinkBookTarget; +import net.dnorberg.rivencraft.item.LinkProperties; import net.dnorberg.rivencraft.item.InkVialItem; import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.PageItem; @@ -26,11 +33,14 @@ import net.minecraft.world.level.material.MapColor; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.level.block.entity.BlockEntityType; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.registries.DeferredBlock; import net.neoforged.neoforge.registries.DeferredItem; import net.neoforged.neoforge.registries.DeferredRegister; +import net.neoforged.neoforge.common.extensions.IMenuTypeExtension; import java.util.function.Supplier; import net.neoforged.neoforge.common.NeoForge; import org.slf4j.Logger; @@ -42,6 +52,10 @@ public final class Rivencraft { public static final Logger LOGGER = LogUtils.getLogger(); private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID); private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID); + private static final DeferredRegister> BLOCK_ENTITY_TYPES = + DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID); + private static final DeferredRegister> MENUS = + DeferredRegister.create(Registries.MENU, MOD_ID); private static final DeferredRegister> RECIPE_SERIALIZERS = DeferredRegister.create(Registries.RECIPE_SERIALIZER, MOD_ID); private static final DeferredRegister.DataComponents COMPONENTS = @@ -60,9 +74,30 @@ public final class Rivencraft { public static final Supplier> INK_REMAINING = COMPONENTS.registerComponentType( "ink_remaining", builder -> builder.persistent(Codec.INT) .networkSynchronized(ByteBufCodecs.VAR_INT)); + public static final Supplier> LINK_PROPERTIES = COMPONENTS.registerComponentType( + "link_properties", builder -> builder.persistent(LinkProperties.CODEC) + .networkSynchronized(LinkProperties.STREAM_CODEC)); public static final DeferredBlock CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block", Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK)); + public static final DeferredBlock BOOK_BINDER = BLOCKS.registerBlock( + "book_binder", PortBookBinderBlock::new, + () -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE)); + public static final DeferredItem BOOK_BINDER_ITEM = ITEMS.registerSimpleBlockItem(BOOK_BINDER); + public static final DeferredBlock INK_MIXER = BLOCKS.registerBlock( + "ink_mixer", InkMixerBlock::new, + () -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE)); + public static final DeferredItem INK_MIXER_ITEM = ITEMS.registerSimpleBlockItem(INK_MIXER); + public static final Supplier> BOOK_BINDER_BLOCK_ENTITY = + BLOCK_ENTITY_TYPES.register("book_binder", () -> + new BlockEntityType<>(PortBookBinderBlockEntity::new, BOOK_BINDER.get())); + public static final Supplier> BOOK_BINDER_MENU = + MENUS.register("book_binder", () -> IMenuTypeExtension.create(PortBookBinderMenu::new)); + public static final Supplier> INK_MIXER_BLOCK_ENTITY = + BLOCK_ENTITY_TYPES.register("ink_mixer", () -> + new BlockEntityType<>(InkMixerBlockEntity::new, INK_MIXER.get())); + public static final Supplier> INK_MIXER_MENU = + MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new)); public static final DeferredItem CRYSTAL_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_BLOCK); public static final DeferredBlock BUDDING_CRYSTAL = BLOCKS.registerBlock( "budding_crystal", BuddingCrystalBlock::new, @@ -98,6 +133,8 @@ public final class Rivencraft { public Rivencraft(IEventBus modEventBus) { BLOCKS.register(modEventBus); ITEMS.register(modEventBus); + BLOCK_ENTITY_TYPES.register(modEventBus); + MENUS.register(modEventBus); COMPONENTS.register(modEventBus); RECIPE_SERIALIZERS.register(modEventBus); ModCreativeTabs.register(modEventBus); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/InkMixerBlock.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/InkMixerBlock.java new file mode 100644 index 0000000..374f4ae --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/InkMixerBlock.java @@ -0,0 +1,47 @@ +package net.dnorberg.rivencraft.block; + +import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity; +import net.dnorberg.rivencraft.menu.InkMixerMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +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.ItemStack; +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.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +/** 26.3 API adaptation of the 1.21.1 Ink Mixer block. */ +public final class InkMixerBlock extends BaseEntityBlock { + public InkMixerBlock(Properties properties) { super(properties); } + + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; } + @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new InkMixerBlockEntity(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 InkMixerBlockEntity mixer) { + serverPlayer.openMenu(new SimpleMenuProvider( + (id, inventory, ignored) -> new InkMixerMenu(id, inventory, mixer), + Component.translatable("container.rivencraft.ink_mixer")), + buffer -> buffer.writeBlockPos(pos)); + } + return InteractionResult.SUCCESS; + } + + @Override public void playerDestroy(ServerLevel level, ServerPlayer player, BlockPos pos, + BlockState state, BlockEntity entity, ItemStack tool) { + if (entity instanceof InkMixerBlockEntity mixer) Containers.dropContents(level, pos, mixer); + super.playerDestroy(level, player, pos, state, entity, tool); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/PortBookBinderBlock.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/PortBookBinderBlock.java new file mode 100644 index 0000000..7bd0cf2 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/PortBookBinderBlock.java @@ -0,0 +1,63 @@ +package net.dnorberg.rivencraft.block; + +import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity; +import net.dnorberg.rivencraft.menu.PortBookBinderMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerLevel; +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.ItemStack; +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.entity.BlockEntity; +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.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; + +public final class PortBookBinderBlock extends BaseEntityBlock { + public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + public PortBookBinderBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @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 PortBookBinderBlockEntity(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 PortBookBinderBlockEntity binder) { + serverPlayer.openMenu(new SimpleMenuProvider( + (id, inventory, ignored) -> new PortBookBinderMenu(id, inventory, binder), + Component.translatable("container.rivencraft.book_binder")), + buffer -> { + buffer.writeBlockPos(pos); + buffer.writeUtf(binder.title(), 32); + }); + } + return InteractionResult.SUCCESS; + } + @Override public void playerDestroy(ServerLevel level, ServerPlayer player, BlockPos pos, + BlockState state, BlockEntity entity, ItemStack tool) { + if (entity instanceof PortBookBinderBlockEntity binder) Containers.dropContents(level, pos, binder); + super.playerDestroy(level, player, pos, state, entity, tool); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/entity/InkMixerBlockEntity.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/entity/InkMixerBlockEntity.java new file mode 100644 index 0000000..6d14b23 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/entity/InkMixerBlockEntity.java @@ -0,0 +1,176 @@ +package net.dnorberg.rivencraft.block.entity; + +import java.util.Random; +import net.dnorberg.rivencraft.item.LinkProperties; +import net.dnorberg.rivencraft.item.InkVialItem; +import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.writing.InkVialExchange; +import net.dnorberg.rivencraft.writing.InkModifierIngredients; +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.core.BlockPos; +import net.minecraft.core.NonNullList; +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.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +/** Server-owned Link Panel mixer with a persistent, Legacy-style additive mixture. */ +public final class InkMixerBlockEntity extends BlockEntity implements Container { + public static final int INK = 0; + public static final int PAPER = 1; + public static final int BOTTLES = 2; + public static final int SIZE = 3; + + private NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private int inkRemaining; + private long nextModifierSeed = new Random().nextLong(); + private final double[] modifierChances = new double[3]; + private static final int[] FLAGS = {LinkProperties.DISARM, + LinkProperties.MAINTAIN_MOMENTUM, LinkProperties.INTRA_LINKING}; + private static final String[] CHANCE_KEYS = {"DisarmChance", "MomentumChance", "IntraLinkingChance"}; + private static final int[] OLD_CHANCE_INDEX = {0, 2, 3}; + + public InkMixerBlockEntity(BlockPos pos, BlockState state) { + super(Rivencraft.INK_MIXER_BLOCK_ENTITY.get(), pos, state); + } + + public int inkRemaining() { return inkRemaining; } + + public int modifierChancePermille(int index) { + return Math.clamp((int) Math.round(modifierChances[index] * 1000), 0, 1000); + } + + /** Infuses a carried ingredient into the current ink pool; only the server calls this. */ + public boolean addModifier(ItemStack carried, int amount) { + InkModifierIngredients.Ingredient ingredient = InkModifierIngredients.find(carried); + if (ingredient == null || inkRemaining == 0 || amount <= 0 || carried.getCount() < amount) + return false; + for (InkModifierIngredients.Effect effect : ingredient.effects()) + for (int index = 0; index < FLAGS.length; index++) + if (FLAGS[index] == effect.flag()) + modifierChances[index] = 1.0 - (1.0 - modifierChances[index]) + * Math.pow(1.0 - effect.chance(), amount); + carried.shrink(amount); + setChanged(); + return true; + } + + public ItemStack previewLinkPanel() { + if (!canCreateLinkPanel()) return ItemStack.EMPTY; + ItemStack panel = PageItem.createLinkPanelPage(); + LinkProperties properties = rollProperties(nextModifierSeed).properties(); + if (properties.flags() != 0) panel.set(Rivencraft.LINK_PROPERTIES.get(), properties); + return panel; + } + + private record Roll(LinkProperties properties, long nextSeed) {} + + private Roll rollProperties(long seed) { + Random random = new Random(seed); + int selected = 0; + for (int index = 0; index < FLAGS.length; index++) + if (modifierChances[index] > 0 && random.nextDouble() < modifierChances[index]) + selected |= FLAGS[index]; + return new Roll(new LinkProperties(selected), random.nextLong()); + } + + @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(); + if (slot == BOTTLES) refillEmptyBasin(); + } + 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(); + if (slot == INK || slot == BOTTLES) refillEmptyBasin(); + } + @Override public boolean canPlaceItem(int slot, ItemStack stack) { + return switch (slot) { + case INK -> stack.is(Rivencraft.INK_VIAL.get()); + case PAPER -> stack.is(Items.PAPER); + default -> false; + }; + } + @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; + } + + /** No server tick: refill only when ink, bottle space, or consumption changes. */ + private void refillEmptyBasin() { + if (level != null && !level.isClientSide() && inkRemaining == 0) { + int addedInk = InkVialExchange.exchange(items, INK, BOTTLES); + if (addedInk > 0) { + inkRemaining = addedInk; + setChanged(); + } + } + } + + public boolean canCreateLinkPanel() { + if (!items.get(PAPER).is(Items.PAPER)) return false; + return InkVialExchange.canPay(items, INK, BOTTLES, inkRemaining, 1); + } + + /** Commit inputs only when a virtual output is actually taken. */ + public boolean consumeLinkPanelMaterials() { + if (!canCreateLinkPanel()) return false; + int cost = 1; + if (inkRemaining < cost) { + inkRemaining += InkVialExchange.exchange(items, INK, BOTTLES); + } + inkRemaining -= cost; + items.get(PAPER).shrink(1); + if (items.get(PAPER).isEmpty()) items.set(PAPER, ItemStack.EMPTY); + nextModifierSeed = rollProperties(nextModifierSeed).nextSeed(); + java.util.Arrays.fill(modifierChances, 0); + setChanged(); + if (inkRemaining == 0) refillEmptyBasin(); + return true; + } + + @Override public void setLevel(Level level) { + super.setLevel(level); + refillEmptyBasin(); + } + @Override public void clearContent() { + items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + inkRemaining = 0; + java.util.Arrays.fill(modifierChances, 0); + setChanged(); + } + @Override protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + ContainerHelper.loadAllItems(input, items); + inkRemaining = Math.clamp(input.getIntOr("InkRemaining", 0), 0, InkVialItem.CAPACITY); + nextModifierSeed = input.getLongOr("NextModifierSeed", nextModifierSeed); + for (int index = 0; index < modifierChances.length; index++) { + modifierChances[index] = Math.clamp(input.getDoubleOr(CHANCE_KEYS[index], + input.getDoubleOr("ModifierChance" + OLD_CHANCE_INDEX[index], 0.0)), 0.0, 1.0); + } + } + @Override protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + ContainerHelper.saveAllItems(output, items); + output.putInt("InkRemaining", inkRemaining); + output.putLong("NextModifierSeed", nextModifierSeed); + for (int index = 0; index < modifierChances.length; index++) + output.putDouble(CHANCE_KEYS[index], modifierChances[index]); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/entity/PortBookBinderBlockEntity.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/entity/PortBookBinderBlockEntity.java new file mode 100644 index 0000000..02f6d09 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/entity/PortBookBinderBlockEntity.java @@ -0,0 +1,86 @@ +package net.dnorberg.rivencraft.block.entity; + +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.item.PageItem; +import net.minecraft.core.BlockPos; +import net.minecraft.core.NonNullList; +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; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +/** Persistent, server-owned inputs and output for the first 26.3 Binder slice. */ +public final class PortBookBinderBlockEntity extends BlockEntity implements Container { + public static final int COVER = 0; + public static final int PANEL = 1; + public static final int OUTPUT = 2; + public static final int SIZE = 3; + private NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private String title = ""; + + public PortBookBinderBlockEntity(BlockPos pos, BlockState state) { + super(Rivencraft.BOOK_BINDER_BLOCK_ENTITY.get(), pos, state); + } + + public String title() { return title; } + + public void setTitle(String value) { + String normalized = value == null ? "" : value.strip(); + if (normalized.length() > 32) return; + if (!title.equals(normalized)) { + title = normalized; + setChanged(); + } + } + + @Override public int getContainerSize() { return SIZE; } + @Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); } + @Override public void clearContent() { + items.clear(); + items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + setChanged(); + } + @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) { + ItemStack removed = ContainerHelper.takeItem(items, slot); + if (!removed.isEmpty()) setChanged(); + return removed; + } + @Override public void setItem(int slot, ItemStack stack) { + items.set(slot, stack); + setChanged(); + } + @Override public boolean canPlaceItem(int slot, ItemStack stack) { + return switch (slot) { + case COVER -> stack.is(Items.LEATHER); + case PANEL -> PageItem.isLinkPanel(stack); + default -> false; + }; + } + @Override public boolean stillValid(Player player) { + return level != null && level.getBlockEntity(worldPosition) == this + && player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5, + worldPosition.getZ() + 0.5) <= 64.0; + } + @Override protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + ContainerHelper.saveAllItems(output, items); + output.putString("Title", title); + } + @Override protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + ContainerHelper.loadAllItems(input, items); + title = input.getStringOr("Title", ""); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/InkMixerScreen.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/InkMixerScreen.java new file mode 100644 index 0000000..4e380c0 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/InkMixerScreen.java @@ -0,0 +1,167 @@ +package net.dnorberg.rivencraft.client; + +import com.mojang.blaze3d.platform.InputConstants; +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.item.InkVialItem; +import net.dnorberg.rivencraft.menu.InkMixerMenu; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.item.ItemStack; + +/** 1.21.1 Legacy atlas and mixer animation adapted to the 26.3 GUI API. */ +public final class InkMixerScreen extends AbstractContainerScreen { + private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath( + Rivencraft.MOD_ID, "textures/gui/ink_mixer_legacy.png"); + private static final int BASIN_X = 88; + private static final int BASIN_Y = 49; + private static final int BASIN_RADIUS = 30; + private static final int[] PROPERTY_COLORS = {0xFFE84848, 0xFF5555FF, 0xFF55EF55}; + private final int[] lastChances = new int[3]; + private boolean basinClickPendingRelease; + private long lastAdditionTime; + + public InkMixerScreen(InkMixerMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 176, 181); + inventoryLabelY = 87; + } + + @Override public boolean isPauseScreen() { return false; } + + @Override public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) { + super.extractBackground(graphics, mouseX, mouseY, partialTick); + int left = (width - imageWidth) / 2; + int top = (height - imageHeight) / 2; + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left + 54, top + 16, + 179, 16, 66, 65, 256, 256); + if (menu.inkRemaining() > 0) { + graphics.fill(left + 54, top + 16, left + 120, top + 81, 0xFF101014); + renderPropertyGlyph(graphics); + } + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top, + 0, 0, imageWidth, imageHeight, 256, 256); + } + + @Override protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xFF404040, false); + } + + @Override protected void extractTooltip(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + super.extractTooltip(graphics, mouseX, mouseY); + if (overBasin(mouseX, mouseY)) { + var lines = new java.util.ArrayList(); + lines.add(Component.translatable("screen.rivencraft.ink_mixer.ink", + menu.inkRemaining(), InkVialItem.CAPACITY)); + lines.add(Component.translatable("screen.rivencraft.ink_mixer.additive_hint")); + for (int index = 0; index < 3; index++) { + int chance = menu.modifierChancePermille(index); + if (chance > 0) lines.add(Component.translatable("screen.rivencraft.ink_mixer.chance", + switch (index) { + case 0 -> Component.translatable("item.rivencraft.link_property.disarm"); + case 1 -> Component.translatable("item.rivencraft.link_property.maintain_momentum"); + default -> Component.translatable("item.rivencraft.link_property.intra_linking"); + }, chance / 10.0)); + } + graphics.setComponentTooltipForNextFrame(font, lines, mouseX, mouseY, ItemStack.EMPTY, null); + } + } + + @Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { + if ((event.button() == InputConstants.MOUSE_BUTTON_LEFT + || event.button() == InputConstants.MOUSE_BUTTON_RIGHT) + && overBasin(event.x(), event.y()) && !menu.getCarried().isEmpty()) { + minecraft.gameMode.handleContainerInput(menu.containerId, InkMixerMenu.MODIFIER, + event.button() == InputConstants.MOUSE_BUTTON_RIGHT ? 1 : 0, + ContainerInput.PICKUP, minecraft.player); + basinClickPendingRelease = true; + return true; + } + return super.mouseClicked(event, doubleClick); + } + + @Override public boolean mouseReleased(MouseButtonEvent event) { + if (basinClickPendingRelease) { + basinClickPendingRelease = false; + return true; + } + return super.mouseReleased(event); + } + + private boolean overBasin(double mouseX, double mouseY) { + double x = mouseX - leftPos - BASIN_X; + double y = mouseY - topPos - BASIN_Y; + return x * x + y * y < BASIN_RADIUS * BASIN_RADIUS; + } + + /** A GUI-scale projection of Legacy's colored, rotating D'ni ink effect. */ + private void renderPropertyGlyph(GuiGraphicsExtractor graphics) { + long now = System.currentTimeMillis(); + int[] active = new int[3]; + int count = 0; + int strongest = 0; + for (int index = 0; index < lastChances.length; index++) { + int chance = menu.modifierChancePermille(index); + if (chance > lastChances[index]) lastAdditionTime = now; + lastChances[index] = chance; + if (chance > 0) { + active[count++] = index; + strongest = Math.max(strongest, chance); + } + } + if (count == 0) { + lastAdditionTime = 0; + return; + } + + double cycle = now / 1800.0; + int first = active[(int) cycle % count]; + int second = active[((int) cycle + 1) % count]; + double blend = (1.0 - Math.cos((cycle % 1.0) * Math.PI)) / 2.0; + int rgb = blendRgb(PROPERTY_COLORS[first], PROPERTY_COLORS[second], blend); + double pulse = Math.max(0.0, 1.0 - (now - lastAdditionTime) / 700.0); + int alpha = Math.clamp((int) (70 + strongest * 0.14 + pulse * 45), 0, 255); + int color = alpha << 24 | rgb; + int centerX = leftPos + BASIN_X; + int centerY = topPos + BASIN_Y; + double turn = now / 1200.0; + + for (int spoke = 0; spoke < 4; spoke++) { + for (int step = 0; step <= 30; step++) { + double progress = step / 30.0; + double angle = turn + spoke * Math.PI / 2.0 + progress * 0.9; + double radius = 3.0 + progress * (15.0 + pulse * 3.0); + plot(graphics, centerX + Math.cos(angle) * radius, + centerY + Math.sin(angle) * radius, color); + } + } + for (int step = 0; step < 96; step++) { + double angle = step * Math.PI / 48.0 + turn * 0.3; + plot(graphics, centerX + Math.cos(angle) * 19.0, + centerY + Math.sin(angle) * 19.0, color); + if (strongest >= 990 && step % 2 == 0) { + plot(graphics, centerX + Math.cos(angle) * 23.0, + centerY + Math.sin(angle) * 23.0, color); + } + } + } + + private static int blendRgb(int first, int second, double amount) { + int red = (int) (((first >> 16) & 0xFF) * (1.0 - amount) + + ((second >> 16) & 0xFF) * amount); + int green = (int) (((first >> 8) & 0xFF) * (1.0 - amount) + + ((second >> 8) & 0xFF) * amount); + int blue = (int) ((first & 0xFF) * (1.0 - amount) + (second & 0xFF) * amount); + return red << 16 | green << 8 | blue; + } + + private static void plot(GuiGraphicsExtractor graphics, double x, double y, int color) { + int pixelX = (int) Math.round(x); + int pixelY = (int) Math.round(y); + graphics.fill(pixelX, pixelY, pixelX + 1, pixelY + 1, color); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderClientEvents.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderClientEvents.java new file mode 100644 index 0000000..f4ae23b --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderClientEvents.java @@ -0,0 +1,18 @@ +package net.dnorberg.rivencraft.client; + +import net.dnorberg.rivencraft.Rivencraft; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; + +@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT) +public final class PortBookBinderClientEvents { + private PortBookBinderClientEvents() {} + + @SubscribeEvent + public static void registerScreens(RegisterMenuScreensEvent event) { + event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new); + event.register(Rivencraft.INK_MIXER_MENU.get(), InkMixerScreen::new); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderScreen.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderScreen.java new file mode 100644 index 0000000..2db6c82 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderScreen.java @@ -0,0 +1,170 @@ +package net.dnorberg.rivencraft.client; + +import com.mojang.blaze3d.platform.InputConstants; +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.menu.PortBookBinderMenu; +import net.dnorberg.rivencraft.network.AssembleBinderPayload; +import net.dnorberg.rivencraft.network.PortBookNetworking; +import net.dnorberg.rivencraft.network.SetBinderTitlePayload; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; + +/** Legacy-sized Binder with a page strip and single-click book assembly. */ +public final class PortBookBinderScreen extends AbstractContainerScreen { + private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath( + "rivencraft", "textures/gui/book_binder_legacy.png"); + private static final Identifier PAGE_ATLAS = Identifier.fromNamespaceAndPath( + "rivencraft", "textures/gui/notebook_pages.png"); + private static final int PAGE_Y = 48; + private static final int PAGE_WIDTH = 26; + private static final int PAGE_HEIGHT = 34; + private static final int MISSING_PANEL_X = 27; + private static final int MISSING_PANEL_Y = 26; + private static final int MISSING_PANEL_SIZE = 16; + private static final ItemStack MISSING_PANEL_ICON = PageItem.createLinkPanelPage(); + private EditBox titleInput; + private boolean stripClickPendingRelease; + + public PortBookBinderScreen(PortBookBinderMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title, 176, 181); + inventoryLabelY = 87; + } + + @Override public boolean isPauseScreen() { return false; } + + @Override protected void init() { + super.init(); + int left = (width - imageWidth) / 2; + int top = (height - imageHeight) / 2; + titleInput = new EditBox(font, left + 9, top + 10, 112, 12, Component.literal("Age name")); + titleInput.setBordered(false); + titleInput.setTextColor(0xFFFFFFFF); + titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH); + titleInput.setValue(menu.title()); + titleInput.setHint(Component.literal("Name this Age")); + titleInput.setResponder(value -> ClientPacketDistributor.sendToServer( + new SetBinderTitlePayload(menu.containerId, value))); + addRenderableWidget(titleInput); + setInitialFocus(titleInput); + } + + @Override public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) { + super.extractBackground(graphics, mouseX, mouseY, partialTick); + int left = (width - imageWidth) / 2; + int top = (height - imageHeight) / 2; + graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top, 0, 0, + imageWidth, imageHeight, 256, 256); + graphics.fill(left + 7, top + 9, left + 123, top + 23, 0xFF000000); + // The strip grows only when a page is present or the player is carrying + // a page to insert, just as the 1.21.1 Binder does. + boolean panelPresent = !menu.getSlot(1).getItem().isEmpty(); + if (panelPresent || PageItem.isLinkPanel(menu.getCarried())) { + int x = left + 16; + graphics.blit(RenderPipelines.GUI_TEXTURED, PAGE_ATLAS, + x, top + PAGE_Y, 156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256); + if (!panelPresent) + graphics.fill(x, top + PAGE_Y, x + PAGE_WIDTH, top + PAGE_Y + PAGE_HEIGHT, + 0xAA000000); + else graphics.fill(x + 5, top + PAGE_Y + 6, x + 22, top + PAGE_Y + 17, + 0xFF000000); + } + if (titleInput == null || titleInput.getValue().isBlank()) { + graphics.fill(left + 7, top + 9, left + 123, top + 10, 0xFFFF3030); + graphics.fill(left + 7, top + 22, left + 123, top + 23, 0xFFFF3030); + } + if (!panelPresent) { + graphics.fakeItem(MISSING_PANEL_ICON, left + MISSING_PANEL_X, top + MISSING_PANEL_Y); + float phase = (System.currentTimeMillis() % 4000L) / 2000.0F; + float fade = phase > 1.0F ? 2.0F - phase : phase; + int alpha = 40 + (int) (125 * fade); + graphics.fill(left + MISSING_PANEL_X, top + MISSING_PANEL_Y, + left + MISSING_PANEL_X + MISSING_PANEL_SIZE, + top + MISSING_PANEL_Y + MISSING_PANEL_SIZE, + (alpha << 24) | 0x00B02020); + } + if (menu.getSlot(2).getItem().isEmpty() && !menu.getSlot(0).getItem().isEmpty() + && !menu.getSlot(1).getItem().isEmpty() && titleInput != null + && !titleInput.getValue().isBlank()) { + graphics.fakeItem(new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()), left + 152, top + 27); + } + } + + @Override protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xFF404040, false); + } + + @Override protected void extractTooltip(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { + super.extractTooltip(graphics, mouseX, mouseY); + int x = mouseX - leftPos; + int y = mouseY - topPos; + if (x >= MISSING_PANEL_X && x < MISSING_PANEL_X + MISSING_PANEL_SIZE + && y >= MISSING_PANEL_Y && y < MISSING_PANEL_Y + MISSING_PANEL_SIZE + && menu.getSlot(1).getItem().isEmpty()) { + graphics.setTooltipForNextFrame(font, + Component.translatable("screen.rivencraft.book_binder.missing_panel"), mouseX, mouseY); + } else if (x >= 16 && x < 16 + PAGE_WIDTH && y >= PAGE_Y && y < PAGE_Y + PAGE_HEIGHT + && !menu.getSlot(1).getItem().isEmpty()) { + graphics.setTooltipForNextFrame(font, menu.getSlot(1).getItem(), mouseX, mouseY); + } + } + + @Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { + int left = (width - imageWidth) / 2; + int top = (height - imageHeight) / 2; + if ((event.button() == InputConstants.MOUSE_BUTTON_LEFT + || event.button() == InputConstants.MOUSE_BUTTON_RIGHT) + && event.x() >= left + 16 && event.x() < left + 16 + PAGE_WIDTH + && event.y() >= top + PAGE_Y && event.y() < top + PAGE_Y + PAGE_HEIGHT) { + boolean hasPanel = !menu.getSlot(1).getItem().isEmpty(); + if (hasPanel && menu.getCarried().isEmpty() + || !hasPanel && PageItem.isLinkPanel(menu.getCarried())) { + minecraft.gameMode.handleInventoryButtonClick(menu.containerId, + PortBookBinderMenu.STRIP_CLICK_PANEL); + stripClickPendingRelease = true; + return true; + } + } + if (event.button() == InputConstants.MOUSE_BUTTON_LEFT + && event.x() >= left + 151 && event.x() < left + 170 + && event.y() >= top + 26 && event.y() < top + 45 + && menu.getSlot(2).getItem().isEmpty() && menu.getCarried().isEmpty()) { + String name = titleInput.getValue().strip(); + if (!name.isEmpty()) ClientPacketDistributor.sendToServer(new AssembleBinderPayload(name)); + return true; + } + return super.mouseClicked(event, doubleClick); + } + + @Override public boolean mouseReleased(MouseButtonEvent event) { + if (stripClickPendingRelease) { + stripClickPendingRelease = false; + return true; + } + return super.mouseReleased(event); + } + + @Override public boolean keyPressed(KeyEvent event) { + if (titleInput != null && titleInput.isFocused()) { + if (event.isEscape()) { + setFocused(null); + titleInput.setFocused(false); + } else { + titleInput.keyPressed(event); + } + // The character event inserts text. Never forward a focused key + // to the container's inventory/hotbar shortcuts. + return true; + } + return super.keyPressed(event); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkProperties.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkProperties.java new file mode 100644 index 0000000..d524d04 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkProperties.java @@ -0,0 +1,29 @@ +package net.dnorberg.rivencraft.item; + +import com.mojang.serialization.Codec; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; + +/** The three Ink Mixer modifiers currently supported by the 26.3 port. */ +public record LinkProperties(int flags) { + public static final int DISARM = 1; + public static final int MAINTAIN_MOMENTUM = 4; + public static final int INTRA_LINKING = 8; + public static final int SUPPORTED = DISARM | MAINTAIN_MOMENTUM | INTRA_LINKING; + public static final LinkProperties NONE = new LinkProperties(0); + public static final Codec CODEC = Codec.INT.xmap(LinkProperties::new, LinkProperties::flags); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buffer, value) -> ByteBufCodecs.VAR_INT.encode(buffer, value.flags()), + buffer -> new LinkProperties(ByteBufCodecs.VAR_INT.decode(buffer))); + + public LinkProperties { + if (flags < 0 || (flags & ~SUPPORTED) != 0) { + throw new IllegalArgumentException("Unsupported Link Panel flags: " + flags); + } + } + + public boolean has(int flag) { + return (flags & flag) != 0; + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java index 5efee01..2c3e3fd 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java @@ -18,6 +18,8 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.component.TooltipDisplay; import net.minecraft.world.level.Level; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.phys.Vec3; /** Handheld Linking Book; destination binding and travel are server-authoritative packets. */ public final class LinkingBookItem extends Item { @@ -37,10 +39,13 @@ public final class LinkingBookItem extends Item { return InteractionResult.SUCCESS; } - public static String travel(ServerPlayer player, LinkBookTarget destination) { + public static String travel(ServerPlayer player, ItemStack book) { + LinkBookTarget destination = book.get(Rivencraft.LINK_BOOK_TARGET.get()); + LinkProperties properties = book.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE); if (destination == null) return "Unbound Linking Book (26.3 port preview)"; if (!destination.valid()) return "Invalid Linking Book destination"; - if (player.level().dimension().identifier().equals(destination.dimension())) { + if (player.level().dimension().identifier().equals(destination.dimension()) + && !properties.has(LinkProperties.INTRA_LINKING)) { return "Same-dimension linking requires an Intra-Linking page"; } ServerLevel target = player.level().getServer().getLevel( @@ -61,11 +66,41 @@ public final class LinkingBookItem extends Item { destination.z() - player.getZ()))) { return "Linked destination is obstructed"; } + ServerLevel source = player.level(); + Vec3 departure = player.position(); + Vec3 incoming = player.getDeltaMovement(); boolean moved = player.teleportTo(target, destination.x(), destination.y(), destination.z(), Set.of(), destination.yaw(), destination.pitch(), true); + if (moved) applySuccessfulLink(player, properties, source, departure, incoming, destination.yaw()); return moved ? null : "Teleportation failed"; } + /** Link Panel effects are shared by Linking and Descriptive Books. */ + public static void applySuccessfulLink(ServerPlayer player, LinkProperties properties, + ServerLevel source, Vec3 departure, Vec3 incoming, float destinationYaw) { + if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) { + double speed = Math.hypot(incoming.x, incoming.z); + double yaw = Math.toRadians(destinationYaw); + player.setDeltaMovement(speed * Math.cos(yaw), incoming.y + 0.2, + speed * Math.sin(yaw)); + } else { + player.setDeltaMovement(Vec3.ZERO); + } + if (properties.has(LinkProperties.DISARM)) { + for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) { + ItemStack carried = player.getInventory().getItem(slot); + if (carried.isEmpty()) continue; + player.getInventory().setItem(slot, ItemStack.EMPTY); + ItemEntity dropped = new ItemEntity(source, departure.x, departure.y, departure.z, + carried.copy()); + dropped.setDefaultPickUpDelay(); + source.addFreshEntity(dropped); + } + player.getInventory().setChanged(); + player.containerMenu.broadcastChanges(); + } + } + @Override public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { @@ -73,5 +108,12 @@ public final class LinkingBookItem extends Item { tooltip.accept(Component.literal(target == null ? "Unbound (26.3 port preview)" : "Destination: " + target.dimension() + " (" + (int) target.x() + ", " + (int) target.y() + ", " + (int) target.z() + ")")); + LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE); + if (properties.has(LinkProperties.DISARM)) + tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm")); + if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) + tooltip.accept(Component.translatable("item.rivencraft.link_property.maintain_momentum")); + if (properties.has(LinkProperties.INTRA_LINKING)) + tooltip.accept(Component.translatable("item.rivencraft.link_property.intra_linking")); } } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/PageItem.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/PageItem.java index ac7dc75..4274e88 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/PageItem.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/PageItem.java @@ -16,8 +16,13 @@ public final class PageItem extends Item { } public static ItemStack createLinkPanelPage() { + return createLinkPanelPage(0); + } + + public static ItemStack createLinkPanelPage(int flags) { ItemStack stack = new ItemStack(Rivencraft.PAGE.get()); stack.set(Rivencraft.LINK_PANEL_PAGE.get(), true); + if (flags != 0) stack.set(Rivencraft.LINK_PROPERTIES.get(), new LinkProperties(flags)); return stack; } @@ -36,6 +41,13 @@ public final class PageItem extends Item { @Override public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display, Consumer tooltip, TooltipFlag flag) { + LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE); + if (properties.has(LinkProperties.DISARM)) + tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm")); + if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) + tooltip.accept(Component.translatable("item.rivencraft.link_property.maintain_momentum")); + if (properties.has(LinkProperties.INTRA_LINKING)) + tooltip.accept(Component.translatable("item.rivencraft.link_property.intra_linking")); tooltip.accept(Component.translatable(isLinkPanel(stack) ? "item.rivencraft.page.link_panel.port_status" : "item.rivencraft.page.blank.port_status")); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/menu/InkMixerMenu.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/menu/InkMixerMenu.java new file mode 100644 index 0000000..bbe3cb1 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/menu/InkMixerMenu.java @@ -0,0 +1,158 @@ +package net.dnorberg.rivencraft.menu; + +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity; +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.ContainerInput; +import net.minecraft.world.inventory.DataSlot; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** Server-owned virtual output; taking it spends paper and the configured ink cost once. */ +public final class InkMixerMenu extends AbstractContainerMenu { + public static final int MODIFIER = InkMixerBlockEntity.SIZE; + public static final int RESULT = MODIFIER + 1; + private static final int PLAYER_START = RESULT + 1; + private final InkMixerBlockEntity mixer; + private final Container inputs; + private final SimpleContainer result = new SimpleContainer(1); + private final boolean clientSide; + private int inkRemaining; + private final int[] modifierChances = new int[3]; + + public InkMixerMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) { + this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()) + instanceof InkMixerBlockEntity found ? found : null); + } + + public InkMixerMenu(int id, Inventory inventory, InkMixerBlockEntity mixer) { + super(Rivencraft.INK_MIXER_MENU.get(), id); + this.mixer = mixer; + inputs = mixer == null ? new SimpleContainer(InkMixerBlockEntity.SIZE) : mixer; + clientSide = inventory.player.level().isClientSide(); + addDataSlot(new DataSlot() { + @Override public int get() { return clientSide ? inkRemaining : mixer.inkRemaining(); } + @Override public void set(int value) { inkRemaining = value; } + }); + for (int index = 0; index < modifierChances.length; index++) { + final int chanceIndex = index; + addDataSlot(new DataSlot() { + @Override public int get() { + return clientSide ? modifierChances[chanceIndex] : mixer.modifierChancePermille(chanceIndex); + } + @Override public void set(int value) { modifierChances[chanceIndex] = value; } + }); + } + addSlot(new MixerSlot(InkMixerBlockEntity.INK, 8, 27)); + addSlot(new MixerSlot(InkMixerBlockEntity.PAPER, 8, 48)); + addSlot(new MixerSlot(InkMixerBlockEntity.BOTTLES, 152, 27)); + addSlot(new ModifierSlot(79, 40)); + addSlot(new ResultSlot(152, 48)); + for (int row = 0; row < 3; row++) + for (int column = 0; column < 9; column++) + addSlot(new Slot(inventory, column + row * 9 + 9, 8 + column * 18, 99 + row * 18)); + for (int column = 0; column < 9; column++) + addSlot(new Slot(inventory, column, 8 + column * 18, 157)); + refreshResult(); + } + + public int inkRemaining() { return inkRemaining; } + public int modifierChancePermille(int index) { return modifierChances[index]; } + + @Override public void clicked(int slotId, int button, ContainerInput clickType, Player player) { + if (slotId == MODIFIER) { + if (!clientSide && stillValid(player) && clickType == ContainerInput.PICKUP + && (button == 0 || button == 1)) { + ItemStack carried = getCarried(); + int amount = button == 1 ? 1 : carried.getCount(); + if (mixer.addModifier(carried, amount)) { + setCarried(carried); + refreshResult(); + broadcastChanges(); + } + } + return; + } + super.clicked(slotId, button, clickType, player); + // In 26.3, recompute the virtual result in the same packet that changes + // paper or ink. Waiting for a later menu update can require a second click. + if (!clientSide) refreshResult(); + } + + @Override public boolean stillValid(Player player) { + return mixer != null && mixer.stillValid(player); + } + + @Override public void broadcastChanges() { + refreshResult(); + super.broadcastChanges(); + } + + private void refreshResult() { + if (clientSide) return; + ItemStack preview = mixer == null ? ItemStack.EMPTY : mixer.previewLinkPanel(); + if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview); + } + + @Override public ItemStack quickMoveStack(Player player, int index) { + if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY; + if (index == RESULT) { + refreshResult(); + if (!mixer.canCreateLinkPanel()) return ItemStack.EMPTY; + ItemStack output = result.getItem(0).copy(); + ItemStack remainder = output.copy(); + if (!moveItemStackTo(remainder, PLAYER_START, slots.size(), true) || !remainder.isEmpty()) + return ItemStack.EMPTY; + if (!mixer.consumeLinkPanelMaterials()) return ItemStack.EMPTY; + refreshResult(); + return output; + } + Slot slot = slots.get(index); + if (!slot.hasItem()) return ItemStack.EMPTY; + ItemStack source = slot.getItem(); + ItemStack original = source.copy(); + boolean moved; + if (index < MODIFIER) moved = moveItemStackTo(source, PLAYER_START, slots.size(), true); + else if (source.is(Rivencraft.INK_VIAL.get())) + moved = moveItemStackTo(source, InkMixerBlockEntity.INK, InkMixerBlockEntity.INK + 1, false); + else if (source.is(Items.PAPER)) + moved = moveItemStackTo(source, InkMixerBlockEntity.PAPER, InkMixerBlockEntity.PAPER + 1, false); + else moved = false; + if (!moved) return ItemStack.EMPTY; + if (source.isEmpty()) slot.setByPlayer(ItemStack.EMPTY); + else slot.setChanged(); + refreshResult(); + return original; + } + + private final class MixerSlot extends Slot { + MixerSlot(int index, int x, int y) { super(inputs, index, x, y); } + @Override public boolean mayPlace(ItemStack stack) { return inputs.canPlaceItem(getContainerSlot(), stack); } + } + + private static final class ModifierSlot extends Slot { + ModifierSlot(int x, int y) { super(new SimpleContainer(1), 0, x, y); } + @Override public boolean mayPlace(ItemStack stack) { return false; } + @Override public boolean mayPickup(Player player) { return false; } + } + + private final class ResultSlot extends Slot { + ResultSlot(int x, int y) { super(result, 0, x, y); } + @Override public boolean mayPlace(ItemStack stack) { return false; } + @Override public boolean mayPickup(Player player) { + return clientSide ? hasItem() : stillValid(player) && mixer.canCreateLinkPanel(); + } + @Override public void onTake(Player player, ItemStack stack) { + if (!clientSide && mixer.consumeLinkPanelMaterials()) { + refreshResult(); + } + super.onTake(player, stack); + } + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/menu/PortBookBinderMenu.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/menu/PortBookBinderMenu.java new file mode 100644 index 0000000..ed87ce8 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/menu/PortBookBinderMenu.java @@ -0,0 +1,129 @@ +package net.dnorberg.rivencraft.menu; + +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity; +import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.network.PortBookNetworking; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +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.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** Binder inputs persist in the block; assembling places the finished book on the cursor. */ +public final class PortBookBinderMenu extends AbstractContainerMenu { + public static final int STRIP_CLICK_PANEL = 3000; + private final Container binder; + private final PortBookBinderBlockEntity blockEntity; + private final String initialTitle; + + public PortBookBinderMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) { + this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()) + instanceof PortBookBinderBlockEntity found ? found : null, data.readUtf(32)); + } + + public PortBookBinderMenu(int id, Inventory inventory, PortBookBinderBlockEntity blockEntity) { + this(id, inventory, blockEntity, blockEntity == null ? "" : blockEntity.title()); + } + + private PortBookBinderMenu(int id, Inventory inventory, PortBookBinderBlockEntity blockEntity, String title) { + super(Rivencraft.BOOK_BINDER_MENU.get(), id); + this.blockEntity = blockEntity; + initialTitle = title; + binder = blockEntity == null ? new SimpleContainer(PortBookBinderBlockEntity.SIZE) : blockEntity; + addSlot(new Slot(binder, PortBookBinderBlockEntity.COVER, 8, 27) { + @Override public boolean mayPlace(ItemStack stack) { return stack.is(Items.LEATHER); } + }); + addSlot(new Slot(binder, PortBookBinderBlockEntity.PANEL, -1000, -1000) { + @Override public boolean mayPlace(ItemStack stack) { return PageItem.isLinkPanel(stack); } + @Override public int getMaxStackSize() { return 1; } + }); + addSlot(new Slot(binder, PortBookBinderBlockEntity.OUTPUT, 152, 27) { + @Override public boolean mayPlace(ItemStack stack) { return false; } + }); + addStandardInventorySlots(inventory, 8, 99); + } + + @Override public boolean stillValid(Player player) { + return blockEntity != null && blockEntity.stillValid(player); + } + + public String title() { return initialTitle; } + + @Override public boolean clickMenuButton(Player player, int id) { + if (id != STRIP_CLICK_PANEL || !stillValid(player)) return false; + ItemStack carried = getCarried(); + if (carried.isEmpty()) { + ItemStack panel = binder.removeItem(PortBookBinderBlockEntity.PANEL, 1); + if (panel.isEmpty()) return false; + setCarried(panel); + } else { + if (!PageItem.isLinkPanel(carried) + || !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty()) return false; + binder.setItem(PortBookBinderBlockEntity.PANEL, carried.copyWithCount(1)); + ItemStack remainder = carried.copy(); + remainder.shrink(1); + setCarried(remainder); + } + broadcastChanges(); + return true; + } + + public void setTitle(ServerPlayer player, String value) { + if (stillValid(player) && value != null && value.length() <= PortBookNetworking.MAX_BOOK_TITLE_LENGTH) + blockEntity.setTitle(value); + } + + public void assemble(ServerPlayer player, String title) { + if (!stillValid(player) || title.isBlank() || title.length() > PortBookNetworking.MAX_BOOK_TITLE_LENGTH + || !binder.getItem(PortBookBinderBlockEntity.OUTPUT).isEmpty() + || !getCarried().isEmpty()) return; + ItemStack cover = binder.getItem(PortBookBinderBlockEntity.COVER); + ItemStack panel = binder.getItem(PortBookBinderBlockEntity.PANEL); + if (!cover.is(Items.LEATHER) || !PageItem.isLinkPanel(panel)) return; + blockEntity.setTitle(title); + ItemStack book = new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()); + var id = PortBookNetworking.createAge(player, title); + if (id == null) return; + book.set(Rivencraft.AGE_BOOK_ID.get(), id); + book.set(DataComponents.CUSTOM_NAME, Component.literal(title)); + var modifiers = panel.get(Rivencraft.LINK_PROPERTIES.get()); + if (modifiers != null) book.set(Rivencraft.LINK_PROPERTIES.get(), modifiers); + cover.shrink(1); + panel.shrink(1); + binder.setItem(PortBookBinderBlockEntity.COVER, cover); + binder.setItem(PortBookBinderBlockEntity.PANEL, panel); + setCarried(book); + broadcastChanges(); + } + + @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 stack = slot.getItem(); + ItemStack original = stack.copy(); + boolean moved; + if (index < PortBookBinderBlockEntity.SIZE) { + moved = moveItemStackTo(stack, PortBookBinderBlockEntity.SIZE, slots.size(), true); + } else if (stack.is(Items.LEATHER)) { + moved = moveItemStackTo(stack, PortBookBinderBlockEntity.COVER, + PortBookBinderBlockEntity.COVER + 1, false); + } else if (PageItem.isLinkPanel(stack)) { + moved = moveItemStackTo(stack, PortBookBinderBlockEntity.PANEL, + PortBookBinderBlockEntity.PANEL + 1, false); + } else return ItemStack.EMPTY; + if (!moved) return ItemStack.EMPTY; + if (stack.isEmpty()) slot.setByPlayer(ItemStack.EMPTY); + else slot.setChanged(); + slot.onTake(player, stack); + return original; + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/AssembleBinderPayload.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/AssembleBinderPayload.java new file mode 100644 index 0000000..8e3730f --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/AssembleBinderPayload.java @@ -0,0 +1,17 @@ +package net.dnorberg.rivencraft.network; + +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +public record AssembleBinderPayload(String title) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "assemble_binder")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite(ByteBufCodecs.stringUtf8(32), AssembleBinderPayload::title, + AssembleBinderPayload::new); + @Override public Type type() { return TYPE; } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java index f6fb241..2baffb3 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java @@ -4,7 +4,9 @@ import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.age.PortAgeTravel; import net.dnorberg.rivencraft.age.RuntimeAgeBackend; import net.dnorberg.rivencraft.item.LinkBookTarget; +import net.dnorberg.rivencraft.item.LinkProperties; import net.dnorberg.rivencraft.item.LinkingBookItem; +import net.dnorberg.rivencraft.menu.PortBookBinderMenu; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; @@ -13,6 +15,8 @@ import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.world.InteractionHand; import net.minecraft.world.item.ItemStack; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.phys.Vec3; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; import java.util.Locale; @@ -41,6 +45,21 @@ public final class PortBookNetworking { (payload, context) -> { if (context.player() instanceof ServerPlayer player) enterAge(player, payload); }); + registrar.playToServer(AssembleBinderPayload.TYPE, AssembleBinderPayload.STREAM_CODEC, + (payload, context) -> { + if (context.player() instanceof ServerPlayer player + && player.containerMenu instanceof PortBookBinderMenu menu) { + menu.assemble(player, payload.title().strip()); + } + }); + registrar.playToServer(SetBinderTitlePayload.TYPE, SetBinderTitlePayload.STREAM_CODEC, + (payload, context) -> { + if (context.player() instanceof ServerPlayer player + && player.containerMenu instanceof PortBookBinderMenu menu + && menu.containerId == payload.containerId()) { + menu.setTitle(player, payload.title()); + } + }); } private static void bind(ServerPlayer player, BindLinkingBookPayload payload) { @@ -59,7 +78,7 @@ public final class PortBookNetworking { private static void use(ServerPlayer player, UseLinkingBookPayload payload) { ItemStack stack = player.getItemInHand(hand(payload.offhand())); if (!stack.is(Rivencraft.LINKING_BOOK.get())) return; - String failure = LinkingBookItem.travel(player, stack.get(Rivencraft.LINK_BOOK_TARGET.get())); + String failure = LinkingBookItem.travel(player, stack); if (failure != null) player.sendOverlayMessage(Component.literal(failure)); } @@ -68,6 +87,19 @@ public final class PortBookNetworking { if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get()) || stack.has(Rivencraft.AGE_BOOK_ID.get())) return; String title = payload.title().strip(); if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return; + Identifier id = createAge(player, title); + if (id == null) return; + stack.set(Rivencraft.AGE_BOOK_ID.get(), id); + stack.set(DataComponents.CUSTOM_NAME, Component.literal(title)); + player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, + SoundSource.PLAYERS, 1.0F, 1.0F); + player.sendOverlayMessage(Component.literal("Bound Descriptive Book: " + title)); + player.containerMenu.broadcastChanges(); + } + + /** Shared name resolution and Age creation for handheld books and the Binder. */ + public static Identifier createAge(ServerPlayer player, String title) { + if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null; String base = title.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9._-]+", "_") .replaceAll("^[_\\.-]+|[_\\.-]+$", ""); if (base.isEmpty()) base = "age"; @@ -83,14 +115,9 @@ public final class PortBookNetworking { RuntimeAgeBackend.create(server, id, player.getRandom().nextLong()); } catch (IllegalArgumentException | IllegalStateException exception) { player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage())); - return; + return null; } - stack.set(Rivencraft.AGE_BOOK_ID.get(), id); - stack.set(DataComponents.CUSTOM_NAME, Component.literal(title)); - player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, - SoundSource.PLAYERS, 1.0F, 1.0F); - player.sendOverlayMessage(Component.literal("Bound Descriptive Book: " + title)); - player.containerMenu.broadcastChanges(); + return id; } private static void enterAge(ServerPlayer player, UseDescriptiveBookPayload payload) { @@ -98,8 +125,14 @@ public final class PortBookNetworking { if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return; Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get()); if (id == null) return; + ServerLevel source = player.level(); + Vec3 departure = player.position(); + Vec3 incoming = player.getDeltaMovement(); String failure = PortAgeTravel.enter(player, id); if (failure != null) player.sendOverlayMessage(Component.literal(failure)); + else LinkingBookItem.applySuccessfulLink(player, + stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE), + source, departure, incoming, player.getYRot()); } private static InteractionHand hand(boolean offhand) { diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/SetBinderTitlePayload.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/SetBinderTitlePayload.java new file mode 100644 index 0000000..eecb192 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/SetBinderTitlePayload.java @@ -0,0 +1,18 @@ +package net.dnorberg.rivencraft.network; + +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +public record SetBinderTitlePayload(int containerId, String title) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_binder_title")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite(ByteBufCodecs.VAR_INT, SetBinderTitlePayload::containerId, + ByteBufCodecs.stringUtf8(32), SetBinderTitlePayload::title, + SetBinderTitlePayload::new); + @Override public Type type() { return TYPE; } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java index 831d823..f8b85a7 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/registry/ModCreativeTabs.java @@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.item.LinkProperties; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.world.item.CreativeModeTab; @@ -28,12 +29,23 @@ public final class ModCreativeTabs { output.accept(Rivencraft.CRYSTAL_CLUSTER_ITEM.get()); output.accept(Rivencraft.CRYSTAL_SHARD.get()); output.accept(Rivencraft.INK_VIAL.get()); + output.accept(Rivencraft.BOOK_BINDER_ITEM.get()); + output.accept(Rivencraft.INK_MIXER_ITEM.get()); }) .build()); TABS.register("rivencraft_link_pages", () -> CreativeModeTab.builder() .title(Component.translatable("itemGroup.rivencraft.pages.link")) .icon(PageItem::createLinkPanelPage) - .displayItems((parameters, output) -> output.accept(PageItem.createLinkPanelPage())) + .displayItems((parameters, output) -> { + int[] flags = {LinkProperties.DISARM, LinkProperties.MAINTAIN_MOMENTUM, + LinkProperties.INTRA_LINKING}; + for (int combination = 0; combination < 8; combination++) { + int selected = 0; + for (int bit = 0; bit < flags.length; bit++) + if ((combination & (1 << bit)) != 0) selected |= flags[bit]; + output.accept(PageItem.createLinkPanelPage(selected)); + } + }) .build()); } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/InkModifierIngredients.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/InkModifierIngredients.java new file mode 100644 index 0000000..23e9b2d --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/InkModifierIngredients.java @@ -0,0 +1,31 @@ +package net.dnorberg.rivencraft.writing; + +import java.util.List; +import net.dnorberg.rivencraft.item.LinkProperties; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** Built-in Legacy ink additives, carried forward from the 1.21.1 mixer. */ +public final class InkModifierIngredients { + public record Effect(int flag, double chance) {} + public record Ingredient(ItemStack item, List effects) {} + + private static final List BUILT_IN = List.of( + new Ingredient(new ItemStack(Items.GUNPOWDER), List.of(new Effect(LinkProperties.DISARM, 0.20))), + new Ingredient(new ItemStack(Items.MUSHROOM_STEW), List.of(new Effect(LinkProperties.DISARM, 0.05))), + new Ingredient(new ItemStack(Items.EXPERIENCE_BOTTLE), List.of(new Effect(LinkProperties.INTRA_LINKING, 0.15))), + new Ingredient(new ItemStack(Items.ENDER_PEARL), List.of( + new Effect(LinkProperties.INTRA_LINKING, 0.15), + new Effect(LinkProperties.DISARM, 0.15))), + new Ingredient(new ItemStack(Items.FEATHER), List.of(new Effect(LinkProperties.MAINTAIN_MOMENTUM, 0.15))), + new Ingredient(new ItemStack(Items.FIRE_CHARGE), List.of(new Effect(LinkProperties.DISARM, 0.25))) + ); + + private InkModifierIngredients() {} + + public static List builtIn() { return BUILT_IN; } + + public static Ingredient find(ItemStack stack) { + return BUILT_IN.stream().filter(ingredient -> stack.is(ingredient.item().getItem())).findFirst().orElse(null); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/InkVialExchange.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/InkVialExchange.java new file mode 100644 index 0000000..8845c5f --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/InkVialExchange.java @@ -0,0 +1,43 @@ +package net.dnorberg.rivencraft.writing; + +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.item.InkVialItem; +import net.minecraft.core.NonNullList; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +/** Shared, server-side vial-to-meter exchange from the 1.21.1 Desk and Mixer. */ +public final class InkVialExchange { + private InkVialExchange() {} + + public static boolean canPay(NonNullList items, int inkSlot, int bottleSlot, + int storedInk, int cost) { + return storedInk >= cost || canExchange(items, inkSlot, bottleSlot) + && storedInk + vialInk(items.get(inkSlot)) >= cost; + } + + /** Returns added ink; zero means no inputs were changed. */ + public static int exchange(NonNullList items, int inkSlot, int bottleSlot) { + if (!canExchange(items, inkSlot, bottleSlot)) return 0; + ItemStack vial = items.get(inkSlot); + int addedInk = vialInk(vial); + vial.shrink(1); + if (vial.isEmpty()) items.set(inkSlot, ItemStack.EMPTY); + ItemStack bottles = items.get(bottleSlot); + if (bottles.isEmpty()) items.set(bottleSlot, new ItemStack(Items.GLASS_BOTTLE)); + else bottles.grow(1); + return addedInk; + } + + private static boolean canExchange(NonNullList items, int inkSlot, int bottleSlot) { + ItemStack vial = items.get(inkSlot); + ItemStack bottles = items.get(bottleSlot); + return vial.is(Rivencraft.INK_VIAL.get()) && vialInk(vial) > 0 + && (bottles.isEmpty() || bottles.is(Items.GLASS_BOTTLE) + && bottles.getCount() < bottles.getMaxStackSize()); + } + + private static int vialInk(ItemStack vial) { + return Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/LinkingBookRecipe.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/LinkingBookRecipe.java index ebba3d2..1543d35 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/LinkingBookRecipe.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/LinkingBookRecipe.java @@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.writing; import com.mojang.serialization.MapCodec; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.item.PageItem; +import net.dnorberg.rivencraft.item.LinkProperties; import net.minecraft.core.component.DataComponents; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; @@ -53,6 +54,8 @@ public final class LinkingBookRecipe extends CustomRecipe { if (panel.has(DataComponents.CUSTOM_NAME)) { book.set(DataComponents.CUSTOM_NAME, panel.get(DataComponents.CUSTOM_NAME)); } + LinkProperties properties = panel.get(Rivencraft.LINK_PROPERTIES.get()); + if (properties != null) book.set(Rivencraft.LINK_PROPERTIES.get(), properties); return book; } diff --git a/ports/mc263/src/main/resources/assets/rivencraft/items/book_binder.json b/ports/mc263/src/main/resources/assets/rivencraft/items/book_binder.json new file mode 100644 index 0000000..bbf3add --- /dev/null +++ b/ports/mc263/src/main/resources/assets/rivencraft/items/book_binder.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "rivencraft:item/book_binder" + } +} diff --git a/ports/mc263/src/main/resources/assets/rivencraft/items/ink_mixer.json b/ports/mc263/src/main/resources/assets/rivencraft/items/ink_mixer.json new file mode 100644 index 0000000..05e4368 --- /dev/null +++ b/ports/mc263/src/main/resources/assets/rivencraft/items/ink_mixer.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "rivencraft:item/ink_mixer" + } +} diff --git a/ports/mc263/src/main/resources/assets/rivencraft/lang/en_us.json b/ports/mc263/src/main/resources/assets/rivencraft/lang/en_us.json index f2c9eee..0079c26 100644 --- a/ports/mc263/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/ports/mc263/src/main/resources/assets/rivencraft/lang/en_us.json @@ -5,11 +5,22 @@ "item.rivencraft.page": "Page", "item.rivencraft.page.blank": "Blank Page", "item.rivencraft.page.link_panel": "Link Panel Page", + "item.rivencraft.link_property.disarm": "Disarm", + "item.rivencraft.link_property.maintain_momentum": "Maintain Momentum", + "item.rivencraft.link_property.intra_linking": "Intra-Linking", "item.rivencraft.ink_vial": "Ink Vial", "item.rivencraft.ink_vial.remaining": "Ink: %s/%s", - "item.rivencraft.ink_vial.usage": "Use with paper in the other hand to prepare a Link Panel.", + "item.rivencraft.ink_vial.usage": "Use with paper in the other hand, or place in an Ink Mixer.", "item.rivencraft.page.blank.port_status": "Base for future symbol writing; not yet usable in this port.", - "item.rivencraft.page.link_panel.port_status": "Prepared for book binding; the Book Binder is not yet ported.", + "item.rivencraft.page.link_panel.port_status": "Use with leather in a Book Binder to make a Descriptive Book.", + "block.rivencraft.book_binder": "Book Binder", + "container.rivencraft.book_binder": "Book Binder", + "screen.rivencraft.book_binder.missing_panel": "Link Panel required", + "block.rivencraft.ink_mixer": "Ink Mixer", + "container.rivencraft.ink_mixer": "Ink Mixer", + "screen.rivencraft.ink_mixer.ink": "Ink: %s/%s", + "screen.rivencraft.ink_mixer.additive_hint": "Click with an additive to mix it in", + "screen.rivencraft.ink_mixer.chance": "%s: %s%%", "block.rivencraft.crystal_block": "Crystal Block", "block.rivencraft.budding_crystal": "Budding Crystal", "block.rivencraft.small_crystal_bud": "Small Crystal Bud", diff --git a/ports/mc263/src/main/resources/data/rivencraft/recipe/book_binder.json b/ports/mc263/src/main/resources/data/rivencraft/recipe/book_binder.json new file mode 100644 index 0000000..02c04d8 --- /dev/null +++ b/ports/mc263/src/main/resources/data/rivencraft/recipe/book_binder.json @@ -0,0 +1,10 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "pattern": ["III", "PPP", "P P"], + "key": { + "I": "minecraft:iron_ingot", + "P": "#minecraft:planks" + }, + "result": {"id": "rivencraft:book_binder", "count": 1} +} diff --git a/ports/mc263/src/main/resources/data/rivencraft/recipe/ink_mixer.json b/ports/mc263/src/main/resources/data/rivencraft/recipe/ink_mixer.json new file mode 100644 index 0000000..3042c49 --- /dev/null +++ b/ports/mc263/src/main/resources/data/rivencraft/recipe/ink_mixer.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "pattern": ["S S", "SVS", "PSP"], + "key": { + "S": "minecraft:stone", + "V": "minecraft:glass_bottle", + "P": "#minecraft:planks" + }, + "result": {"id": "rivencraft:ink_mixer", "count": 1} +}